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 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa692f3c9f
commit
18951b01d6
29 files changed
+2503
-1593
No files matched your search
@@ -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; }
|
||||
+24
-133
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/* 折叠卡片:<details class="aksrt-native-card aksrt-native-collapse"><summary>… */
|
||||
.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%; }
|
||||
}
|
||||
+75
-133
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
/**
|
||||
* 原生后台运营页共享 UI。
|
||||
*
|
||||
* 支付中心(aksrt_pay 菜单)与用户子菜单(users.php)的运营页统一使用这里的
|
||||
* 页面外壳、页头、统计卡、卡片、徽标与空状态。样式在 assets/css/admin/native-ui.css,
|
||||
* 只在 SLUGS 列出的页面入队,规则全部限定在 .aksrt-native 下。
|
||||
*
|
||||
* 输出方法直接 echo,参数在内部转义;$html 形参约定为调用方已转义的片段。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
final class NativeUi {
|
||||
|
||||
/** 使用共享 UI 的 admin page 参数。 */
|
||||
public const SLUGS = [
|
||||
'aksrt_pay',
|
||||
'aksrt_orders',
|
||||
'aksrt_pay_products',
|
||||
'aksrt_coupons',
|
||||
'aksrt_charge_cards',
|
||||
'aksrt_income',
|
||||
'aksrt_withdraw',
|
||||
'aksrt_invite_codes',
|
||||
'aksrt_messages',
|
||||
'aksrt_user_vip',
|
||||
'aksrt_user_auth',
|
||||
'aksrt_user_ban',
|
||||
'aksrt_tickets',
|
||||
];
|
||||
|
||||
public const STYLE_HANDLE = 'aksrt-admin-native-ui';
|
||||
|
||||
private const TONES = ['success', 'info', 'warning', 'danger', 'neutral', 'primary'];
|
||||
|
||||
private static $booted = false;
|
||||
|
||||
/** 由 PayMenus / UsersMenus 的 init() 调用,重复调用无副作用。 */
|
||||
public static function boot(): void {
|
||||
if (self::$booted) {
|
||||
return;
|
||||
}
|
||||
self::$booted = true;
|
||||
add_action('admin_enqueue_scripts', [self::class, 'enqueue']);
|
||||
}
|
||||
|
||||
public static function is_native_page(): bool {
|
||||
$page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
|
||||
return in_array($page, self::SLUGS, true);
|
||||
}
|
||||
|
||||
public static function enqueue(): void {
|
||||
if (!self::is_native_page()) {
|
||||
return;
|
||||
}
|
||||
$css = '/assets/css/admin/native-ui.css';
|
||||
if (is_file(AKSRT_DIR . $css)) {
|
||||
wp_enqueue_style(self::STYLE_HANDLE, AKSRT_URI . $css, [], (string) filemtime(AKSRT_DIR . $css));
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 页面外壳
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/** 打开 .wrap 外壳。$modifier 追加到 class(如页面专属根类)。 */
|
||||
public static function page_open(string $modifier = ''): void {
|
||||
$class = trim('wrap aksrt-native ' . $modifier);
|
||||
echo '<div class="' . esc_attr($class) . '">';
|
||||
}
|
||||
|
||||
public static function page_close(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 页头:图标 + 标题 + 说明 + 右侧操作。
|
||||
*
|
||||
* @param array<int, array{label:string,url:string,primary?:bool,icon?:string}> $actions
|
||||
*/
|
||||
public static function header(string $title, string $description = '', string $icon = '', array $actions = []): void {
|
||||
echo '<div class="aksrt-native-header"><div class="aksrt-native-header__main">';
|
||||
// h1 让 WP 把 admin_notices 挪到标题之后。
|
||||
echo '<h1 class="aksrt-native-header__title">';
|
||||
if ($icon !== '') {
|
||||
echo '<span class="aksrt-native-header__icon dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>';
|
||||
}
|
||||
echo esc_html($title) . '</h1>';
|
||||
if ($description !== '') {
|
||||
echo '<p class="aksrt-native-header__desc">' . esc_html($description) . '</p>';
|
||||
}
|
||||
echo '</div>';
|
||||
if ($actions !== []) {
|
||||
echo '<div class="aksrt-native-header__actions">';
|
||||
foreach ($actions as $action) {
|
||||
echo self::button_link($action['label'], $action['url'], !empty($action['primary']), (string) ($action['icon'] ?? ''));
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
// WP 会把 .notice 插在首个 .wrap h1 之后;这里留一个固定落点。
|
||||
echo '<hr class="wp-header-end">';
|
||||
}
|
||||
|
||||
public static function button_link(string $label, string $url, bool $primary = false, string $icon = ''): string {
|
||||
$class = $primary ? 'button button-primary' : 'button';
|
||||
$icon_html = $icon !== '' ? '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' : '';
|
||||
return '<a class="' . esc_attr($class) . '" href="' . esc_url($url) . '">' . $icon_html . esc_html($label) . '</a>';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 统计卡
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @param array<int, array{label:string,value:string|int|float,url?:string,tone?:string,icon?:string,hint?:string,unit?:string}> $items
|
||||
*/
|
||||
public static function stats(array $items): void {
|
||||
if ($items === []) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="aksrt-native-stats">';
|
||||
foreach ($items as $item) {
|
||||
self::stat($item);
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/** @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 '<span class="aksrt-native-stat__label">';
|
||||
if (!empty($item['icon'])) {
|
||||
echo '<span class="dashicons ' . esc_attr((string) $item['icon']) . '" aria-hidden="true"></span>';
|
||||
}
|
||||
echo esc_html($item['label']) . '</span>';
|
||||
echo '<span class="aksrt-native-stat__value">';
|
||||
if (!empty($item['unit'])) {
|
||||
echo '<span class="aksrt-native-stat__unit">' . esc_html((string) $item['unit']) . '</span>';
|
||||
}
|
||||
echo esc_html((string) $item['value']) . '</span>';
|
||||
if (!empty($item['hint'])) {
|
||||
echo '<span class="aksrt-native-stat__hint">' . esc_html((string) $item['hint']) . '</span>';
|
||||
}
|
||||
echo '</' . $tag . '>';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 卡片
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 打开卡片。$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 '<section class="' . esc_attr($class) . '">';
|
||||
if ($title !== '' || $actions_html !== '') {
|
||||
echo '<div class="aksrt-native-card__head">';
|
||||
if ($title !== '') {
|
||||
echo '<h2 class="aksrt-native-card__title">' . esc_html($title) . '</h2>';
|
||||
}
|
||||
if ($actions_html !== '') {
|
||||
echo '<div class="aksrt-native-card__actions">' . $actions_html . '</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
echo '<div class="aksrt-native-card__body">';
|
||||
}
|
||||
|
||||
public static function card_close(): void {
|
||||
echo '</div></section>';
|
||||
}
|
||||
|
||||
/** 表格卡:整表铺满卡片,带横向滚动容器。 */
|
||||
public static function table_card_open(string $title = '', string $actions_html = ''): void {
|
||||
self::card_open($title, $actions_html, true);
|
||||
echo '<div class="aksrt-native-table-wrap">';
|
||||
}
|
||||
|
||||
/** 关闭表格卡;分页条应在 table-wrap 关闭后、卡片关闭前输出,故拆成两步。 */
|
||||
public static function table_wrap_close(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 徽标 / 空状态
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
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 '<span class="' . esc_attr($class) . '">' . esc_html($label) . '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 按状态键取徽标。$map: 状态键 => [文案, 色调];未命中时原样输出灰色徽标。
|
||||
*
|
||||
* @param array<string, array{0:string,1:string}> $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 = '<div class="aksrt-native-empty">';
|
||||
$html .= '<span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>';
|
||||
$html .= '<span class="aksrt-native-empty__title">' . esc_html($title) . '</span>';
|
||||
if ($hint !== '') {
|
||||
$html .= '<span>' . esc_html($hint) . '</span>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
/** 表格内的空状态整行。 */
|
||||
public static function empty_row(int $colspan, string $title, string $hint = ''): string {
|
||||
return '<tr class="no-items"><td class="aksrt-native-empty-cell" colspan="' . esc_attr((string) max(1, $colspan)) . '">'
|
||||
. self::empty_state($title, $hint)
|
||||
. '</td></tr>';
|
||||
}
|
||||
|
||||
/** 用户单元格:头像 + 名称 + #ID。 */
|
||||
public static function user_cell(int $user_id, string $fallback = ''): string {
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : false;
|
||||
if (!$user) {
|
||||
return '<span class="aksrt-native-muted">' . esc_html($fallback !== '' ? $fallback : ($user_id > 0 ? '#' . $user_id : '—')) . '</span>';
|
||||
}
|
||||
$avatar = function_exists('aksrt_get_avatar_url') ? (string) aksrt_get_avatar_url($user_id, 28) : (string) get_avatar_url($user_id, ['size' => 28]);
|
||||
return '<a class="aksrt-native-user" href="' . esc_url(get_edit_user_link($user_id)) . '">'
|
||||
. ($avatar !== '' ? '<img src="' . esc_url($avatar) . '" width="28" height="28" alt="">' : '')
|
||||
. '<span><span class="aksrt-native-user__name">' . esc_html($user->display_name) . '</span>'
|
||||
. '<span class="aksrt-native-user__meta">#' . esc_html((string) $user_id) . '</span></span></a>';
|
||||
}
|
||||
|
||||
private static function tone(string $tone): string {
|
||||
return in_array($tone, self::TONES, true) ? $tone : 'neutral';
|
||||
}
|
||||
}
|
||||
@@ -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 '<div class="wrap"><h1>' . esc_html__('卡密管理', 'aksrt') . '</h1>';
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('卡密模块未初始化。', 'aksrt') . '</p></div></div>';
|
||||
NativeUi::page_open();
|
||||
echo '<h1 class="wp-heading-inline">' . esc_html__('卡密管理', 'aksrt') . '</h1>';
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('卡密模块未初始化。', 'aksrt') . '</p></div>';
|
||||
NativeUi::page_close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,10 +96,11 @@ class PayChargeCards {
|
||||
break;
|
||||
}
|
||||
|
||||
echo '<div class="wrap">';
|
||||
NativeUi::page_open();
|
||||
$this->print_assets();
|
||||
echo '<h1 class="wp-heading-inline">' . esc_html($page_title) . '</h1>';
|
||||
echo $head_but; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
|
||||
echo '<hr class="wp-header-end">';
|
||||
|
||||
if ($tab === 'add') {
|
||||
$this->render_add_form();
|
||||
@@ -97,463 +110,7 @@ class PayChargeCards {
|
||||
$this->render_list($s);
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 列表
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @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 .= '<li><a href="' . esc_url($but['href']) . '">' . esc_html($but['name']) . '</a></li> | ';
|
||||
}
|
||||
$but_html = substr($but_html, 0, -2);
|
||||
|
||||
$search = '<form class="form-inline form-order" method="post">
|
||||
<div class="form-group" style="float: right;">
|
||||
<input type="text" class="form-control" name="s" placeholder="' . esc_attr__('搜索卡密', 'aksrt') . '">
|
||||
<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</button>
|
||||
</div>
|
||||
</form>';
|
||||
|
||||
// 当前筛选参数(不含 paged)。分页链接显式带上它,不再依赖 add_query_arg()
|
||||
// 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。
|
||||
$query_args = $this->list_query_args($status, $type, $other, $s, $orderby, $order, $per_page);
|
||||
|
||||
echo '<div class="order-header"><ul class="subsubsub">' . $but_html . '</ul>' . $search . '</div>';
|
||||
|
||||
$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 '<div class="clear"></div>';
|
||||
echo '<table class="widefat fixed striped posts table table-bordered" style="min-width: 1000px;">';
|
||||
echo '<thead><tr><td style="color: #ff4a4a;text-align: center;">' . esc_html__('未找到对应内容,或暂无内容', 'aksrt') . '</td></tr></thead>';
|
||||
echo '</table>';
|
||||
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 = '<td id="cb" class="manage-column column-cb check-column" style="width: 2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'aksrt') . '</label><input id="cb-select-all-1" type="checkbox"></td>';
|
||||
foreach ($theads as $thead) {
|
||||
$orderby_links = '';
|
||||
if ($thead['orderby']) {
|
||||
$orderby_url = add_query_arg('orderby', $thead['orderby']);
|
||||
$orderby_links .= '<a title="' . esc_attr__('降序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
|
||||
$orderby_links .= '<a title="' . esc_attr__('升序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'DESC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-down"></span></a>';
|
||||
$orderby_links = '<span class="orderby-but">' . $orderby_links . '</span>';
|
||||
}
|
||||
$thead_th .= '<th class="" width="' . esc_attr($thead['width']) . '">' . esc_html($thead['name']) . $orderby_links . '</th>';
|
||||
}
|
||||
|
||||
$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 = '<span style="color: #3d7ffd;">' . esc_html__('未使用', 'aksrt') . '</span>';
|
||||
if ($msg['status'] === 'used') {
|
||||
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'aksrt') . '</span>';
|
||||
if (!empty($msg['order_number'])) {
|
||||
$order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number']));
|
||||
$status_html .= '<a style="color: #ff6215;" target="_blank" href="' . esc_url($order_link_url) . '"> [' . esc_html__('查看', 'aksrt') . ']</a>';
|
||||
}
|
||||
} elseif ($msg['status'] === 'expired') {
|
||||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已过期', 'aksrt') . '</span>';
|
||||
} elseif ($msg['status'] === 'disabled') {
|
||||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已禁用', 'aksrt') . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
$other_a = '';
|
||||
if (!empty($msg['note'])) {
|
||||
$other_a = '<a href="' . esc_url(add_query_arg('other', $msg['note'], $this->page_url)) . '">' . esc_html($msg['note']) . '</a>';
|
||||
}
|
||||
|
||||
if ($msg['type'] === 'vip') {
|
||||
$type_html = '<a style="color:#975106" href="' . esc_url(add_query_arg('type', 'vip', $this->page_url)) . '">' . esc_html__('会员兑换', 'aksrt') . '</a>';
|
||||
$level = (int) ($meta['level'] ?? 1);
|
||||
$type_html .= '<br>' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta));
|
||||
} elseif ($msg['type'] === 'credits') {
|
||||
$type_html = '<a style="color:#289d0f" href="' . esc_url(add_query_arg('type', 'credits', $this->page_url)) . '">' . esc_html__('积分兑换', 'aksrt') . '</a>';
|
||||
$type_html .= '<br>' . esc_html__('积分:', 'aksrt') . esc_html((string) (int) ($meta['points'] ?? 0));
|
||||
} elseif ($msg['type'] === 'balance') {
|
||||
$type_html = '<a style="color:#0c76e6" href="' . esc_url(add_query_arg('type', 'balance', $this->page_url)) . '">' . esc_html__('余额充值', 'aksrt') . '</a>';
|
||||
$type_html .= '<br>' . esc_html__('金额:', 'aksrt') . esc_html($this->balance_amount_text($meta));
|
||||
} else {
|
||||
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'aksrt') . '</a>';
|
||||
}
|
||||
|
||||
$card_name = (string) ($meta['custom_name']['card_name'] ?? '');
|
||||
$password_name = (string) ($meta['custom_name']['password_name'] ?? '');
|
||||
|
||||
$tbody .= '<tr>';
|
||||
$tbody .= '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . (int) $msg['id'] . '">' . esc_html__('选择', 'aksrt') . '</label>
|
||||
<input id="cb-select-' . (int) $msg['id'] . '" type="checkbox" name="action_id[]" value="' . (int) $msg['id'] . '">
|
||||
</th>';
|
||||
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['card']) . "' data-clipboard-tag=\"" . esc_attr($card_name !== '' ? $card_name : __('卡号', 'aksrt')) . "\">" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . '</span></td>';
|
||||
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['password']) . "' data-clipboard-tag=\"" . esc_attr($password_name !== '' ? $password_name : __('密码', 'aksrt')) . "\">" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . '</span></td>';
|
||||
$tbody .= "<td>$type_html</td>";
|
||||
$tbody .= '<td>' . esc_html((string) $msg['created_at']) . '</td>';
|
||||
$tbody .= '<td>' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . '</td>';
|
||||
$tbody .= "<td>$status_html</td>";
|
||||
$tbody .= "<td>$other_a</td>";
|
||||
$tbody .= '</tr>';
|
||||
}
|
||||
|
||||
echo '<div class="clear"></div>';
|
||||
echo '<form class="" method="post">';
|
||||
wp_nonce_field(self::NONCE);
|
||||
echo '<div class="bulkactions" style="margin: 10px 0;">
|
||||
<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'aksrt') . '</label><select name="action" id="bulk-action-selector-top">
|
||||
<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>
|
||||
<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>
|
||||
</select>
|
||||
<input type="submit" class="button action" value="' . esc_attr__('应用', 'aksrt') . '">
|
||||
</div>';
|
||||
|
||||
echo '<div style="overflow-y: auto;width: 100%;">';
|
||||
echo '<table class="widefat fixed striped posts table table-bordered" style="min-width: 1000px;">';
|
||||
echo '<thead><tr>' . $thead_th . '</tr></thead>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
|
||||
echo '<tbody>' . $tbody . '</tbody>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
|
||||
echo '</table>';
|
||||
echo '</div>';
|
||||
echo '</form>';
|
||||
echo '<div class="clear"></div>';
|
||||
|
||||
AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 添加卡密
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
private function render_add_form(): void {
|
||||
$type_labels = $this->type_labels();
|
||||
?>
|
||||
<p><b><?php esc_html_e('在此添加卡密', 'aksrt'); ?></b></p>
|
||||
<ul style="list-style:disc;margin:0 0 12px 18px;line-height:1.9">
|
||||
<li><?php esc_html_e('如果您已经准备好了卡密资料,请选择导入的方式添加', 'aksrt'); ?></li>
|
||||
<li><?php esc_html_e('您也可以采用系统生成的方式,自动批量添加卡密', 'aksrt'); ?></li>
|
||||
</ul>
|
||||
<form class="aksrt-cardpass-form" method="post">
|
||||
<?php wp_nonce_field(self::NONCE); ?>
|
||||
<input type="hidden" name="action" value="add" />
|
||||
<table class="form-table" role="presentation">
|
||||
<?php
|
||||
$this->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'),
|
||||
'<input type="text" class="regular-text" name="auto_remarks" value="' . esc_attr('cardpass_' . current_time('YmdHis')) . '" />',
|
||||
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
|
||||
__('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '<br>' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), '<code>shipped</code>')
|
||||
);
|
||||
$this->field_row(
|
||||
__('面额', 'aksrt'),
|
||||
'<input type="number" step="any" name="auto_price" value="0" />',
|
||||
[
|
||||
['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') . '<br><input type="text" name="vip_exchange[time]" value="" />',
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||||
],
|
||||
__('填', 'aksrt') . '<code>Permanent</code>' . __('为永久', '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') . '<br><input type="text" class="regular-text" name="custom_name[card_name]" value="" /><br>' .
|
||||
__('卡密显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[password_name]" value="" />',
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||||
]
|
||||
);
|
||||
$this->field_row(
|
||||
__('高级选项', 'aksrt'),
|
||||
'<input type="checkbox" name="auto_top_s" value="1" />',
|
||||
[['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'),
|
||||
'<input type="checkbox" name="only_pass_s" value="1" />',
|
||||
[
|
||||
['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'],
|
||||
],
|
||||
'<p><b>' . esc_html__('导入卡密', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '</div>
|
||||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '</div>'
|
||||
);
|
||||
$this->submessage_row(
|
||||
[
|
||||
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||||
],
|
||||
'<p><b>' . esc_html__('导入会员兑换卡密', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('会员参数格式为:', 'aksrt') . '<code>level=1&time=5&unit=day</code></li>
|
||||
<li>' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '</div>
|
||||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '</div>
|
||||
'
|
||||
);
|
||||
$this->submessage_row(
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||||
['key' => 'only_pass_s', 'op' => 'unchecked'],
|
||||
],
|
||||
'<p><b>' . esc_html__('自定义卡密导入', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>card_name=授权号&password_name=授权密码</code>,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||||
<div>111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '</div>'
|
||||
);
|
||||
$this->submessage_row(
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||||
['key' => 'only_pass_s', 'op' => 'checked'],
|
||||
],
|
||||
'<p><b>' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('密码 标识 自定义参数', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>password_name=授权密码</code>,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||||
<div>SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>'
|
||||
);
|
||||
$this->field_row(
|
||||
__('卡密数据', 'aksrt'),
|
||||
'<textarea name="import_data" rows="10" class="large-text code" style="resize: both;max-width: none;"></textarea>',
|
||||
[['key' => 'add_type', 'op' => '==', 'val' => 'import']]
|
||||
);
|
||||
$this->field_row(
|
||||
__('自定义分隔符号', 'aksrt'),
|
||||
'<input type="text" class="mini-input" style="width:80px;" name="import_division" value=" " />',
|
||||
[['key' => 'add_type', 'op' => '==', 'val' => 'import']],
|
||||
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
|
||||
);
|
||||
?>
|
||||
</table>
|
||||
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 导出卡密
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
private function render_export_form(): void {
|
||||
?>
|
||||
<form class="aksrt-cardpass-form" method="post" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>">
|
||||
<input type="hidden" name="action" value="<?php echo esc_attr(self::EXPORT_AJAX_ACTION); ?>" />
|
||||
<table class="form-table" role="presentation">
|
||||
<?php
|
||||
$this->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') . '<br><input type="text" class="mini-input" style="width:80px;" name="text_division" value=" " />',
|
||||
[['key' => 'export_format', 'op' => '==', 'val' => 'text']],
|
||||
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
|
||||
);
|
||||
?>
|
||||
</table>
|
||||
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
|
||||
</form>
|
||||
<?php
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -861,330 +418,6 @@ class PayChargeCards {
|
||||
$this->page_notice(__('删除完成', 'aksrt'), sprintf(__('已删除%s个卡密', 'aksrt'), (string) $delete_i));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 渲染辅助
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @param array<int,array{key:string,op:string,val:string}> $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 '<tr' . $dep_attr . $hidden . '><th scope="row">' . esc_html($title) . '</th><td>' . $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
|
||||
if ($desc !== '') {
|
||||
echo '<p class="description">' . wp_kses_post($desc) . '</p>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array{key:string,op:string,val:string}> $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 与初始隐藏状态同时挂在 <tr> 和内层提示块上:提示块若脱离所在行,仍能自行显隐。
|
||||
echo '<tr' . $dep_attr . $hidden . '><td colspan="2"><div' . $dep_attr . ' class="aksrt-submsg"' . $hidden . '>' . $content . '</div></td></tr>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端按表单默认值预判依赖可见性(求值规则与行内 JS 完全一致)。
|
||||
* 仅在表单渲染默认值(添加方式=系统自动生成、类型=余额)下,把不满足依赖的行标记为初始隐藏,
|
||||
* 避免四条导入提示首帧铺开。
|
||||
*
|
||||
* 注意:已知键必须覆盖 field_row/submessage_row 用到的所有依赖键,否则该键的判定会与 JS 不一致,
|
||||
* 出现「服务端判可见、JS 判隐藏」之类的错配(例如自定义卡密的两条导入提示同时铺开)。
|
||||
*
|
||||
* @param array<int,array{key:string,op:string,val:string}> $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<string,string> $options
|
||||
*/
|
||||
private function button_set(string $name, array $options, string $current): string {
|
||||
$html = '<div class="aksrt-btnset">';
|
||||
foreach ($options as $value => $label) {
|
||||
$html .= '<label class="aksrt-btnset-item"><input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . checked((string) $current, (string) $value, false) . '><span>' . esc_html((string) $label) . '</span></label>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字输入(参考主题 CSF spinner / number 字段的原生替代)。
|
||||
*
|
||||
* **一律不写原生 min / max / step 约束属性,只写 `step="any"`:**
|
||||
* - 本表单的行是条件显隐的(`field_row()` 会给不满足依赖的 `<tr>` 打上 `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 '<input type="number" class="small-text" step="any" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" />'
|
||||
. ' <span class="description">' . esc_html($unit) . '</span>';
|
||||
}
|
||||
|
||||
private function page_notice(string $title, string $msg, string $type = 'success'): void {
|
||||
$html = '';
|
||||
$html .= $title ? '<h3>' . esc_html($title) . '</h3>' : '';
|
||||
$html .= $msg ? '<p>' . esc_html($msg) . '</p>' : '';
|
||||
|
||||
if ($html) {
|
||||
echo '<div class="notice notice-' . esc_attr($type) . '">' . $html . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表筛选参数(不含 paged / page),用于构造翻页链接与「每页显示」表单。
|
||||
* 显式列条件而不是依赖 add_query_arg() 默认取 $_SERVER['REQUEST_URI']:
|
||||
* 后者把链接绑到当前请求上,一旦请求形态变化(POST、代理改写)就会丢条件。
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
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 {
|
||||
?>
|
||||
<style>
|
||||
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
||||
.order-header .subsubsub { margin: 6px 0; }
|
||||
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
||||
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
||||
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
||||
两枚中心分别落在基线以上 .725em / .075em,对称于文字视觉中心(约 .4em)。
|
||||
不要改回「无尺寸 inline 宿主 + 依赖静态位置 + 固定 -3px/+6px」的写法:
|
||||
静态位置随表头字体/字号变化,会让整组箭头相对文字偏下(历史缺陷,已量测确认偏下 2px)。 */
|
||||
.orderby-but { display: inline-block; position: relative; width: 0; height: 0; }
|
||||
.orderby-but > a { opacity: .4; position: absolute; left: 0; bottom: .725em; transform: translateY(50%); transition: opacity .3s; padding-right: 1em; }
|
||||
.orderby-but > a + a { bottom: .075em; }
|
||||
.orderby-but:hover a { opacity: .6; }
|
||||
.orderby-but > a:hover { opacity: 1; }
|
||||
.aksrt-btnset { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.aksrt-btnset-item { display: inline-block; cursor: pointer; }
|
||||
.aksrt-btnset-item input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.aksrt-btnset-item span { display: inline-block; padding: 3px 12px; border: 1px solid #c3c4c7; border-radius: 3px; background: #fff; }
|
||||
.aksrt-btnset-item input:checked + span { background: #2271b1; border-color: #2271b1; color: #fff; }
|
||||
/* 导入提示:复刻后台 notice-warning 的外观,但**不用** .notice 类 —— 见 submessage_row()
|
||||
的说明,带 .notice 会被 wp-admin/js/common.js 搬到页面顶部。改这里请连带改那里。 */
|
||||
.aksrt-submsg { margin: 6px 0; padding: 6px 12px; background: #fef8ee; border-left: 4px solid #f0b849; }
|
||||
/* 提示内的条目列表必须有 <ul> 承载:list-style-position 默认 outside,
|
||||
圆点落在内容盒左侧的 padding 区里;若 <li> 没有列表容器提供缩进,
|
||||
圆点就会溢出到提示块的文字边界之外。这里显式给出缩进,避免依赖 UA/后台的 ul 默认值。 */
|
||||
.aksrt-submsg-list { margin: 6px 0; padding-left: 20px; list-style: disc; }
|
||||
.aksrt-copy-tip { margin-left: 6px; color: #00a32a; font-size: 12px; white-space: nowrap; }
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
// 按 name 属性逐元素匹配,避免把字段名拼进 CSS 选择器:
|
||||
// 含方括号的字段名(如 vip_exchange[time])会让选择器抛异常并被 catch 吞掉,
|
||||
// 从而把依赖行永久留在初始状态。
|
||||
function fieldsByName(key) {
|
||||
var all = document.querySelectorAll('.aksrt-cardpass-form [name]');
|
||||
var out = [];
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
if (all[i].getAttribute('name') === key) out.push(all[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function formVal(key) {
|
||||
var els = fieldsByName(key);
|
||||
if (!els.length) return '';
|
||||
if (els[0].type === 'radio') {
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
if (els[i].checked) return els[i].value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (els[0].type === 'checkbox') return els[0].checked ? '1' : '';
|
||||
return els[0].value;
|
||||
}
|
||||
function evalDeps() {
|
||||
// 全文档查找 data-dep 节点:行与提示块各自带状态、各自落 display。
|
||||
// 刻意不做限域(不写 .aksrt-cardpass-form 前缀):提示块一旦被 HTML 解析
|
||||
// 移出表格甚至移出表单子树,限域查找会让它停在初始隐藏状态,表现为「提示不见了」。
|
||||
document.querySelectorAll('[data-dep]').forEach(function (el) {
|
||||
try {
|
||||
var deps = JSON.parse(el.getAttribute('data-dep'));
|
||||
var show = deps.every(function (d) {
|
||||
var v = formVal(d.key);
|
||||
switch (d.op) {
|
||||
case '==': return String(v) === String(d.val);
|
||||
case '!=': return String(v) !== String(d.val);
|
||||
case 'any': return String(d.val).split(',').indexOf(String(v)) !== -1;
|
||||
case 'checked': return !!v;
|
||||
case 'unchecked': return !v;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
el.style.display = show ? '' : 'none';
|
||||
} catch (e) { /* noop */ }
|
||||
});
|
||||
}
|
||||
// 任何 change 都重新求值:不做「是否属于本表单」的限定,
|
||||
// 因为提示块的祖先链在这种页面上并不保证还挂在表单内。
|
||||
document.addEventListener('change', evalDeps);
|
||||
// 唯一的初始求值入口:本脚本在表单之前输出,此刻解析尚未到达表单,
|
||||
// 直接求值必然取不到任何行(空操作);因此必须等 DOM 就绪后按默认值校正一次。
|
||||
// 服务端已按默认值预隐藏,这里只做兜底校正。
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', evalDeps);
|
||||
} else {
|
||||
evalDeps();
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var el = e.target.closest('[data-clipboard-text]');
|
||||
if (!el) return;
|
||||
var text = el.getAttribute('data-clipboard-text') || '';
|
||||
if (!text) return;
|
||||
var done = function () {
|
||||
var tip = document.createElement('span');
|
||||
tip.className = 'aksrt-copy-tip';
|
||||
var tag = el.getAttribute('data-clipboard-tag') || '';
|
||||
tip.textContent = (tag ? tag + ' ' : '') + '<?php echo esc_js(__('已复制', 'aksrt')); ?>';
|
||||
el.appendChild(tip);
|
||||
setTimeout(function () { tip.remove(); }, 1200);
|
||||
};
|
||||
var legacy = function () {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); } catch (err) { /* noop */ }
|
||||
document.body.removeChild(ta);
|
||||
done();
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(done, legacy);
|
||||
} else {
|
||||
legacy();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 数据辅助
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*
|
||||
* PayChargeCards 页面专属内联样式与脚本:条件依赖显隐 + 点击复制。
|
||||
* 这些是卡密管理页特有的表现(排序箭头基线锚定、按钮组、导入提示块、复制提示),
|
||||
* 不属于共享 native-ui.css 的通用组件,故随页面内联输出。
|
||||
*
|
||||
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait PayChargeCardsAssets {
|
||||
|
||||
/** 行内样式与脚本:依赖显隐 + 点击复制。 */
|
||||
private function print_assets(): void {
|
||||
?>
|
||||
<style>
|
||||
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
||||
.order-header .subsubsub { margin: 6px 0; }
|
||||
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
||||
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
||||
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
||||
两枚中心分别落在基线以上 .725em / .075em,对称于文字视觉中心(约 .4em)。
|
||||
不要改回「无尺寸 inline 宿主 + 依赖静态位置 + 固定 -3px/+6px」的写法:
|
||||
静态位置随表头字体/字号变化,会让整组箭头相对文字偏下(历史缺陷,已量测确认偏下 2px)。 */
|
||||
.orderby-but { display: inline-block; position: relative; width: 0; height: 0; }
|
||||
.orderby-but > a { opacity: .4; position: absolute; left: 0; bottom: .725em; transform: translateY(50%); transition: opacity .3s; padding-right: 1em; }
|
||||
.orderby-but > a + a { bottom: .075em; }
|
||||
.orderby-but:hover a { opacity: .6; }
|
||||
.orderby-but > a:hover { opacity: 1; }
|
||||
.aksrt-cardpass-bulk { margin: 0 0 12px; }
|
||||
/* 卡密表列多(≥10 列),窄视口下须保持最小宽度并由表格卡片横向滚动,
|
||||
而非让各列挤扁。复刻原先列表表格的内联 style="min-width: 1000px;"。 */
|
||||
.aksrt-cardpass-table { min-width: 1000px; }
|
||||
.aksrt-btnset { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.aksrt-btnset-item { display: inline-block; cursor: pointer; }
|
||||
.aksrt-btnset-item input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.aksrt-btnset-item span { display: inline-block; padding: 3px 12px; border: 1px solid #c3c4c7; border-radius: 3px; background: #fff; }
|
||||
.aksrt-btnset-item input:checked + span { background: #2271b1; border-color: #2271b1; color: #fff; }
|
||||
/* 导入提示:复刻后台 notice-warning 的外观,但**不用** .notice 类 —— 见 submessage_row()
|
||||
的说明,带 .notice 会被 wp-admin/js/common.js 搬到页面顶部。改这里请连带改那里。 */
|
||||
.aksrt-submsg { margin: 6px 0; padding: 6px 12px; background: #fef8ee; border-left: 4px solid #f0b849; }
|
||||
/* 提示内的条目列表必须有 <ul> 承载:list-style-position 默认 outside,
|
||||
圆点落在内容盒左侧的 padding 区里;若 <li> 没有列表容器提供缩进,
|
||||
圆点就会溢出到提示块的文字边界之外。这里显式给出缩进,避免依赖 UA/后台的 ul 默认值。 */
|
||||
.aksrt-submsg-list { margin: 6px 0; padding-left: 20px; list-style: disc; }
|
||||
.aksrt-copy-tip { margin-left: 6px; color: #00a32a; font-size: 12px; white-space: nowrap; }
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
// 按 name 属性逐元素匹配,避免把字段名拼进 CSS 选择器:
|
||||
// 含方括号的字段名(如 vip_exchange[time])会让选择器抛异常并被 catch 吞掉,
|
||||
// 从而把依赖行永久留在初始状态。
|
||||
function fieldsByName(key) {
|
||||
var all = document.querySelectorAll('.aksrt-cardpass-form [name]');
|
||||
var out = [];
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
if (all[i].getAttribute('name') === key) out.push(all[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function formVal(key) {
|
||||
var els = fieldsByName(key);
|
||||
if (!els.length) return '';
|
||||
if (els[0].type === 'radio') {
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
if (els[i].checked) return els[i].value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if (els[0].type === 'checkbox') return els[0].checked ? '1' : '';
|
||||
return els[0].value;
|
||||
}
|
||||
function evalDeps() {
|
||||
// 全文档查找 data-dep 节点:行与提示块各自带状态、各自落 display。
|
||||
// 刻意不做限域(不写 .aksrt-cardpass-form 前缀):提示块一旦被 HTML 解析
|
||||
// 移出表格甚至移出表单子树,限域查找会让它停在初始隐藏状态,表现为「提示不见了」。
|
||||
document.querySelectorAll('[data-dep]').forEach(function (el) {
|
||||
try {
|
||||
var deps = JSON.parse(el.getAttribute('data-dep'));
|
||||
var show = deps.every(function (d) {
|
||||
var v = formVal(d.key);
|
||||
switch (d.op) {
|
||||
case '==': return String(v) === String(d.val);
|
||||
case '!=': return String(v) !== String(d.val);
|
||||
case 'any': return String(d.val).split(',').indexOf(String(v)) !== -1;
|
||||
case 'checked': return !!v;
|
||||
case 'unchecked': return !v;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
el.style.display = show ? '' : 'none';
|
||||
} catch (e) { /* noop */ }
|
||||
});
|
||||
}
|
||||
// 任何 change 都重新求值:不做「是否属于本表单」的限定,
|
||||
// 因为提示块的祖先链在这种页面上并不保证还挂在表单内。
|
||||
document.addEventListener('change', evalDeps);
|
||||
// 唯一的初始求值入口:本脚本在表单之前输出,此刻解析尚未到达表单,
|
||||
// 直接求值必然取不到任何行(空操作);因此必须等 DOM 就绪后按默认值校正一次。
|
||||
// 服务端已按默认值预隐藏,这里只做兜底校正。
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', evalDeps);
|
||||
} else {
|
||||
evalDeps();
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var el = e.target.closest('[data-clipboard-text]');
|
||||
if (!el) return;
|
||||
var text = el.getAttribute('data-clipboard-text') || '';
|
||||
if (!text) return;
|
||||
var done = function () {
|
||||
var tip = document.createElement('span');
|
||||
tip.className = 'aksrt-copy-tip';
|
||||
var tag = el.getAttribute('data-clipboard-tag') || '';
|
||||
tip.textContent = (tag ? tag + ' ' : '') + '<?php echo esc_js(__('已复制', 'aksrt')); ?>';
|
||||
el.appendChild(tip);
|
||||
setTimeout(function () { tip.remove(); }, 1200);
|
||||
};
|
||||
var legacy = function () {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); } catch (err) { /* noop */ }
|
||||
document.body.removeChild(ta);
|
||||
done();
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(done, legacy);
|
||||
} else {
|
||||
legacy();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*
|
||||
* PayChargeCards 的「添加卡密 / 导出卡密」表单渲染与依赖显隐求值。
|
||||
* 表单结构、字段依赖(field_row / submessage_row / deps_visible)、按钮组与数字输入
|
||||
* 均与参考主题 1:1 对齐,由 charge-cards-dependency / charge-cards-form-constraints 两组契约锁定。
|
||||
*
|
||||
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait PayChargeCardsForms {
|
||||
|
||||
private function render_add_form(): void {
|
||||
$type_labels = $this->type_labels();
|
||||
?>
|
||||
<p><b><?php esc_html_e('在此添加卡密', 'aksrt'); ?></b></p>
|
||||
<ul style="list-style:disc;margin:0 0 12px 18px;line-height:1.9">
|
||||
<li><?php esc_html_e('如果您已经准备好了卡密资料,请选择导入的方式添加', 'aksrt'); ?></li>
|
||||
<li><?php esc_html_e('您也可以采用系统生成的方式,自动批量添加卡密', 'aksrt'); ?></li>
|
||||
</ul>
|
||||
<form class="aksrt-cardpass-form" method="post">
|
||||
<?php wp_nonce_field(self::NONCE); ?>
|
||||
<input type="hidden" name="action" value="add" />
|
||||
<table class="form-table" role="presentation">
|
||||
<?php
|
||||
$this->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'),
|
||||
'<input type="text" class="regular-text" name="auto_remarks" value="' . esc_attr('cardpass_' . current_time('YmdHis')) . '" />',
|
||||
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
|
||||
__('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '<br>' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), '<code>shipped</code>')
|
||||
);
|
||||
$this->field_row(
|
||||
__('面额', 'aksrt'),
|
||||
'<input type="number" step="any" name="auto_price" value="0" />',
|
||||
[
|
||||
['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') . '<br><input type="text" name="vip_exchange[time]" value="" />',
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||||
],
|
||||
__('填', 'aksrt') . '<code>Permanent</code>' . __('为永久', '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') . '<br><input type="text" class="regular-text" name="custom_name[card_name]" value="" /><br>' .
|
||||
__('卡密显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[password_name]" value="" />',
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||||
]
|
||||
);
|
||||
$this->field_row(
|
||||
__('高级选项', 'aksrt'),
|
||||
'<input type="checkbox" name="auto_top_s" value="1" />',
|
||||
[['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'),
|
||||
'<input type="checkbox" name="only_pass_s" value="1" />',
|
||||
[
|
||||
['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'],
|
||||
],
|
||||
'<p><b>' . esc_html__('导入卡密', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '</div>
|
||||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '</div>'
|
||||
);
|
||||
$this->submessage_row(
|
||||
[
|
||||
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||||
],
|
||||
'<p><b>' . esc_html__('导入会员兑换卡密', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('会员参数格式为:', 'aksrt') . '<code>level=1&time=5&unit=day</code></li>
|
||||
<li>' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '</div>
|
||||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '</div>
|
||||
'
|
||||
);
|
||||
$this->submessage_row(
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||||
['key' => 'only_pass_s', 'op' => 'unchecked'],
|
||||
],
|
||||
'<p><b>' . esc_html__('自定义卡密导入', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>card_name=授权号&password_name=授权密码</code>,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||||
<div>111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '</div>'
|
||||
);
|
||||
$this->submessage_row(
|
||||
[
|
||||
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
|
||||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||||
['key' => 'only_pass_s', 'op' => 'checked'],
|
||||
],
|
||||
'<p><b>' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '</b></p>
|
||||
<ul class="aksrt-submsg-list">
|
||||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('密码 标识 自定义参数', 'aksrt') . '</code></li>
|
||||
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
|
||||
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>password_name=授权密码</code>,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
|
||||
</ul>
|
||||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||||
<div>3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||||
<div>SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>'
|
||||
);
|
||||
$this->field_row(
|
||||
__('卡密数据', 'aksrt'),
|
||||
'<textarea name="import_data" rows="10" class="large-text code" style="resize: both;max-width: none;"></textarea>',
|
||||
[['key' => 'add_type', 'op' => '==', 'val' => 'import']]
|
||||
);
|
||||
$this->field_row(
|
||||
__('自定义分隔符号', 'aksrt'),
|
||||
'<input type="text" class="mini-input" style="width:80px;" name="import_division" value=" " />',
|
||||
[['key' => 'add_type', 'op' => '==', 'val' => 'import']],
|
||||
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
|
||||
);
|
||||
?>
|
||||
</table>
|
||||
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function render_export_form(): void {
|
||||
?>
|
||||
<form class="aksrt-cardpass-form" method="post" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>">
|
||||
<input type="hidden" name="action" value="<?php echo esc_attr(self::EXPORT_AJAX_ACTION); ?>" />
|
||||
<table class="form-table" role="presentation">
|
||||
<?php
|
||||
$this->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') . '<br><input type="text" class="mini-input" style="width:80px;" name="text_division" value=" " />',
|
||||
[['key' => 'export_format', 'op' => '==', 'val' => 'text']],
|
||||
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
|
||||
);
|
||||
?>
|
||||
</table>
|
||||
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* 字段渲染 / 依赖显隐
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* @param array<int,array{key:string,op:string,val:string}> $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 '<tr' . $dep_attr . $hidden . '><th scope="row">' . esc_html($title) . '</th><td>' . $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
|
||||
if ($desc !== '') {
|
||||
echo '<p class="description">' . wp_kses_post($desc) . '</p>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array{key:string,op:string,val:string}> $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 与初始隐藏状态同时挂在 <tr> 和内层提示块上:提示块若脱离所在行,仍能自行显隐。
|
||||
echo '<tr' . $dep_attr . $hidden . '><td colspan="2"><div' . $dep_attr . ' class="aksrt-submsg"' . $hidden . '>' . $content . '</div></td></tr>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端按表单默认值预判依赖可见性(求值规则与行内 JS 完全一致)。
|
||||
* 仅在表单渲染默认值(添加方式=系统自动生成、类型=余额)下,把不满足依赖的行标记为初始隐藏,
|
||||
* 避免四条导入提示首帧铺开。
|
||||
*
|
||||
* 注意:已知键必须覆盖 field_row/submessage_row 用到的所有依赖键,否则该键的判定会与 JS 不一致,
|
||||
* 出现「服务端判可见、JS 判隐藏」之类的错配(例如自定义卡密的两条导入提示同时铺开)。
|
||||
*
|
||||
* @param array<int,array{key:string,op:string,val:string}> $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<string,string> $options
|
||||
*/
|
||||
private function button_set(string $name, array $options, string $current): string {
|
||||
$html = '<div class="aksrt-btnset">';
|
||||
foreach ($options as $value => $label) {
|
||||
$html .= '<label class="aksrt-btnset-item"><input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . checked((string) $current, (string) $value, false) . '><span>' . esc_html((string) $label) . '</span></label>';
|
||||
}
|
||||
return $html . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字输入(参考主题 CSF spinner / number 字段的原生替代)。
|
||||
*
|
||||
* **一律不写原生 min / max / step 约束属性,只写 `step="any"`:**
|
||||
* - 本表单的行是条件显隐的(`field_row()` 会给不满足依赖的 `<tr>` 打上 `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 '<input type="number" class="small-text" step="any" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" />'
|
||||
. ' <span class="description">' . esc_html($unit) . '</span>';
|
||||
}
|
||||
|
||||
private function page_notice(string $title, string $msg, string $type = 'success'): void {
|
||||
$html = '';
|
||||
$html .= $title ? '<h3>' . esc_html($title) . '</h3>' : '';
|
||||
$html .= $msg ? '<p>' . esc_html($msg) . '</p>' : '';
|
||||
|
||||
if ($html) {
|
||||
echo '<div class="notice notice-' . esc_attr($type) . '">' . $html . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*
|
||||
* PayChargeCards 列表渲染(过滤 / 搜索 / 排序 / 批量删除 / 分页)与翻页参数拼装。
|
||||
* 作为 flat sibling trait 被 PayChargeCards 使用(PSR-4:trait 必须与宿主类同目录)。
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait PayChargeCardsList {
|
||||
|
||||
/**
|
||||
* @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 .= '<li><a href="' . esc_url($but['href']) . '">' . esc_html($but['name']) . '</a></li> | ';
|
||||
}
|
||||
$but_html = substr($but_html, 0, -2);
|
||||
|
||||
$search = '<form class="form-inline form-order" method="post">
|
||||
<div class="form-group" style="float: right;">
|
||||
<input type="text" class="form-control" name="s" placeholder="' . esc_attr__('搜索卡密', 'aksrt') . '">
|
||||
<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</button>
|
||||
</div>
|
||||
</form>';
|
||||
|
||||
// 当前筛选参数(不含 paged)。分页链接显式带上它,不再依赖 add_query_arg()
|
||||
// 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。
|
||||
$query_args = $this->list_query_args($status, $type, $other, $s, $orderby, $order, $per_page);
|
||||
|
||||
echo '<div class="order-header"><ul class="subsubsub">' . $but_html . '</ul>' . $search . '</div>';
|
||||
|
||||
$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 '<table class="widefat fixed striped posts table table-bordered aksrt-cardpass-table">';
|
||||
echo '<tbody>' . NativeUi::empty_row(1, esc_html__('未找到对应内容,或暂无内容', 'aksrt')) . '</tbody>';
|
||||
echo '</table>';
|
||||
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 = '<td id="cb" class="manage-column column-cb check-column" style="width: 2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'aksrt') . '</label><input id="cb-select-all-1" type="checkbox"></td>';
|
||||
foreach ($theads as $thead) {
|
||||
$orderby_links = '';
|
||||
if ($thead['orderby']) {
|
||||
$orderby_url = add_query_arg('orderby', $thead['orderby']);
|
||||
$orderby_links .= '<a title="' . esc_attr__('降序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
|
||||
$orderby_links .= '<a title="' . esc_attr__('升序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'DESC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-down"></span></a>';
|
||||
$orderby_links = '<span class="orderby-but">' . $orderby_links . '</span>';
|
||||
}
|
||||
$thead_th .= '<th class="" width="' . esc_attr($thead['width']) . '">' . esc_html($thead['name']) . $orderby_links . '</th>';
|
||||
}
|
||||
|
||||
$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 = '<span style="color: #3d7ffd;">' . esc_html__('未使用', 'aksrt') . '</span>';
|
||||
if ($msg['status'] === 'used') {
|
||||
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'aksrt') . '</span>';
|
||||
if (!empty($msg['order_number'])) {
|
||||
$order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number']));
|
||||
$status_html .= '<a style="color: #ff6215;" target="_blank" href="' . esc_url($order_link_url) . '"> [' . esc_html__('查看', 'aksrt') . ']</a>';
|
||||
}
|
||||
} elseif ($msg['status'] === 'expired') {
|
||||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已过期', 'aksrt') . '</span>';
|
||||
} elseif ($msg['status'] === 'disabled') {
|
||||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已禁用', 'aksrt') . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
$other_a = '';
|
||||
if (!empty($msg['note'])) {
|
||||
$other_a = '<a href="' . esc_url(add_query_arg('other', $msg['note'], $this->page_url)) . '">' . esc_html($msg['note']) . '</a>';
|
||||
}
|
||||
|
||||
if ($msg['type'] === 'vip') {
|
||||
$type_html = '<a style="color:#975106" href="' . esc_url(add_query_arg('type', 'vip', $this->page_url)) . '">' . esc_html__('会员兑换', 'aksrt') . '</a>';
|
||||
$level = (int) ($meta['level'] ?? 1);
|
||||
$type_html .= '<br>' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta));
|
||||
} elseif ($msg['type'] === 'credits') {
|
||||
$type_html = '<a style="color:#289d0f" href="' . esc_url(add_query_arg('type', 'credits', $this->page_url)) . '">' . esc_html__('积分兑换', 'aksrt') . '</a>';
|
||||
$type_html .= '<br>' . esc_html__('积分:', 'aksrt') . esc_html((string) (int) ($meta['points'] ?? 0));
|
||||
} elseif ($msg['type'] === 'balance') {
|
||||
$type_html = '<a style="color:#0c76e6" href="' . esc_url(add_query_arg('type', 'balance', $this->page_url)) . '">' . esc_html__('余额充值', 'aksrt') . '</a>';
|
||||
$type_html .= '<br>' . esc_html__('金额:', 'aksrt') . esc_html($this->balance_amount_text($meta));
|
||||
} else {
|
||||
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'aksrt') . '</a>';
|
||||
}
|
||||
|
||||
$card_name = (string) ($meta['custom_name']['card_name'] ?? '');
|
||||
$password_name = (string) ($meta['custom_name']['password_name'] ?? '');
|
||||
|
||||
$tbody .= '<tr>';
|
||||
$tbody .= '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . (int) $msg['id'] . '">' . esc_html__('选择', 'aksrt') . '</label>
|
||||
<input id="cb-select-' . (int) $msg['id'] . '" type="checkbox" name="action_id[]" value="' . (int) $msg['id'] . '">
|
||||
</th>';
|
||||
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['card']) . "' data-clipboard-tag=\"" . esc_attr($card_name !== '' ? $card_name : __('卡号', 'aksrt')) . "\">" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . '</span></td>';
|
||||
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['password']) . "' data-clipboard-tag=\"" . esc_attr($password_name !== '' ? $password_name : __('密码', 'aksrt')) . "\">" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . '</span></td>';
|
||||
$tbody .= "<td>$type_html</td>";
|
||||
$tbody .= '<td>' . esc_html((string) $msg['created_at']) . '</td>';
|
||||
$tbody .= '<td>' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . '</td>';
|
||||
$tbody .= "<td>$status_html</td>";
|
||||
$tbody .= "<td>$other_a</td>";
|
||||
$tbody .= '</tr>';
|
||||
}
|
||||
|
||||
echo '<form class="aksrt-cardpass-list-form" method="post">';
|
||||
wp_nonce_field(self::NONCE);
|
||||
echo '<div class="bulkactions aksrt-cardpass-bulk">
|
||||
<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'aksrt') . '</label><select name="action" id="bulk-action-selector-top">
|
||||
<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>
|
||||
<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>
|
||||
</select>
|
||||
<input type="submit" class="button action" value="' . esc_attr__('应用', 'aksrt') . '">
|
||||
</div>';
|
||||
|
||||
NativeUi::table_card_open();
|
||||
echo '<table class="widefat fixed striped posts table table-bordered aksrt-cardpass-table">';
|
||||
echo '<thead><tr>' . $thead_th . '</tr></thead>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
|
||||
echo '<tbody>' . $tbody . '</tbody>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
|
||||
echo '</table>';
|
||||
NativeUi::table_wrap_close();
|
||||
AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args);
|
||||
NativeUi::card_close();
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表筛选参数(不含 paged / page),用于构造翻页链接与「每页显示」表单。
|
||||
* 显式列条件而不是依赖 add_query_arg() 默认取 $_SERVER['REQUEST_URI']:
|
||||
* 后者把链接绑到当前请求上,一旦请求形态变化(POST、代理改写)就会丢条件。
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
+117
-79
@@ -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 '<div class="notice notice-error"><p>' . esc_html__('优惠码模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$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 '<h2>' . esc_html(sprintf(
|
||||
$title = sprintf(
|
||||
/* translators: %d coupon id */
|
||||
__('编辑优惠码 #%d', 'aksrt'),
|
||||
$id
|
||||
)) . '</h2>';
|
||||
echo '<p><a href="' . esc_url($this->page_url()) . '">← ' . esc_html__('返回列表', 'aksrt') . '</a></p>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
|
||||
);
|
||||
$back = NativeUi::button_link(__('返回列表', 'aksrt'), $this->page_url(), false, 'dashicons-arrow-left-alt');
|
||||
|
||||
NativeUi::card_open($title, $back);
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_coupon" />';
|
||||
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_coupon_' . $id);
|
||||
echo '<p><label>' . esc_html__('代码', 'aksrt') . '<br><input type="text" value="' . esc_attr((string) ($row['code'] ?? '')) . '" class="regular-text" readonly /></label></p>';
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type">';
|
||||
foreach (['fixed' => __('固定金额', 'aksrt'), 'percent' => __('折扣百分比', 'aksrt')] as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected((string) ($row['type'] ?? ''), $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label></p>';
|
||||
echo '<p><label>' . esc_html__('面值', 'aksrt') . '<br><input type="number" step="0.01" name="value" value="' . esc_attr((string) ($row['value'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最低消费', 'aksrt') . '<br><input type="number" step="0.01" name="min_amount" value="' . esc_attr((string) ($row['min_amount'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最高优惠(百分比用)', 'aksrt') . '<br><input type="number" step="0.01" name="max_discount" value="' . esc_attr((string) ($row['max_discount'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('总次数(0=不限)', 'aksrt') . '<br><input type="number" name="usage_limit" value="' . esc_attr((string) ($row['usage_limit'] ?? 0)) . '" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('每人限次(0=不限)', 'aksrt') . '<br><input type="number" name="per_user_limit" value="' . esc_attr((string) ($row['per_user_limit'] ?? 0)) . '" /></label></p>';
|
||||
$this->render_scope_fields($scope_type, implode(',', $scope_ids));
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" value="' . esc_attr($expires_local) . '" /></label></p>';
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
$this->field(__('代码', 'aksrt'), '<input type="text" value="' . esc_attr((string) ($row['code'] ?? '')) . '" readonly />');
|
||||
$this->field(__('类型', 'aksrt'), $this->type_select((string) ($row['type'] ?? '')));
|
||||
$this->field(__('面值', 'aksrt'), '<input type="number" step="0.01" name="value" value="' . esc_attr((string) ($row['value'] ?? 0)) . '" />');
|
||||
$this->field(__('最低消费', 'aksrt'), '<input type="number" step="0.01" name="min_amount" value="' . esc_attr((string) ($row['min_amount'] ?? 0)) . '" />');
|
||||
$this->field(__('最高优惠(百分比用)', 'aksrt'), '<input type="number" step="0.01" name="max_discount" value="' . esc_attr((string) ($row['max_discount'] ?? 0)) . '" />');
|
||||
$this->field(__('总次数(0=不限)', 'aksrt'), '<input type="number" name="usage_limit" value="' . esc_attr((string) ($row['usage_limit'] ?? 0)) . '" />');
|
||||
$this->field(__('每人限次(0=不限)', 'aksrt'), '<input type="number" name="per_user_limit" value="' . esc_attr((string) ($row['per_user_limit'] ?? 0)) . '" />');
|
||||
$this->scope_fields($scope_type, implode(',', $scope_ids));
|
||||
$this->field(__('过期时间', 'aksrt'), '<input type="datetime-local" name="expires_at" value="' . esc_attr($expires_local) . '" />');
|
||||
echo '</div>';
|
||||
echo '<div class="aksrt-native-form-foot">';
|
||||
submit_button(__('保存修改', 'aksrt'), 'primary', '', false);
|
||||
echo '</form><hr>';
|
||||
echo '</div>';
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
|
||||
private function render_create_form(): void {
|
||||
echo '<h2>' . esc_html__('新建优惠码', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
|
||||
NativeUi::card_open(__('新建优惠码', 'aksrt'));
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_create_coupon" />';
|
||||
wp_nonce_field('aksrt_pay_create_coupon');
|
||||
echo '<p><label>' . esc_html__('代码', 'aksrt') . '<br><input type="text" name="code" required /></label></p>';
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type"><option value="fixed">' . esc_html__('固定金额', 'aksrt') . '</option><option value="percent">' . esc_html__('折扣百分比', 'aksrt') . '</option></select></label></p>';
|
||||
echo '<p><label>' . esc_html__('面值', 'aksrt') . '<br><input type="number" step="0.01" name="value" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最低消费', 'aksrt') . '<br><input type="number" step="0.01" name="min_amount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最高优惠(百分比用)', 'aksrt') . '<br><input type="number" step="0.01" name="max_discount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('总次数(0=不限)', 'aksrt') . '<br><input type="number" name="usage_limit" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('每人限次(0=不限)', 'aksrt') . '<br><input type="number" name="per_user_limit" value="0" /></label></p>';
|
||||
$this->render_scope_fields('', '');
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" /></label></p>';
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
$this->field(__('代码', 'aksrt'), '<input type="text" name="code" required />');
|
||||
$this->field(__('类型', 'aksrt'), $this->type_select(''));
|
||||
$this->field(__('面值', 'aksrt'), '<input type="number" step="0.01" name="value" value="0" />');
|
||||
$this->field(__('最低消费', 'aksrt'), '<input type="number" step="0.01" name="min_amount" value="0" />');
|
||||
$this->field(__('最高优惠(百分比用)', 'aksrt'), '<input type="number" step="0.01" name="max_discount" value="0" />');
|
||||
$this->field(__('总次数(0=不限)', 'aksrt'), '<input type="number" name="usage_limit" value="0" />');
|
||||
$this->field(__('每人限次(0=不限)', 'aksrt'), '<input type="number" name="per_user_limit" value="0" />');
|
||||
$this->scope_fields('', '');
|
||||
$this->field(__('过期时间', 'aksrt'), '<input type="datetime-local" name="expires_at" />');
|
||||
echo '</div>';
|
||||
echo '<div class="aksrt-native-form-foot">';
|
||||
submit_button(__('创建', 'aksrt'), 'primary', '', false);
|
||||
echo '</div>';
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
|
||||
private function render_batch_form(): void {
|
||||
echo '<h2>' . esc_html__('批量生成优惠码', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-bottom:20px;display:flex;gap:8px;flex-wrap:wrap;align-items:end;">';
|
||||
echo '<details class="aksrt-native-card aksrt-native-collapse">';
|
||||
echo '<summary><span class="dashicons dashicons-randomize" aria-hidden="true"></span>' . esc_html__('批量生成优惠码', 'aksrt') . '</summary>';
|
||||
echo '<div class="aksrt-native-card__body">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_batch_coupons" />';
|
||||
wp_nonce_field('aksrt_pay_batch_coupons');
|
||||
echo '<p><label>' . esc_html__('数量', 'aksrt') . '<br><input type="number" name="count" min="1" max="100" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('长度', 'aksrt') . '<br><input type="number" name="length" min="6" max="24" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('前缀', 'aksrt') . '<br><input type="text" name="prefix" value="" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('类型', 'aksrt') . '<br><select name="type"><option value="fixed">' . esc_html__('固定金额', 'aksrt') . '</option><option value="percent">' . esc_html__('折扣百分比', 'aksrt') . '</option></select></label></p>';
|
||||
echo '<p><label>' . esc_html__('面值', 'aksrt') . '<br><input type="number" step="0.01" name="value" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最低消费', 'aksrt') . '<br><input type="number" step="0.01" name="min_amount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('最高优惠', 'aksrt') . '<br><input type="number" step="0.01" name="max_discount" value="0" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('总次数(0=不限)', 'aksrt') . '<br><input type="number" name="usage_limit" value="1" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('每人限次', 'aksrt') . '<br><input type="number" name="per_user_limit" value="1" /></label></p>';
|
||||
$this->render_scope_fields('', '');
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" /></label></p>';
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
$this->field(__('数量', 'aksrt'), '<input type="number" name="count" min="1" max="100" value="10" />');
|
||||
$this->field(__('长度', 'aksrt'), '<input type="number" name="length" min="6" max="24" value="10" />');
|
||||
$this->field(__('前缀', 'aksrt'), '<input type="text" name="prefix" value="" />');
|
||||
$this->field(__('类型', 'aksrt'), $this->type_select('fixed'));
|
||||
$this->field(__('面值', 'aksrt'), '<input type="number" step="0.01" name="value" value="10" />');
|
||||
$this->field(__('最低消费', 'aksrt'), '<input type="number" step="0.01" name="min_amount" value="0" />');
|
||||
$this->field(__('最高优惠', 'aksrt'), '<input type="number" step="0.01" name="max_discount" value="0" />');
|
||||
$this->field(__('总次数(0=不限)', 'aksrt'), '<input type="number" name="usage_limit" value="1" />');
|
||||
$this->field(__('每人限次', 'aksrt'), '<input type="number" name="per_user_limit" value="1" />');
|
||||
$this->scope_fields('', '');
|
||||
$this->field(__('过期时间', 'aksrt'), '<input type="datetime-local" name="expires_at" />');
|
||||
echo '</div>';
|
||||
echo '<div class="aksrt-native-form-foot">';
|
||||
submit_button(__('批量生成', 'aksrt'), 'secondary', '', false);
|
||||
echo '</div>';
|
||||
echo '</form>';
|
||||
echo '</div>';
|
||||
echo '</details>';
|
||||
}
|
||||
|
||||
private function render_scope_fields(string $selected_type, string $ids_text): void {
|
||||
echo '<p><label>' . esc_html__('适用范围', 'aksrt') . '<br><select name="scope_type">';
|
||||
/** 单字段单元(.aksrt-native-field);$html 为调用方已转义的控件片段。 */
|
||||
private function field(string $label, string $html, bool $wide = false): void {
|
||||
echo '<div class="aksrt-native-field' . ($wide ? ' is-wide' : '') . '">';
|
||||
echo '<span class="aksrt-native-field__label">' . esc_html($label) . '</span>';
|
||||
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 调用方已转义。
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function type_select(string $current): string {
|
||||
$html = '<select name="type">';
|
||||
foreach (['fixed' => __('固定金额', 'aksrt'), 'percent' => __('折扣百分比', 'aksrt')] as $k => $label) {
|
||||
$html .= sprintf('<option value="%s"%s>%s</option>', esc_attr($k), selected($current, $k, false), esc_html($label));
|
||||
}
|
||||
return $html . '</select>';
|
||||
}
|
||||
|
||||
private function scope_fields(string $selected_type, string $ids_text): void {
|
||||
$select = '<select name="scope_type">';
|
||||
foreach ([
|
||||
'' => __('全部商品', 'aksrt'),
|
||||
'include' => __('仅指定商品 ID', 'aksrt'),
|
||||
'exclude' => __('排除商品 ID', 'aksrt'),
|
||||
'category' => __('指定分类 ID', 'aksrt'),
|
||||
] as $k => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($selected_type, $k, false), esc_html($label));
|
||||
$select .= sprintf('<option value="%s"%s>%s</option>', esc_attr($k), selected($selected_type, $k, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label></p>';
|
||||
echo '<p><label>' . esc_html__('范围 ID', 'aksrt') . '<br><input type="text" name="scope_ids" class="regular-text" value="' . esc_attr($ids_text) . '" placeholder="' . esc_attr__('逗号分隔,如 12,34', 'aksrt') . '" /></label></p>';
|
||||
$select .= '</select>';
|
||||
$this->field(__('适用范围', 'aksrt'), $select);
|
||||
$this->field(
|
||||
__('范围 ID', 'aksrt'),
|
||||
'<input type="text" name="scope_ids" value="' . esc_attr($ids_text) . '" placeholder="' . esc_attr__('逗号分隔,如 12,34', '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 '<div class="notice notice-info"><p><strong>' . esc_html__('本次生成优惠码(请及时复制保存):', 'aksrt') . '</strong></p>';
|
||||
NativeUi::card_open(__('本次生成优惠码(请及时复制保存)', 'aksrt'));
|
||||
echo '<textarea class="large-text code" rows="' . esc_attr((string) min(12, max(3, count($generated_codes)))) . '" readonly onclick="this.select();">' . esc_textarea(implode("\n", $generated_codes)) . '</textarea>';
|
||||
echo '</div>';
|
||||
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 '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
||||
echo '<select name="status">';
|
||||
// 筛选栏(卡片头部的 .aksrt-native-filters 表单)。
|
||||
$filters = '<form method="get" class="aksrt-native-filters">';
|
||||
$filters .= '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
||||
$filters .= '<select name="status">';
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'active' => __('可用', 'aksrt'), 'disabled' => __('已禁用', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
$filters .= sprintf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('搜索代码', 'aksrt') . '" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
$filters .= '</select>';
|
||||
$filters .= '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('搜索代码', 'aksrt') . '" />';
|
||||
$filters .= '<button type="submit" class="button">' . esc_html__('筛选', 'aksrt') . '</button>';
|
||||
$filters .= '</form>';
|
||||
|
||||
$result = $svc->get_list([
|
||||
'status' => $status,
|
||||
@@ -215,11 +260,12 @@ class PayCoupons {
|
||||
$items = $result['items'] ?? [];
|
||||
}
|
||||
|
||||
NativeUi::table_card_open(__('优惠码列表', 'aksrt'), $filters);
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('代码', 'aksrt') . '</th><th>' . esc_html__('类型', 'aksrt') . '</th><th>' . esc_html__('面值', 'aksrt') . '</th><th>' . esc_html__('门槛/封顶', 'aksrt') . '</th><th>' . esc_html__('范围', 'aksrt') . '</th><th>' . esc_html__('已用/上限/每人', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('过期', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="10">' . esc_html__('暂无优惠码', 'aksrt') . '</td></tr>';
|
||||
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 '<td>' . esc_html((string) ($row['min_amount'] ?? 0)) . ' / ' . esc_html((string) ($row['max_discount'] ?? 0)) . '</td>';
|
||||
echo '<td>' . esc_html($this->format_scope_label($row['product_scope'] ?? [])) . '</td>';
|
||||
echo '<td>' . esc_html((string) ((int) ($row['used_count'] ?? 0)) . '/' . (string) ((int) ($row['usage_limit'] ?? 0)) . '/' . (string) ((int) ($row['per_user_limit'] ?? 0))) . '</td>';
|
||||
echo '<td>' . esc_html($status_labels[$st] ?? $st) . '</td>';
|
||||
echo '<td>' . NativeUi::status_badge($st, $status_map) . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
|
||||
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '')) . '</td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<td><div class="aksrt-native-inline">';
|
||||
echo '<a class="button button-small" href="' . esc_url($this->page_url(['edit_id' => $id])) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
|
||||
if ($st === 'active') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_disable_coupon" />';
|
||||
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_disable_coupon_' . $id);
|
||||
submit_button(__('禁用', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} elseif ($st === 'disabled') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_enable_coupon" />';
|
||||
echo '<input type="hidden" name="coupon_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_enable_coupon_' . $id);
|
||||
submit_button(__('启用', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
echo '</div></td></tr>';
|
||||
}
|
||||
// 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。
|
||||
$query_args = array_filter([
|
||||
@@ -263,7 +309,9 @@ class PayCoupons {
|
||||
});
|
||||
|
||||
echo '</tbody></table>';
|
||||
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 '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
|
||||
@@ -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 '<div class="notice notice-error"><p>' . esc_html__('分成模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
NativeUi::page_close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,24 +69,17 @@ class PayIncome {
|
||||
return '0.00';
|
||||
}
|
||||
};
|
||||
echo '<p>' . 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)))
|
||||
)) . '</p>';
|
||||
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 '<form id="aksrt-pay-batch-settle-income" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_batch_settle_income" />';
|
||||
echo '<input type="hidden" name="operation_key" value="' . esc_attr(wp_generate_uuid4()) . '" />';
|
||||
wp_nonce_field('aksrt_pay_batch_settle_income');
|
||||
submit_button(__('批量结算选中记录', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
NativeUi::card_open(__('筛选', 'aksrt'));
|
||||
echo '<form method="get" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_income" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt')] as $key => $label) {
|
||||
@@ -106,6 +101,7 @@ class PayIncome {
|
||||
echo ' <a class="button button-small" href="' . esc_url($this->page_url('aksrt_income')) . '">' . esc_html__('清除用户过滤', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
|
||||
$list_args = [
|
||||
'status' => $status,
|
||||
@@ -150,32 +146,46 @@ class PayIncome {
|
||||
return $value !== null;
|
||||
});
|
||||
|
||||
// 批量结算表单只提供提交按钮,表格里的复选框通过 form="…" 关联到它。
|
||||
ob_start();
|
||||
echo '<form id="aksrt-pay-batch-settle-income" method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_batch_settle_income" />';
|
||||
echo '<input type="hidden" name="operation_key" value="' . esc_attr(wp_generate_uuid4()) . '" />';
|
||||
wp_nonce_field('aksrt_pay_batch_settle_income');
|
||||
submit_button(__('批量结算选中记录', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
$batch_actions = ob_get_clean();
|
||||
|
||||
NativeUi::table_card_open(__('分成记录', 'aksrt'), $batch_actions);
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th><input type="checkbox" form="aksrt-pay-batch-settle-income" aria-label="' . esc_attr__('全选待结算记录', 'aksrt') . '" /></th><th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="8">' . esc_html__('暂无分成记录', 'aksrt') . '</td></tr>';
|
||||
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 '<tr>';
|
||||
echo '<td>' . ($st === 'pending' ? '<input type="checkbox" form="aksrt-pay-batch-settle-income" name="revenue_ids[]" value="' . esc_attr((string) $id) . '" />' : '') . '</td>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td><td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['order_number'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td><td>' . NativeUi::user_cell($user_id) . '</td>';
|
||||
echo '<td><code class="aksrt-native-mono">' . esc_html((string) ($row['order_number'] ?? '')) . '</code></td>';
|
||||
$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 '<td>' . esc_html($row_amount . ' ' . $row_currency) . '</td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td><span class="aksrt-native-amount">' . esc_html($row_amount . ' ' . $row_currency) . '</span></td>';
|
||||
echo '<td>' . NativeUi::status_badge($st, $status_map) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
if ($st === 'pending') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_settle_income" />';
|
||||
echo '<input type="hidden" name="revenue_id" value="' . esc_attr((string) $id) . '" />';
|
||||
echo '<input type="hidden" name="operation_key" value="' . esc_attr(wp_generate_uuid4()) . '" />';
|
||||
@@ -183,13 +193,16 @@ class PayIncome {
|
||||
submit_button(__('结算', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} else {
|
||||
echo '—';
|
||||
echo '<span class="aksrt-native-muted">—</span>';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
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 '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
|
||||
@@ -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 '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
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 '<p class="description">' . esc_html__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '</p>';
|
||||
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 '<div class="aksrt-pay-stats" style="display:flex;flex-wrap:wrap;gap:16px;margin:20px 0;">';
|
||||
$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 '</div>';
|
||||
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 '<h2>' . esc_html__('快捷入口', 'aksrt') . '</h2><ul>';
|
||||
foreach ($this->pages() as $page) {
|
||||
NativeUi::card_open(__('快捷入口', 'aksrt'));
|
||||
echo '<ul class="aksrt-native-links">';
|
||||
foreach ($this->pages() as $key => $page) {
|
||||
if ($page['slug'] === self::MENU_SLUG) {
|
||||
continue;
|
||||
}
|
||||
echo '<li><a href="' . esc_url($this->page_url($page['slug'])) . '">' . esc_html($page['title']) . '</a></li>';
|
||||
$icon = self::PAGE_ICONS[$key] ?? 'dashicons-admin-links';
|
||||
echo '<li><a href="' . esc_url($this->page_url($page['slug'])) . '"><span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' . esc_html($page['title']) . '</a></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
NativeUi::card_close();
|
||||
|
||||
$spa = admin_url('admin.php?page=aksrt-settings#/payment/gateway');
|
||||
echo '<p><a class="button" href="' . esc_url($spa) . '">' . esc_html__('打开支付配置(SPA)', 'aksrt') . '</a></p>';
|
||||
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
private function stat_card(string $label, string $value, string $url): void {
|
||||
echo '<a href="' . esc_url($url) . '" style="display:block;min-width:140px;padding:16px;background:#fff;border:1px solid #c3c4c7;border-radius:4px;text-decoration:none;color:inherit;">';
|
||||
echo '<div style="font-size:12px;color:#646970;">' . esc_html($label) . '</div>';
|
||||
echo '<div style="font-size:22px;font-weight:600;margin-top:6px;">' . esc_html($value) . '</div>';
|
||||
echo '</a>';
|
||||
NativeUi::page_close();
|
||||
}
|
||||
}
|
||||
@@ -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 '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
|
||||
$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 '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
|
||||
$tab_bits = [];
|
||||
echo '<div class="aksrt-native-tabs">';
|
||||
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[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a></li>';
|
||||
$cls = ($product_type === $tkey) ? ' is-active' : '';
|
||||
echo '<a class="aksrt-native-tabs__link' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($tlabel) . '</a>';
|
||||
}
|
||||
echo implode(' | ', $tab_bits);
|
||||
echo '</ul><div style="clear:both;"></div>';
|
||||
echo '</div>';
|
||||
|
||||
// Payment method quick tabs.
|
||||
echo '<ul class="subsubsub" style="float:none;margin:0 0 12px;">';
|
||||
$m_bits = [];
|
||||
echo '<div class="aksrt-native-tabs">';
|
||||
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[] = '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a></li>';
|
||||
$cls = ($pay_method === $mkey) ? ' is-active' : '';
|
||||
echo '<a class="aksrt-native-tabs__link' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($mlabel) . '</a>';
|
||||
}
|
||||
echo implode(' | ', $m_bits);
|
||||
echo '</ul><div style="clear:both;"></div>';
|
||||
echo '</div>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
NativeUi::card_open(__('筛选', 'aksrt'));
|
||||
echo '<form method="get" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_orders" />';
|
||||
if ($product_type !== '') {
|
||||
echo '<input type="hidden" name="product_type" value="' . esc_attr($product_type) . '" />';
|
||||
@@ -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 ' <span class="description">' . esc_html(implode(' · ', $filter_bits)) . '</span>';
|
||||
echo ' <span class="aksrt-native-muted">' . esc_html(implode(' · ', $filter_bits)) . '</span>';
|
||||
echo ' <a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders')) . '">' . esc_html__('清除过滤', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
|
||||
// Closed-order cleanup.
|
||||
echo '<p style="margin:8px 0 12px;">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('将删除 14 天前所有已关闭订单,不可恢复。确认清理?', 'aksrt')) . '\');">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline" onsubmit="return confirm(\'' . esc_js(__('将删除 14 天前所有已关闭订单,不可恢复。确认清理?', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_clear_closed_orders" />';
|
||||
echo '<input type="hidden" name="days" value="14" />';
|
||||
wp_nonce_field('aksrt_pay_clear_closed_orders');
|
||||
submit_button(__('清理 14 天前关闭订单', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
echo '</p>';
|
||||
NativeUi::card_close();
|
||||
|
||||
$status_map = $this->status_badge_map($labels);
|
||||
|
||||
NativeUi::table_card_open(__('订单明细', 'aksrt'));
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
||||
@@ -240,15 +241,13 @@ class PayOrders {
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无订单', 'aksrt') . '</td></tr>';
|
||||
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 '<tr>';
|
||||
echo '<td><code>' . esc_html($order_number) . '</code></td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><strong>' . esc_html($product_title !== '' ? $product_title : ($type_labels[$product_type] ?? $product_type)) . '</strong><br><span class="description">' . esc_html($type_labels[$product_type] ?? $product_type) . ($product_id > 0 ? ' #' . esc_html((string) $product_id) : '') . '</span></td>';
|
||||
echo '<td><strong>' . esc_html($this->order_amount($row)) . '</strong><br><span class="description">' . esc_html((string) ($row['payment_method'] ?? '')) . '</span></td>';
|
||||
echo '<td>' . esc_html($status_label) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="description">' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '</span></td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<td><code class="aksrt-native-mono">' . esc_html($order_number) . '</code></td>';
|
||||
echo '<td>' . NativeUi::user_cell($user_id) . '</td>';
|
||||
echo '<td><strong>' . esc_html($product_title !== '' ? $product_title : ($type_labels[$product_type] ?? $product_type)) . '</strong><br><span class="aksrt-native-muted">' . esc_html($type_labels[$product_type] ?? $product_type) . ($product_id > 0 ? ' #' . esc_html((string) $product_id) : '') . '</span></td>';
|
||||
echo '<td><strong class="aksrt-native-amount">' . esc_html($this->order_amount($row)) . '</strong><br><span class="aksrt-native-muted">' . esc_html((string) ($row['payment_method'] ?? '')) . '</span></td>';
|
||||
echo '<td>' . NativeUi::status_badge($status_key, $status_map) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="aksrt-native-muted">' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '</span></td>';
|
||||
echo '<td class="aksrt-native-inline">';
|
||||
echo '<a class="button button-small" href="' . esc_url($this->page_url('aksrt_orders', ['order' => $order_number])) . '">' . esc_html__('详情', 'aksrt') . '</a>';
|
||||
// 已支付是已退款的唯一前置状态。
|
||||
if ($status_key === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认将该订单标记为已退款?不可恢复。', 'aksrt')) . '\');">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline" onsubmit="return confirm(\'' . esc_js(__('确认将该订单标记为已退款?不可恢复。', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_refund_order" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
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 '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_delete_order" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
wp_nonce_field('aksrt_pay_delete_order_' . $order_number);
|
||||
@@ -290,10 +289,31 @@ class PayOrders {
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
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<string, string> $labels DomainModel::order_status_labels()
|
||||
* @return array<string, array{0:string,1:string}>
|
||||
*/
|
||||
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 '<p><a class="button" href="' . esc_url($this->page_url('aksrt_orders')) . '">← ' . esc_html__('返回订单列表', 'aksrt') . '</a></p>';
|
||||
$back = NativeUi::button_link(__('返回订单列表', 'aksrt'), $this->page_url('aksrt_orders'), false, 'dashicons-arrow-left-alt');
|
||||
|
||||
echo '<table class="widefat striped" style="max-width:960px;"><tbody>';
|
||||
NativeUi::card_open(sprintf(__('订单 %s', 'aksrt'), $order_number), $back);
|
||||
echo '<table class="widefat striped"><tbody>';
|
||||
$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 '<tr><th style="width:160px;">' . esc_html($label) . '</th><td>' . esc_html((string) $value) . '</td></tr>';
|
||||
echo '<tr><th style="width:160px;">' . esc_html($label) . '</th><td>' . $value . '</td></tr>';
|
||||
}
|
||||
if (!empty($context)) {
|
||||
echo '<tr><th>' . esc_html__('附加上下文', 'aksrt') . '</th><td><pre style="white-space:pre-wrap;margin:0;">' . esc_html(wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre></td></tr>';
|
||||
echo '<tr><th>' . esc_html__('附加上下文', 'aksrt') . '</th><td><pre class="aksrt-native-mono" style="white-space:pre-wrap;margin:0;">' . esc_html(wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre></td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
NativeUi::card_close();
|
||||
|
||||
echo '<h2 style="margin-top:20px;">' . esc_html__('运营备注', 'aksrt') . '</h2>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="max-width:960px;">';
|
||||
NativeUi::card_open(__('运营备注', 'aksrt'));
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-form-grid">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_order_note" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_order_note_' . $order_number);
|
||||
echo '<p><textarea name="admin_note" rows="3" class="large-text">' . esc_textarea($admin_note) . '</textarea></p>';
|
||||
echo '<p class="aksrt-native-field is-wide"><textarea name="admin_note" rows="3" class="large-text">' . esc_textarea($admin_note) . '</textarea></p>';
|
||||
echo '<div class="aksrt-native-form-foot">';
|
||||
submit_button(__('保存备注', 'aksrt'), 'secondary', '', false);
|
||||
echo '</div>';
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
|
||||
echo '<h2 style="margin-top:20px;">' . esc_html__('更新状态', 'aksrt') . '</h2>';
|
||||
echo '<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px;">';
|
||||
NativeUi::card_open(__('更新状态', 'aksrt'));
|
||||
echo '<div class="aksrt-native-toolbar">';
|
||||
$this->render_order_quick_status_buttons($order_number, $status, true);
|
||||
echo '</div>';
|
||||
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 '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
|
||||
@@ -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 '<div class="notice notice-error"><p>' . esc_html__('订单服务不可用。', 'aksrt') . '</p></div>';
|
||||
$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 '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th style="width:28%;">' . esc_html__('商品', 'aksrt') . '</th>';
|
||||
@@ -95,7 +97,7 @@ class PayProducts {
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$posts) {
|
||||
echo '<tr><td colspan="10">' . esc_html__('暂无付费文章。', 'aksrt') . '</td></tr>';
|
||||
echo NativeUi::empty_row(10, __('暂无付费文章。', 'aksrt'));
|
||||
}
|
||||
|
||||
foreach ($posts as $post) {
|
||||
@@ -146,7 +148,7 @@ class PayProducts {
|
||||
echo '<td>' . esc_html((string) $db_sales) . '</td>';
|
||||
echo '<td>' . esc_html((string) max(0, $meta_sales)) . '</td>';
|
||||
echo '<td>' . esc_html($revenue) . '</td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;">';
|
||||
echo '<td class="aksrt-native-inline">';
|
||||
if ($edit_url) {
|
||||
echo '<a class="button button-small" href="' . esc_url($edit_url) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
|
||||
}
|
||||
@@ -168,8 +170,10 @@ class PayProducts {
|
||||
});
|
||||
|
||||
echo '</tbody></table>';
|
||||
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<string, string> $type_labels
|
||||
*/
|
||||
private function render_filters(string $pay_type, string $search, array $type_labels): void {
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<ul class="subsubsub aksrt-native-tabs">';
|
||||
$base = $this->page_url(self::PAGE_SLUG);
|
||||
$all_class = $pay_type === '' ? 'is-active' : '';
|
||||
echo '<li><a class="' . esc_attr($all_class) . '" href="' . esc_url($base) . '">' . esc_html__('全部', 'aksrt') . '</a></li>';
|
||||
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 '<li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
|
||||
NativeUi::card_open(__('筛选', 'aksrt'));
|
||||
echo '<form method="get" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
|
||||
echo '<select name="pay_type">';
|
||||
echo '<option value="">' . esc_html__('全部付费类型', 'aksrt') . '</option>';
|
||||
@@ -326,25 +345,12 @@ class PayProducts {
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('标题或文章 ID', 'aksrt') . '" /> ';
|
||||
echo '</select>';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('标题或文章 ID', 'aksrt') . '" />';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
|
||||
echo '<ul class="subsubsub" style="margin:0 0 12px;float:none;">';
|
||||
$base = $this->page_url(self::PAGE_SLUG);
|
||||
$all_class = $pay_type === '' ? 'current' : '';
|
||||
echo '<li><a class="' . esc_attr($all_class) . '" href="' . esc_url($base) . '">' . esc_html__('全部', 'aksrt') . '</a></li>';
|
||||
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 ' | <li><a class="' . esc_attr($cls) . '" href="' . esc_url($url) . '">' . esc_html($label) . '</a></li>';
|
||||
}
|
||||
echo '</ul><div style="clear:both;"></div>';
|
||||
echo '<p class="description">' . esc_html__('本页统计内容付费文章(product_type=resource),不含商城实体商品。订单销量来自已支付/已发货/已完成订单;Meta 销量为缓存计数。', 'aksrt') . '</p>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
|
||||
private function ensure_cap(): void {
|
||||
@@ -361,15 +367,6 @@ class PayProducts {
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.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 '<div class="notice notice-error"><p>' . esc_html__('提现模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
NativeUi::page_close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,18 +55,17 @@ class PayWithdrawals {
|
||||
$retry_key = '';
|
||||
}
|
||||
|
||||
echo '<p>' . 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')
|
||||
)) . '</p>';
|
||||
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 '<form method="get" style="margin:12px 0;">';
|
||||
NativeUi::card_open(__('筛选', 'aksrt'));
|
||||
echo '<form method="get" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_withdraw" />';
|
||||
echo '<select name="status">';
|
||||
$status_options = [
|
||||
@@ -92,6 +93,7 @@ class PayWithdrawals {
|
||||
echo ' <a class="button" href="' . esc_url($this->page_url(self::PAGE_SLUG)) . '">' . esc_html__('清除条件', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
|
||||
$items = $svc->get_all([
|
||||
'status' => $status,
|
||||
@@ -138,6 +140,14 @@ class PayWithdrawals {
|
||||
return $value !== null;
|
||||
});
|
||||
|
||||
$status_map = [
|
||||
'pending' => [__('待处理', 'aksrt'), 'warning'],
|
||||
'processing' => [__('处理中', 'aksrt'), 'info'],
|
||||
'completed' => [__('已完成', 'aksrt'), 'success'],
|
||||
'rejected' => [__('已拒绝', 'aksrt'), 'danger'],
|
||||
];
|
||||
|
||||
NativeUi::table_card_open(__('提现申请', 'aksrt'));
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
||||
@@ -150,13 +160,12 @@ class PayWithdrawals {
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无提现记录', 'aksrt') . '</td></tr>';
|
||||
echo NativeUi::empty_row(7, __('暂无提现记录', 'aksrt'));
|
||||
}
|
||||
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
$gross = (string) ($row['gross_amount'] ?? '0.00');
|
||||
$net = (string) ($row['amount'] ?? '0.00');
|
||||
@@ -171,22 +180,22 @@ class PayWithdrawals {
|
||||
);
|
||||
|
||||
echo '<tr>';
|
||||
echo '<td><code>' . esc_html((string) ($row['withdrawal_number'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><code class="aksrt-native-mono">' . esc_html((string) ($row['withdrawal_number'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . NativeUi::user_cell($user_id) . '</td>';
|
||||
echo '<td>' . esc_html($amount_txt) . '</td>';
|
||||
echo '<td><strong>' . esc_html((string) ($row['method'] ?? '')) . '</strong><br><span class="description">' . esc_html($acct) . '</span></td>';
|
||||
echo '<td>' . esc_html($status_options[$st] ?? $st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="description">' . esc_html((string) ($row['processed_at'] ?? '') ?: '—') . '</span></td>';
|
||||
echo '<td><strong>' . esc_html((string) ($row['method'] ?? '')) . '</strong><br><span class="aksrt-native-muted">' . esc_html($acct) . '</span></td>';
|
||||
echo '<td>' . NativeUi::status_badge($st, $status_map) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '<br><span class="aksrt-native-muted">' . esc_html((string) ($row['processed_at'] ?? '') ?: '—') . '</span></td>';
|
||||
echo '<td>';
|
||||
$next_statuses = $st === 'pending' ? ['processing', 'rejected'] : ($st === 'processing' ? ['completed', 'rejected'] : []);
|
||||
if ($next_statuses === []) {
|
||||
echo '—';
|
||||
echo '<span class="aksrt-native-muted">—</span>';
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
continue;
|
||||
}
|
||||
$request_key = $retry_id === $id && $retry_key !== '' ? $retry_key : wp_generate_uuid4();
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_withdrawal" />';
|
||||
echo '<input type="hidden" name="withdrawal_id" value="' . esc_attr((string) $id) . '" />';
|
||||
echo '<input type="hidden" name="idempotency_key" value="' . esc_attr($request_key) . '" />';
|
||||
@@ -210,10 +219,11 @@ class PayWithdrawals {
|
||||
}
|
||||
|
||||
echo '</tbody></table>';
|
||||
|
||||
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 '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
|
||||
@@ -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 '<div class="wrap"><h2>' . esc_html__('用户禁封申诉及用户举报管理', 'aksrt') . '</h2>';
|
||||
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 '</div>';
|
||||
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 '<div class="notice notice-warning"><p style="color:#fb590a;">'
|
||||
echo '<div class="notice notice-warning"><p>'
|
||||
. esc_html__('注意:邀请码功能暂未启用。您可以在', 'aksrt')
|
||||
. ' <a href="' . esc_url($settings_url) . '">' . esc_html__('登录注册设置', 'aksrt') . '</a> '
|
||||
. esc_html__('中启用此功能', 'aksrt')
|
||||
@@ -46,43 +59,17 @@ trait UsersMenusInviteCode {
|
||||
echo '<div class="notice notice-' . esc_attr($inv_type) . ' is-dismissible"><h3>' . esc_html($inv_title) . '</h3>'
|
||||
. ($inv_msg !== '' ? '<p>' . esc_html($inv_msg) . '</p>' : '') . '</div>';
|
||||
}
|
||||
|
||||
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();
|
||||
?>
|
||||
<div class="wrap aksrt-inv-page">
|
||||
<style>
|
||||
/* 排序箭头:默认弱化,悬停加强。 */
|
||||
.aksrt-inv-page .orderby-but { position: relative; display: inline-block; }
|
||||
.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-field { display: flex; align-items: flex-start; padding: 14px 0; border-bottom: 1px solid #f0f0f1; }
|
||||
.aksrt-inv-field > label { flex: 0 0 170px; font-weight: 600; padding-top: 4px; }
|
||||
.aksrt-inv-field > .aksrt-inv-control { flex: 1; }
|
||||
.aksrt-inv-field .description { display: block; margin-top: 4px; color: #646970; }
|
||||
.aksrt-inv-submessage { background: #fff8e5; border-left: 4px solid #ffb900; box-shadow: 0 1px 1px rgba(0,0,0,.08); padding: 10px 14px; margin: 12px 0; }
|
||||
.aksrt-inv-submessage li { list-style: disc; margin-left: 18px; }
|
||||
.aksrt-inv-btnset label { display: inline-block; margin-right: -1px; border: 1px solid #2271b1; background: #fff; padding: 4px 14px; cursor: pointer; }
|
||||
.aksrt-inv-btnset label:first-child { border-radius: 4px 0 0 4px; }
|
||||
.aksrt-inv-btnset label:last-child { border-radius: 0 4px 4px 0; }
|
||||
.aksrt-inv-btnset input { display: none; }
|
||||
.aksrt-inv-btnset label.on { background: #2271b1; color: #fff; }
|
||||
.aksrt-inv-switch { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
|
||||
.aksrt-inv-switch input { margin: 0; }
|
||||
.aksrt-inv-mini { width: 80px; }
|
||||
.aksrt-inv-submit { background: linear-gradient(135deg, #2f9bff, #2271b1) !important; border-color: #2271b1 !important; color: #fff !important; padding: 4px 24px !important; border-radius: 4px !important; }
|
||||
.aksrt-inv-code { cursor: pointer; }
|
||||
.aksrt-inv-toast { position: fixed; top: 42px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,.75); color: #fff; padding: 8px 22px; border-radius: 4px; font-size: 13px; display: none; z-index: 100100; }
|
||||
.aksrt-inv-unit { color: #646970; margin-left: 4px; }
|
||||
</style>
|
||||
<?php if ($tab === '') : ?>
|
||||
<?php $this->render_invite_codes_list($service, $this_url, $nonce); ?>
|
||||
<?php elseif ($tab === 'add') : ?>
|
||||
<?php $this->render_invite_codes_add($this_url, $nonce); ?>
|
||||
<?php else : ?>
|
||||
<?php $this->render_invite_codes_export($this_url, $nonce); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="aksrt-inv-toast" id="aksrt-inv-toast"><?php esc_html_e('复制成功', 'aksrt'); ?></div>
|
||||
<script>
|
||||
(() => {
|
||||
@@ -142,35 +129,41 @@ trait UsersMenusInviteCode {
|
||||
'desc' => isset($_GET['desc']) ? sanitize_key((string) wp_unslash($_GET['desc'])) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
]);
|
||||
|
||||
echo '<h1 class="wp-heading-inline">' . esc_html__('邀请码管理', 'aksrt') . '</h1>';
|
||||
echo '<a href="' . esc_url(add_query_arg('tab', 'add', $this_url)) . '" class="page-title-action">' . esc_html__('添加邀请码', 'aksrt') . '</a>';
|
||||
echo '<a href="' . esc_url(add_query_arg('tab', 'export', $this_url)) . '" class="page-title-action" style="margin-left:6px;">' . esc_html__('导出邀请码', 'aksrt') . '</a>';
|
||||
echo '<hr class="wp-header-end">';
|
||||
// 页头 + 三视图切换(WP 原生 page-title-action:列表 / 添加 / 导出)。
|
||||
NativeUi::header(
|
||||
__('邀请码管理', 'aksrt'),
|
||||
'',
|
||||
'dashicons-tickets-alt'
|
||||
);
|
||||
printf(
|
||||
'<div class="aksrt-inv-switch-views"><a href="%s" class="page-title-action">%s</a> <a href="%s" class="page-title-action">%s</a></div>',
|
||||
esc_url(add_query_arg('tab', 'add', $this_url)),
|
||||
esc_html__('添加邀请码', 'aksrt'),
|
||||
esc_url(add_query_arg('tab', 'export', $this_url)),
|
||||
esc_html__('导出邀请码', 'aksrt')
|
||||
);
|
||||
|
||||
// 顶部筛选行:左侧状态链接(搜索时改为提示语),右侧搜索表单。
|
||||
$search_html = '<div style="float:right;">
|
||||
<form method="get"><input type="hidden" name="page" value="aksrt_invite_codes">
|
||||
<input type="text" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索邀请码', 'aksrt') . '">
|
||||
<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</button></form></div>';
|
||||
echo '<div style="margin:12px 0;min-height:32px;">';
|
||||
echo '<ul class="subsubsub" style="float:left;margin:0 0 8px;">';
|
||||
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
|
||||
if ($s !== '') {
|
||||
echo '<li style="display:inline-block;background:#fff8e5;border:1px solid #ffb900;padding:2px 10px;">'
|
||||
. esc_html(sprintf(__('搜索 “%s” 的内容', 'aksrt'), $s)) . '</li>';
|
||||
echo '<ul class="subsubsub"><li>' . esc_html(sprintf(__('搜索 “%s” 的内容', 'aksrt'), $s)) . '</li></ul>';
|
||||
} else {
|
||||
$links = [
|
||||
'all' => __('全部', 'aksrt'),
|
||||
'unused' => __('未使用', 'aksrt'),
|
||||
'used' => __('已使用', 'aksrt'),
|
||||
];
|
||||
$parts = [];
|
||||
echo '<ul class="subsubsub">';
|
||||
foreach ($links as $key => $label) {
|
||||
$url = $key === 'all' ? $this_url : add_query_arg('status', $key, $this_url);
|
||||
$parts[] = '<li><a href="' . esc_url($url) . '"' . ($key === $status ? ' style="font-weight:600;"' : '') . '>' . esc_html($label) . '</a></li>';
|
||||
echo '<li><a href="' . esc_url($url) . '"' . ($key === $status ? ' class="current"' : '') . '>' . esc_html($label) . '</a></li>';
|
||||
}
|
||||
echo implode(' | ', $parts); // phpcs:ignore WordPress.Security.EscapeOutput -- 各项均已转义。
|
||||
echo '</ul>';
|
||||
}
|
||||
echo '</ul>' . $search_html . '</div><div class="clear"></div>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 静态结构。
|
||||
echo '<form method="get" class="aksrt-native-filters"><input type="hidden" name="page" value="aksrt_invite_codes">';
|
||||
echo '<input type="search" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索邀请码', 'aksrt') . '">';
|
||||
echo '<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</button></form>';
|
||||
NativeUi::card_close();
|
||||
|
||||
// 可排序列:code / created_at / updated_at / used_count / remark。
|
||||
$columns = [
|
||||
@@ -185,16 +178,18 @@ trait UsersMenusInviteCode {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_invite_delete">';
|
||||
echo '<input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
|
||||
echo '<div class="bulkactions" style="margin:10px 0;">
|
||||
|
||||
$bulk = '<div class="bulkactions aksrt-native-inline">
|
||||
<select name="bulk_action" id="bulk-action-selector-top">
|
||||
<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>
|
||||
<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>
|
||||
</select>
|
||||
<button type="submit" class="button action" onclick="return this.form.bulk_action.value===\'delete\'||confirm(' . wp_json_encode(__('请先选择“删除”操作', 'aksrt')) . ');">' . esc_html__('应用', 'aksrt') . '</button>
|
||||
<button type="submit" class="button action" onclick="return this.form.bulk_action.value===\'delete\'||confirm(' . esc_attr(wp_json_encode(__('请先选择“删除”操作', 'aksrt'))) . ');">' . esc_html__('应用', 'aksrt') . '</button>
|
||||
</div>';
|
||||
echo '<div style="overflow-y:auto;width:100%;">';
|
||||
echo '<table class="widefat fixed striped posts table table-bordered" style="min-width:1000px;"><thead><tr>';
|
||||
echo '<td id="cb" class="manage-column column-cb check-column" style="width:2%;"><input id="cb-select-all-1" type="checkbox" onclick="document.querySelectorAll(\'tbody [name="action_id[]"]\').forEach(c=>c.checked=this.checked)"></td>';
|
||||
|
||||
NativeUi::table_card_open(__('邀请码列表', 'aksrt'), $bulk); // phpcs:ignore WordPress.Security.EscapeOutput -- actions pre-escaped.
|
||||
echo '<table class="widefat fixed striped"><thead><tr>';
|
||||
echo '<td id="cb" class="manage-column column-cb check-column"><input id="cb-select-all-1" type="checkbox" onclick="document.querySelectorAll(\'tbody [name="action_id[]"]\').forEach(c=>c.checked=this.checked)"></td>';
|
||||
foreach ($columns as $column) {
|
||||
$arrows = '';
|
||||
if ($column['key'] !== '') {
|
||||
@@ -203,30 +198,30 @@ trait UsersMenusInviteCode {
|
||||
$arrows = '<span class="orderby-but"><a title="' . esc_attr__('降序', 'aksrt') . '" href="' . esc_url($down) . '"><span class="dashicons dashicons-arrow-up"></span></a>'
|
||||
. '<a title="' . esc_attr__('升序', 'aksrt') . '" href="' . esc_url($up) . '"><span class="dashicons dashicons-arrow-down"></span></a></span>';
|
||||
}
|
||||
echo '<th style="width:' . esc_attr($column['width']) . ';">' . esc_html($column['name']) . $arrows . '</th>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 箭头为静态结构。
|
||||
echo '<th>' . esc_html($column['name']) . $arrows . '</th>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 箭头为静态结构。
|
||||
}
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if ($data['items'] === []) {
|
||||
echo '<tr><td colspan="7" style="color:#ff4a4a;text-align:center;">' . esc_html__('未找到对应内容,或暂无内容', 'aksrt') . '</td></tr>';
|
||||
echo NativeUi::empty_row(7, __('未找到对应内容,或暂无内容', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
} else {
|
||||
foreach ($data['items'] as $item) {
|
||||
$used = (int) $item['used_count'];
|
||||
$max_uses = (int) $item['max_uses'];
|
||||
echo '<tr>';
|
||||
echo '<th scope="row" class="check-column"><input type="checkbox" name="action_id[]" value="' . esc_attr((string) $item['id']) . '"></th>';
|
||||
echo '<td><span class="aksrt-inv-code" data-clipboard-text="' . esc_attr($item['code']) . '" title="' . esc_attr__('点击复制', 'aksrt') . '">' . esc_html($item['code']) . '</span></td>';
|
||||
echo '<td><div style="font-size:12px;">' . wp_kses((string) $item['reward_text'], ['br' => []]) . '</div></td>';
|
||||
echo '<td><span class="aksrt-inv-code aksrt-native-mono" data-clipboard-text="' . esc_attr($item['code']) . '" title="' . esc_attr__('点击复制', 'aksrt') . '">' . esc_html($item['code']) . '</span></td>';
|
||||
echo '<td>' . wp_kses((string) $item['reward_text'], ['br' => []]) . '</td>';
|
||||
echo '<td>' . esc_html($item['created_at']) . '</td>';
|
||||
echo '<td>' . esc_html($item['updated_at']) . '</td>';
|
||||
if ($used === 0) {
|
||||
echo '<td><span style="color:#3d7ffd;">' . esc_html__('未使用', 'aksrt') . '</span></td>';
|
||||
echo '<td>' . NativeUi::badge(__('未使用', 'aksrt'), 'info') . '</td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
} else {
|
||||
$label = $max_uses > 0 && $used < $max_uses ? sprintf(__('已使用 %1$d/%2$d', 'aksrt'), $used, $max_uses) : __('已使用', 'aksrt');
|
||||
echo '<td><span style="color:#f93b3b;">' . esc_html($label) . '</span>';
|
||||
echo '<td>' . NativeUi::badge($label, 'danger'); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
foreach ((array) $item['recent_users'] as $user) {
|
||||
$user_link = get_edit_user_link((int) $user['user_id']);
|
||||
echo '<br><a target="_blank" href="' . esc_url($user_link) . '">' . esc_html($user['name']) . '</a>';
|
||||
echo '<div class="aksrt-native-muted"><a target="_blank" href="' . esc_url($user_link) . '">' . esc_html($user['name']) . '</a></div>';
|
||||
}
|
||||
echo '</td>';
|
||||
}
|
||||
@@ -237,12 +232,13 @@ trait UsersMenusInviteCode {
|
||||
echo '</td></tr>';
|
||||
}
|
||||
}
|
||||
echo '</tbody></table></div></form><div class="clear"></div>';
|
||||
echo '</tbody></table>';
|
||||
NativeUi::table_wrap_close();
|
||||
|
||||
// 分页条:总数 + 页码链接。
|
||||
$total = (int) $data['total'];
|
||||
$max_pages = (int) $data['max_pages'];
|
||||
echo '<div class="tablenav bottom"><div class="tablenav-pages" style="display:flex;align-items:center;gap:6px;">';
|
||||
echo '<div class="tablenav bottom"><div class="tablenav-pages">';
|
||||
echo '<span class="displaying-num">' . esc_html(sprintf(__('共 %s 项', 'aksrt'), number_format_i18n($total))) . '</span>';
|
||||
if ($max_pages > 1) {
|
||||
echo paginate_links([
|
||||
@@ -256,19 +252,29 @@ trait UsersMenusInviteCode {
|
||||
]); // phpcs:ignore WordPress.Security.EscapeOutput -- paginate_links 自行转义。
|
||||
}
|
||||
echo '</div></div>';
|
||||
NativeUi::card_close();
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
/** 添加视图:系统自动生成 / 文本导入 二选一。 */
|
||||
private function render_invite_codes_add(string $this_url, string $nonce): void {
|
||||
echo '<h1 class="wp-heading-inline">' . esc_html__('添加邀请码', 'aksrt') . '</h1>';
|
||||
echo '<a href="' . esc_url($this_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
|
||||
echo '<hr class="wp-header-end">';
|
||||
NativeUi::header(
|
||||
__('添加邀请码', 'aksrt'),
|
||||
'',
|
||||
'dashicons-tickets-alt'
|
||||
);
|
||||
printf(
|
||||
'<div class="aksrt-inv-switch-views"><a href="%s" class="page-title-action"><span class="dashicons dashicons-arrow-left-alt" aria-hidden="true"></span>%s</a></div>',
|
||||
esc_url($this_url),
|
||||
esc_html__('返回列表', 'aksrt')
|
||||
);
|
||||
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="max-width:860px;">';
|
||||
NativeUi::card_open(__('添加邀请码', 'aksrt'));
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_invite_add">';
|
||||
echo '<input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
|
||||
|
||||
echo '<div class="aksrt-inv-submessage"><p><b>' . esc_html__('在此添加邀请码', 'aksrt') . '</b></p><ul>'
|
||||
echo '<div class="aksrt-native-callout"><p><b>' . esc_html__('在此添加邀请码', 'aksrt') . '</b></p><ul>'
|
||||
. '<li>' . esc_html__('如果您已经准备好了邀请码资料,请选择导入的方式添加', 'aksrt') . '</li>'
|
||||
. '<li>' . esc_html__('您也可以采用系统生成的方式,自动批量添加邀请码', 'aksrt') . '</li></ul></div>';
|
||||
|
||||
@@ -297,7 +303,7 @@ trait UsersMenusInviteCode {
|
||||
|
||||
// 导入区。
|
||||
echo '<div data-aksrt-inv-dep="add_type=import" style="display:none;">';
|
||||
echo '<div class="aksrt-inv-submessage"><p><b>' . esc_html__('导入邀请码', 'aksrt') . '</b></p><ul>'
|
||||
echo '<div class="aksrt-native-callout"><p><b>' . esc_html__('导入邀请码', 'aksrt') . '</b></p><ul>'
|
||||
. '<li>' . esc_html__('一行一个邀请码,单行格式为:', 'aksrt') . '<code>' . esc_html__('邀请码 奖励 标识', 'aksrt') . '</code></li>'
|
||||
. '<li>' . esc_html__('邀请码、奖励、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>'
|
||||
. '<li>' . esc_html__('奖励格式为:', 'aksrt') . '<code>level_integral=10&points=20&balance=30&vip=1&vip_time=5</code>,' . esc_html__('无需奖励填', 'aksrt') . ' <code>' . esc_html__('无', 'aksrt') . '</code></li>'
|
||||
@@ -308,31 +314,31 @@ trait UsersMenusInviteCode {
|
||||
. '<code>P86NPWKI points=10</code> ' . esc_html__('奖励10积分', 'aksrt') . '<br>'
|
||||
. '<code>auF9D2B4 balance=30&vip=1&vip_time=5</code> ' . esc_html__('奖励余额30和5天1级会员', 'aksrt') . '</p></div>';
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-import-data">' . esc_html__('邀请码数据', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<textarea id="aksrt-import-data" name="import_data" rows="10" class="large-text code" style="resize:both;"></textarea></div></div>';
|
||||
. '<textarea id="aksrt-import-data" name="import_data" rows="10" class="large-text code aksrt-inv-textarea"></textarea></div></div>';
|
||||
echo '<div class="aksrt-inv-field"><label for="aksrt-import-division">' . esc_html__('自定义分隔符号', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<input type="text" id="aksrt-import-division" name="import_division" value=" " maxlength="4" class="aksrt-inv-mini">'
|
||||
. '<span class="description">' . esc_html__('邀请码和奖励之间分割符号(默认为空格分割)', 'aksrt') . '</span></div></div>';
|
||||
echo '</div>';
|
||||
|
||||
echo '<div class="aksrt-inv-field"><label></label><div class="aksrt-inv-control">'
|
||||
. '<button type="submit" class="button button-primary aksrt-inv-submit">' . esc_html__('确认提交', 'aksrt') . '</button></div></div>';
|
||||
. '<button type="submit" class="button button-primary">' . esc_html__('确认提交', 'aksrt') . '</button></div></div>';
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
|
||||
/** 使用奖励字段集:经验值 / 积分 / 余额 / VIP 会员与赠送时长。 */
|
||||
private function render_invite_reward_fields(): void {
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('使用奖励', 'aksrt') . '</label><div class="aksrt-inv-control">';
|
||||
echo '<div style="display:flex;flex-wrap:wrap;gap:12px;">'
|
||||
echo '<div class="aksrt-inv-rewards">'
|
||||
. '<span><label>' . esc_html__('经验值', 'aksrt') . ' <input type="number" name="level_integral" value="0" min="0" class="small-text"></label></span>'
|
||||
. '<span><label>' . esc_html__('积分', 'aksrt') . ' <input type="number" name="points" value="0" min="0" class="small-text"></label></span>'
|
||||
. '<span><label>' . esc_html__('余额', 'aksrt') . ' <input type="number" name="balance" value="0" min="0" step="0.01" class="small-text"> ' . esc_html__('元', 'aksrt') . '</label></span>'
|
||||
. '</div>';
|
||||
echo '<div style="margin-top:8px;"><label>' . esc_html__('VIP会员', 'aksrt') . ' '
|
||||
. '<label style="font-weight:400;margin:0 8px;"><input type="radio" name="vip" value="" checked>' . esc_html__('无', 'aksrt') . '</label>'
|
||||
. '<label style="font-weight:400;margin:0 8px;"><input type="radio" name="vip" value="1">' . esc_html__('一级', 'aksrt') . '</label>'
|
||||
. '<label style="font-weight:400;margin:0 8px;"><input type="radio" name="vip" value="2">' . esc_html__('二级', 'aksrt') . '</label>'
|
||||
echo '<div class="aksrt-inv-vip"><label>' . esc_html__('VIP会员', 'aksrt') . ' '
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="vip" value="" checked>' . esc_html__('无', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="vip" value="1">' . esc_html__('一级', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="vip" value="2">' . esc_html__('二级', 'aksrt') . '</label>'
|
||||
. '</label></div>';
|
||||
echo '<div data-aksrt-inv-dep="vip=1" style="margin-top:8px;display:none;"><label>' . esc_html__('会员赠送时长', 'aksrt')
|
||||
echo '<div class="aksrt-inv-vip" data-aksrt-inv-dep="vip=1" style="display:none;"><label>' . esc_html__('会员赠送时长', 'aksrt')
|
||||
. ' <input type="number" name="vip_time" value="0" min="0" class="small-text"> <span class="aksrt-inv-unit">' . esc_html__('天', 'aksrt') . '</span></label>'
|
||||
. '<span class="description">' . esc_html__('仅在上方选择 VIP 会员等级后生效', 'aksrt') . '</span></div>';
|
||||
echo '</div></div>';
|
||||
@@ -340,22 +346,30 @@ trait UsersMenusInviteCode {
|
||||
|
||||
/** 导出视图:状态 / 格式 选择后提交下载。 */
|
||||
private function render_invite_codes_export(string $this_url, string $nonce): void {
|
||||
echo '<h1 class="wp-heading-inline">' . esc_html__('导出邀请码', 'aksrt') . '</h1>';
|
||||
echo '<a href="' . esc_url($this_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
|
||||
echo '<hr class="wp-header-end">';
|
||||
NativeUi::header(
|
||||
__('导出邀请码', 'aksrt'),
|
||||
'',
|
||||
'dashicons-download'
|
||||
);
|
||||
printf(
|
||||
'<div class="aksrt-inv-switch-views"><a href="%s" class="page-title-action"><span class="dashicons dashicons-arrow-left-alt" aria-hidden="true"></span>%s</a></div>',
|
||||
esc_url($this_url),
|
||||
esc_html__('返回列表', 'aksrt')
|
||||
);
|
||||
|
||||
echo '<form method="get" action="' . esc_url(admin_url('admin-post.php')) . '" style="max-width:860px;">';
|
||||
NativeUi::card_open();
|
||||
echo '<form method="get" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_invite_export">';
|
||||
echo '<input type="hidden" name="aksrt_nonce" value="' . esc_attr($nonce) . '">';
|
||||
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('选择状态', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<label style="font-weight:400;margin-right:16px;"><input type="radio" name="status" value="all" checked>' . esc_html__('全部', 'aksrt') . '</label>'
|
||||
. '<label style="font-weight:400;margin-right:16px;"><input type="radio" name="status" value="unused">' . esc_html__('未使用', 'aksrt') . '</label>'
|
||||
. '<label style="font-weight:400;margin-right:16px;"><input type="radio" name="status" value="used">' . esc_html__('已使用', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="status" value="all" checked>' . esc_html__('全部', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="status" value="unused">' . esc_html__('未使用', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="status" value="used">' . esc_html__('已使用', 'aksrt') . '</label>'
|
||||
. '</div></div>';
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('导出格式', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
. '<label style="font-weight:400;margin-right:16px;"><input type="radio" name="export_format" value="text">' . esc_html__('文本文档', 'aksrt') . '</label>'
|
||||
. '<label style="font-weight:400;margin-right:16px;"><input type="radio" name="export_format" value="xls" checked>' . esc_html__('Excel表格', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="export_format" value="text">' . esc_html__('文本文档', 'aksrt') . '</label>'
|
||||
. '<label class="aksrt-inv-radio"><input type="radio" name="export_format" value="xls" checked>' . esc_html__('Excel表格', 'aksrt') . '</label>'
|
||||
. '</div></div>';
|
||||
echo '<div data-aksrt-inv-dep="export_format=text" style="display:none;">';
|
||||
echo '<div class="aksrt-inv-field"><label>' . esc_html__('分隔符号', 'aksrt') . '</label><div class="aksrt-inv-control">'
|
||||
@@ -363,7 +377,8 @@ trait UsersMenusInviteCode {
|
||||
. '<span class="description">' . esc_html__('邀请码和奖励之间分割符号(默认为空格分割)', 'aksrt') . '</span></div></div>';
|
||||
echo '</div>';
|
||||
echo '<div class="aksrt-inv-field"><label></label><div class="aksrt-inv-control">'
|
||||
. '<button type="submit" class="button button-primary aksrt-inv-submit">' . esc_html__('确认提交', 'aksrt') . '</button></div></div>';
|
||||
. '<button type="submit" class="button button-primary">' . esc_html__('确认提交', 'aksrt') . '</button></div></div>';
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
}
|
||||
@@ -37,18 +37,22 @@ trait UsersMenusMessages {
|
||||
$filters = $this->message_filters();
|
||||
$paged = max(1, absint($_GET['paged'] ?? 1));
|
||||
|
||||
echo '<div class="wrap aksrt-messages-admin">';
|
||||
echo '<h1 class="wp-heading-inline">' . esc_html__('消息管理', 'aksrt') . '</h1>';
|
||||
printf(
|
||||
'<a class="page-title-action" href="%s">%s</a>',
|
||||
esc_url(admin_url('admin.php?page=aksrt-settings#/users/notifications')),
|
||||
esc_html__('通知配置', 'aksrt')
|
||||
NativeUi::page_open('aksrt-messages-admin');
|
||||
NativeUi::header(
|
||||
__('消息管理', 'aksrt'),
|
||||
'',
|
||||
'dashicons-email-alt',
|
||||
[[
|
||||
'label' => __('通知配置', 'aksrt'),
|
||||
'url' => admin_url('admin.php?page=aksrt-settings#/users/notifications'),
|
||||
'icon' => 'dashicons-admin-generic',
|
||||
]]
|
||||
);
|
||||
echo '<hr class="wp-header-end">';
|
||||
$this->render_message_notices();
|
||||
|
||||
if (NotificationAdminQuery::table() === '') {
|
||||
echo '<div class="notice notice-warning"><p>' . esc_html__('通知表不可用。', 'aksrt') . '</p></div></div>';
|
||||
echo '<div class="notice notice-warning"><p>' . esc_html__('通知表不可用。', 'aksrt') . '</p></div>';
|
||||
NativeUi::page_close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,9 +61,15 @@ trait UsersMenusMessages {
|
||||
|
||||
$this->render_message_overview();
|
||||
$this->render_message_compose();
|
||||
$this->render_message_views($filters, NotificationAdminQuery::status_counts($query));
|
||||
|
||||
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
|
||||
echo $this->message_views_html($filters, NotificationAdminQuery::status_counts($query)); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
$this->render_message_filter_form($filters);
|
||||
NativeUi::card_close();
|
||||
|
||||
NativeUi::table_card_open(__('站内通知', 'aksrt'));
|
||||
$this->render_message_table($result['items'], $filters, $result['page'], $query['user_id'] < 0);
|
||||
NativeUi::table_wrap_close();
|
||||
AdminListPagination::render(
|
||||
self::MESSAGE_PAGE_SLUG,
|
||||
$result['total'],
|
||||
@@ -68,7 +78,8 @@ trait UsersMenusMessages {
|
||||
$this->message_filter_args($filters),
|
||||
'users.php'
|
||||
);
|
||||
echo '</div>';
|
||||
NativeUi::card_close();
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
@@ -189,31 +200,20 @@ trait UsersMenusMessages {
|
||||
|
||||
private function render_message_overview(): void {
|
||||
$stats = NotificationAdminQuery::overview();
|
||||
$cards = [
|
||||
['total', __('消息总数', 'aksrt'), 'dashicons-email-alt'],
|
||||
['unread', __('未读', 'aksrt'), 'dashicons-marker'],
|
||||
['today', __('今日新增', 'aksrt'), 'dashicons-calendar-alt'],
|
||||
['recipients', __('涉及用户', 'aksrt'), 'dashicons-groups'],
|
||||
];
|
||||
echo '<ul class="aksrt-messages-admin__stats">';
|
||||
foreach ($cards as [$key, $label, $icon]) {
|
||||
printf(
|
||||
'<li class="aksrt-messages-admin__stat is-%1$s"><span class="dashicons %2$s" aria-hidden="true"></span><div><strong>%3$s</strong><span>%4$s</span></div></li>',
|
||||
esc_attr($key),
|
||||
esc_attr($icon),
|
||||
esc_html(number_format_i18n($stats[$key])),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
echo '</ul>';
|
||||
NativeUi::stats([
|
||||
['label' => __('消息总数', 'aksrt'), 'value' => number_format_i18n($stats['total']), 'icon' => 'dashicons-email-alt', 'tone' => 'info'],
|
||||
['label' => __('未读', 'aksrt'), 'value' => number_format_i18n($stats['unread']), 'icon' => 'dashicons-marker', 'tone' => 'danger'],
|
||||
['label' => __('今日新增', 'aksrt'), 'value' => number_format_i18n($stats['today']), 'icon' => 'dashicons-calendar-alt', 'tone' => 'success'],
|
||||
['label' => __('涉及用户', 'aksrt'), 'value' => number_format_i18n($stats['recipients']), 'icon' => 'dashicons-groups', 'tone' => 'primary'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function render_message_compose(): void {
|
||||
$open = sanitize_key((string) wp_unslash($_GET['aksrt_msg'] ?? '')) === 'send_failed';
|
||||
|
||||
echo '<details class="aksrt-messages-admin__compose"' . ($open ? ' open' : '') . '>';
|
||||
echo '<details class="aksrt-native-collapse aksrt-messages-admin__compose"' . ($open ? ' open' : '') . '>';
|
||||
echo '<summary><span class="dashicons dashicons-megaphone" aria-hidden="true"></span>' . esc_html__('发送系统消息', 'aksrt') . '</summary>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-messages-admin__compose-form">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_send_message" />';
|
||||
wp_nonce_field('aksrt_users_send_message');
|
||||
|
||||
@@ -228,36 +228,36 @@ trait UsersMenusMessages {
|
||||
);
|
||||
echo '</fieldset>';
|
||||
|
||||
echo '<div class="aksrt-messages-admin__grid">';
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
printf(
|
||||
'<label class="is-wide is-users"><span>%s</span><textarea name="users" rows="2" class="large-text" placeholder="%s"></textarea><small>%s</small></label>',
|
||||
'<label class="aksrt-native-field is-wide is-users"><span class="aksrt-native-field__label">%s</span><textarea name="users" rows="2" class="large-text" placeholder="%s"></textarea><small class="aksrt-native-field__hint">%s</small></label>',
|
||||
esc_html__('用户', 'aksrt'),
|
||||
esc_attr__('用户 ID / 用户名 / 邮箱,逗号或换行分隔', 'aksrt'),
|
||||
esc_html(sprintf(__('单次最多 %d 位。', 'aksrt'), AdminBroadcast::MAX_USERS))
|
||||
);
|
||||
echo '<label class="is-wide is-role"><span>' . esc_html__('角色', 'aksrt') . '</span><select name="role">';
|
||||
echo '<label class="aksrt-native-field is-wide is-role"><span class="aksrt-native-field__label">' . esc_html__('角色', 'aksrt') . '</span><select name="role">';
|
||||
foreach (wp_roles()->get_names() as $role => $name) {
|
||||
printf('<option value="%s">%s</option>', esc_attr($role), esc_html(translate_user_role($name)));
|
||||
}
|
||||
echo '</select><small>' . esc_html(sprintf(__('角色用户超过 %d 人时拒绝发送。', 'aksrt'), AdminBroadcast::MAX_ROLE_USERS)) . '</small></label>';
|
||||
echo '</select><small class="aksrt-native-field__hint">' . esc_html(sprintf(__('角色用户超过 %d 人时拒绝发送。', 'aksrt'), AdminBroadcast::MAX_ROLE_USERS)) . '</small></label>';
|
||||
printf(
|
||||
'<label><span>%s</span><input type="text" name="title" maxlength="%d" class="regular-text" placeholder="%s" /></label>',
|
||||
'<label class="aksrt-native-field"><span class="aksrt-native-field__label">%s</span><input type="text" name="title" maxlength="%d" class="regular-text" placeholder="%s" /></label>',
|
||||
esc_html__('标题', 'aksrt'),
|
||||
(int) AdminBroadcast::TITLE_MAX,
|
||||
esc_attr__('留空则取内容开头', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<label><span>%s</span><input type="url" name="target_url" class="regular-text" placeholder="https://" /></label>',
|
||||
'<label class="aksrt-native-field"><span class="aksrt-native-field__label">%s</span><input type="url" name="target_url" class="regular-text" placeholder="https://" /></label>',
|
||||
esc_html__('跳转链接(可选)', 'aksrt')
|
||||
);
|
||||
printf(
|
||||
'<label class="is-wide"><span>%s</span><textarea name="content" rows="4" maxlength="%d" required class="large-text"></textarea></label>',
|
||||
'<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">%s</span><textarea name="content" rows="4" maxlength="%d" required class="large-text"></textarea></label>',
|
||||
esc_html__('内容', 'aksrt'),
|
||||
(int) AdminBroadcast::CONTENT_MAX
|
||||
);
|
||||
echo '</div>';
|
||||
printf(
|
||||
'<p class="aksrt-messages-admin__compose-foot"><span class="description">%s</span><button type="submit" class="button button-primary">%s</button></p>',
|
||||
'<div class="aksrt-native-form-foot"><span class="aksrt-native-muted">%s</span><button type="submit" class="button button-primary">%s</button></div>',
|
||||
esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'aksrt'),
|
||||
esc_html__('发送', 'aksrt')
|
||||
);
|
||||
@@ -265,7 +265,7 @@ trait UsersMenusMessages {
|
||||
}
|
||||
|
||||
/** @param array{all:int,unread:int,read:int} $counts */
|
||||
private function render_message_views(array $filters, array $counts): void {
|
||||
private function message_views_html(array $filters, array $counts): string {
|
||||
$views = [
|
||||
'' => [__('全部', 'aksrt'), $counts['all']],
|
||||
'unread' => [__('未读', 'aksrt'), $counts['unread']],
|
||||
@@ -274,10 +274,10 @@ trait UsersMenusMessages {
|
||||
$args = $this->message_filter_args($filters);
|
||||
unset($args['status']);
|
||||
|
||||
echo '<ul class="subsubsub">';
|
||||
$html = '<ul class="subsubsub">';
|
||||
$last = array_key_last($views);
|
||||
foreach ($views as $key => [$label, $count]) {
|
||||
printf(
|
||||
$html .= sprintf(
|
||||
'<li><a href="%s"%s>%s <span class="count">(%s)</span></a>%s</li>',
|
||||
esc_url($this->message_page_url($key === '' ? $args : array_merge($args, ['status' => $key]))),
|
||||
$filters['status'] === $key ? ' class="current" aria-current="page"' : '',
|
||||
@@ -286,11 +286,11 @@ trait UsersMenusMessages {
|
||||
$key === $last ? '' : ' |'
|
||||
);
|
||||
}
|
||||
echo '</ul>';
|
||||
return $html . '</ul>';
|
||||
}
|
||||
|
||||
private function render_message_filter_form(array $filters): void {
|
||||
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-messages-admin__filters">';
|
||||
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::MESSAGE_PAGE_SLUG) . '" />';
|
||||
if ($filters['status'] !== '') {
|
||||
echo '<input type="hidden" name="status" value="' . esc_attr($filters['status']) . '" />';
|
||||
@@ -369,11 +369,7 @@ trait UsersMenusMessages {
|
||||
$hint = $user_missing
|
||||
? __('没有找到该收件人。', 'aksrt')
|
||||
: ($this->message_filter_args($filters) === [] ? __('站内还没有任何通知。', 'aksrt') : __('当前筛选条件下没有消息。', 'aksrt'));
|
||||
printf(
|
||||
'<tr class="no-items"><td colspan="6"><div class="aksrt-messages-admin__empty"><span class="dashicons dashicons-email-alt" aria-hidden="true"></span><strong>%s</strong><span>%s</span></div></td></tr>',
|
||||
esc_html__('暂无消息', 'aksrt'),
|
||||
esc_html($hint)
|
||||
);
|
||||
NativeUi::empty_row(6, __('暂无消息', 'aksrt'), $hint);
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$this->render_message_row($row, $filters);
|
||||
|
||||
@@ -129,21 +129,17 @@ trait UsersMenusTickets {
|
||||
'send_user' => $send_user,
|
||||
], static function ($v) { return $v !== '' && $v !== 0; }), $action_url);
|
||||
|
||||
// 对齐参考主题页面样式块
|
||||
echo '<style>
|
||||
.table-box>table{min-width:740px;}
|
||||
.preview-item{position:relative;display:inline-block;width:160px;height:160px;border-radius:4px;background:#fff;margin:5px;vertical-align:top;}
|
||||
.preview-item img{object-fit:contain;width:100%;height:100%;}
|
||||
</style>';
|
||||
|
||||
$rows = $this->collect_ticket_rows($type, $status, $s, $send_user, $orderby, $order);
|
||||
$total = count($rows);
|
||||
$pages = max(1, (int) ceil($total / self::TICKETS_PER_PAGE));
|
||||
$paged = min($paged, $pages);
|
||||
$tickets = array_slice($rows, ($paged - 1) * self::TICKETS_PER_PAGE, self::TICKETS_PER_PAGE);
|
||||
|
||||
NativeUi::card_open();
|
||||
echo '<div class="aksrt-native-toolbar">';
|
||||
|
||||
// 类型筛选按钮组(对齐参考主题:全部 / 禁封申诉 / 用户举报)
|
||||
echo '<div style="margin:8px 0;">';
|
||||
echo '<div class="aksrt-native-inline">';
|
||||
foreach (['' => __('全部', 'aksrt'), 'appeal' => __('禁封申诉', 'aksrt'), 'report' => __('用户举报', 'aksrt')] as $t => $label) {
|
||||
$url = $t === ''
|
||||
? remove_query_arg(['s', 'type', 'action', 'id'], $page_url)
|
||||
@@ -152,6 +148,16 @@ trait UsersMenusTickets {
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
// 搜索(对齐参考主题:右侧搜索记录表单)
|
||||
echo '<form class="aksrt-native-filters" method="get">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="tickets" />';
|
||||
if ($type !== '') {
|
||||
echo '<input type="hidden" name="type" value="' . esc_attr($type) . '" />';
|
||||
}
|
||||
echo '<input type="search" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索记录', 'aksrt') . '" />';
|
||||
submit_button(__('提交', 'aksrt'), 'primary', '', false);
|
||||
echo '</form></div>';
|
||||
|
||||
// 状态 subsubsub(对齐参考主题:全部 | 待处理 | 已批准 | 已拒绝)
|
||||
echo '<ul class="subsubsub">';
|
||||
$status_links = [
|
||||
@@ -164,31 +170,22 @@ trait UsersMenusTickets {
|
||||
foreach ($status_links as $key => $label) {
|
||||
$url = $key === '' ? remove_query_arg(['s', 'status'], $page_url) : add_query_arg('status', $key, remove_query_arg('s', $page_url));
|
||||
$sep = ++$i < count($status_links) ? ' |' : '';
|
||||
printf('<li><a href="%s">%s</a>%s</li> ', esc_url($url), esc_html($label), esc_html($sep));
|
||||
$current = $status === $key ? ' class="current" aria-current="page"' : '';
|
||||
printf('<li><a%s href="%s">%s</a>%s</li> ', $current, esc_url($url), esc_html($label), esc_html($sep));
|
||||
}
|
||||
echo '</ul>';
|
||||
|
||||
// 搜索(对齐参考主题:右侧搜索记录表单)
|
||||
echo '<div class="order-header" style="margin:6px 0 12px;float:right;">';
|
||||
echo '<form class="form-inline" method="get">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_ban" /><input type="hidden" name="tab" value="tickets" />';
|
||||
if ($type !== '') {
|
||||
echo '<input type="hidden" name="type" value="' . esc_attr($type) . '" />';
|
||||
}
|
||||
echo '<input type="text" name="s" value="' . esc_attr($s) . '" placeholder="' . esc_attr__('搜索记录', 'aksrt') . '" />';
|
||||
submit_button(__('提交', 'aksrt'), 'primary', '', false);
|
||||
echo '</form></div>';
|
||||
|
||||
if ($s !== '') {
|
||||
echo '<div class="mb10">"' . esc_html($s) . '" ' . esc_html__('的搜索结果', 'aksrt') . '</div>';
|
||||
echo '<p class="aksrt-native-muted">"' . esc_html($s) . '" ' . esc_html__('的搜索结果', 'aksrt') . '</p>';
|
||||
}
|
||||
NativeUi::card_close();
|
||||
|
||||
$process_id = isset($_GET['id']) ? (string) wp_unslash($_GET['id']) : '';
|
||||
|
||||
// 处理视图不设“查看全部”链接(参考主题无此元素):重置过滤由顶部
|
||||
// 类型按钮组与状态 subsubsub 承担,工单行内直接展开处理表单。
|
||||
echo '<div class="table-box" style="overflow-y:auto;width:100%;">';
|
||||
echo '<table class="widefat fixed striped posts" style="min-width:1000px;">';
|
||||
NativeUi::table_card_open();
|
||||
echo '<table class="widefat fixed striped posts">';
|
||||
echo '<thead><tr>';
|
||||
|
||||
// 表头(对齐参考主题:状态/类型/提交用户/详情/提交时间/更新时间 + 排序箭头)
|
||||
@@ -214,7 +211,7 @@ trait UsersMenusTickets {
|
||||
echo '</tr></thead><tbody>';
|
||||
|
||||
if (!$tickets) {
|
||||
echo '<tr><td colspan="6" align="center"><strong>' . esc_html__('暂无申请记录', 'aksrt') . '</strong></td></tr>';
|
||||
echo NativeUi::empty_row(6, __('暂无申请记录', 'aksrt'));
|
||||
}
|
||||
|
||||
foreach ($tickets as $row) {
|
||||
@@ -235,7 +232,10 @@ trait UsersMenusTickets {
|
||||
echo '</td></tr>';
|
||||
}
|
||||
|
||||
echo '</tbody></table></div>';
|
||||
echo '</tbody></table>';
|
||||
NativeUi::table_wrap_close();
|
||||
$this->ticket_pagenavi($total, self::TICKETS_PER_PAGE);
|
||||
NativeUi::card_close();
|
||||
|
||||
// 行内展开/收起的轻量交互(管理员环境必有 jQuery)。
|
||||
echo '<script>
|
||||
@@ -273,8 +273,6 @@ trait UsersMenusTickets {
|
||||
});
|
||||
})(jQuery);
|
||||
</script>';
|
||||
|
||||
$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 = '<span style="color:#fb4444;">' . esc_html__('正在处理', 'aksrt') . '</span>';
|
||||
$status_html = NativeUi::badge(__('正在处理', 'aksrt'), 'warning');
|
||||
} elseif ($status === 'pending') {
|
||||
// JS 切换展开(无整页刷新);无 JS 时 href 整页兜底。
|
||||
$status_html = '<a class="button" data-aksrt-ticket-toggle="' . esc_attr($process_key) . '" href="' . esc_url($url) . '">' . esc_html__('立即处理', 'aksrt') . '</a>';
|
||||
} elseif ($status === 'resolved') {
|
||||
$status_html = $kind === 'appeal'
|
||||
? '<span style="color:#16a60a;">' . esc_html__('已批准', 'aksrt') . '</span>'
|
||||
: '<span style="color:#0989fd;">' . esc_html__('处理完成', 'aksrt') . '</span>';
|
||||
? NativeUi::badge(__('已批准', 'aksrt'), 'success')
|
||||
: NativeUi::badge(__('处理完成', 'aksrt'), 'info');
|
||||
} else {
|
||||
$status_html = '<span style="color:#fb4444;">' . esc_html__('已拒绝', 'aksrt') . '</span>';
|
||||
$status_html = NativeUi::badge(__('已拒绝', 'aksrt'), 'danger');
|
||||
}
|
||||
|
||||
// 类型列(对齐参考主题:不良举报红 / 禁封申诉绿)
|
||||
$type_html = $kind === 'report'
|
||||
? '<span style="color:#fb4444;">' . esc_html__('不良举报', 'aksrt') . '</span>'
|
||||
: '<span style="color:#16a60a;">' . esc_html__('禁封申诉', 'aksrt') . '</span>';
|
||||
? 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 '<td>' . $status_html . '</td>';
|
||||
echo '<td>' . $type_html . '</td>';
|
||||
echo '<td>' . $user_html . '</td>';
|
||||
echo '<td><div style="max-height:39px;overflow:hidden;">' . $desc_html . '</div></td>';
|
||||
echo '<td><div class="aksrt-native-clamp">' . $desc_html . '</div></td>';
|
||||
echo '<td>' . esc_html((string) $row['time']) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['updated'] !== '' ? $row['updated'] : '—')) . '</td>';
|
||||
echo '</tr>';
|
||||
@@ -358,9 +356,9 @@ trait UsersMenusTickets {
|
||||
$link = $this->ticket_target_link($target_type, $target_id);
|
||||
|
||||
if ($target_type === 'user') {
|
||||
$html = '<div style="color:#fb4444;">' . esc_html__('举报用户:', 'aksrt') . ($link !== '' ? $link : esc_html('#' . $target_id)) . '</div>';
|
||||
$html = '<div class="aksrt-native-text--danger">' . esc_html__('举报用户:', 'aksrt') . ($link !== '' ? $link : esc_html('#' . $target_id)) . '</div>';
|
||||
} else {
|
||||
$html = '<div style="color:#fb4444;">' . esc_html__('举报内容:', 'aksrt') . ($link !== '' ? $link : esc_html($target_type . ' #' . $target_id)) . '</div>';
|
||||
$html = '<div class="aksrt-native-text--danger">' . esc_html__('举报内容:', 'aksrt') . ($link !== '' ? $link : esc_html($target_type . ' #' . $target_id)) . '</div>';
|
||||
}
|
||||
$html .= '<div>' . esc_html(sprintf(__('举报原因:%s', 'aksrt'), (string) ($r['reason'] ?? ''))) . '</div>';
|
||||
if (!empty($r['category'])) {
|
||||
@@ -393,7 +391,7 @@ trait UsersMenusTickets {
|
||||
|
||||
if ($ban) {
|
||||
$end = !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt');
|
||||
$html = '<div style="color:#fd6a11;">' . esc_html(sprintf(__('禁封状态:%s', 'aksrt'), __('封禁中(禁止登录)', 'aksrt'))) . '</div>';
|
||||
$html = '<div class="aksrt-native-text--warning">' . esc_html(sprintf(__('禁封状态:%s', 'aksrt'), __('封禁中(禁止登录)', 'aksrt'))) . '</div>';
|
||||
$html .= '<div>' . esc_html(sprintf(__('禁封原因:%s', 'aksrt'), (string) ($ban['reason'] ?? ''))) . '</div>';
|
||||
$html .= '<div>' . esc_html(sprintf(__('开始时间:%s', 'aksrt'), (string) ($ban['created_at'] ?? ''))) . '</div>';
|
||||
$html .= '<div>' . esc_html(sprintf(__('结束时间:%s', 'aksrt'), $end)) . '</div>';
|
||||
@@ -401,24 +399,24 @@ trait UsersMenusTickets {
|
||||
}
|
||||
|
||||
if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_SUSPEND)) {
|
||||
return '<div style="color:#fd6a11;">' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('停权中', 'aksrt'))) . '</div>';
|
||||
return '<div class="aksrt-native-text--warning">' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('停权中', 'aksrt'))) . '</div>';
|
||||
}
|
||||
if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_MUTE)) {
|
||||
return '<div style="color:#fd6a11;">' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '</div>';
|
||||
return '<div class="aksrt-native-text--warning">' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '</div>';
|
||||
}
|
||||
|
||||
// 对齐参考主题:申诉列表行中用户已无禁封 → 显示已解封并自动结案
|
||||
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 '<div style="color:#fd6a11;">' . esc_html__('此用户已解封', 'aksrt') . '</div>';
|
||||
return '<div class="aksrt-native-text--warning">' . esc_html__('此用户已解封', 'aksrt') . '</div>';
|
||||
}
|
||||
|
||||
// 对齐参考主题:申诉处理表单中用户已解封的提示
|
||||
if ($context === 'appeal_form') {
|
||||
return '<div style="color:#16a60a;">' . esc_html__('当前用户已解封', 'aksrt') . '</div>';
|
||||
return '<div class="aksrt-native-text--success">' . esc_html__('当前用户已解封', 'aksrt') . '</div>';
|
||||
}
|
||||
|
||||
return '<div style="color:#16a60a;">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
|
||||
return '<div class="aksrt-native-text--success">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
|
||||
}
|
||||
|
||||
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 '<div style="color:#16a60a;">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
|
||||
echo '<div class="aksrt-native-text--success">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
|
||||
echo '<p class="description">' . esc_html__('请查阅相关资料以及举证,如举报事实清楚,可以', 'aksrt');
|
||||
echo ' <a target="_blank" href="' . esc_url($ban_url) . '">' . esc_html__('点击此处', 'aksrt') . '</a> ';
|
||||
echo esc_html__('对用户做禁封处理,并将处理结果填写至下方', 'aksrt') . '</p>';
|
||||
@@ -489,7 +487,7 @@ trait UsersMenusTickets {
|
||||
}
|
||||
|
||||
echo '<tr><th>' . esc_html__('处理说明', 'aksrt') . '</th><td>';
|
||||
echo '<textarea style="width:95%;max-width:500px;" rows="2" cols="40" name="note" placeholder="' . esc_attr__('请输入本次处理的方式及说明', 'aksrt') . '"></textarea>';
|
||||
echo '<textarea rows="2" cols="40" name="note" placeholder="' . esc_attr__('请输入本次处理的方式及说明', 'aksrt') . '"></textarea>';
|
||||
echo '<p class="description">' . esc_html__('此处内容会反馈给举报人', 'aksrt') . '</p>';
|
||||
echo '</td></tr>';
|
||||
|
||||
@@ -518,12 +516,12 @@ trait UsersMenusTickets {
|
||||
echo '</td></tr>';
|
||||
|
||||
echo '<tr><th>' . esc_html__('处理方式', 'aksrt') . '</th><td>';
|
||||
echo '<p><input type="radio" name="decision" id="aksrt-process-1-' . $uid . '" value="approve" checked="checked"><label for="aksrt-process-1-' . $uid . '" style="color:#036ee2;">' . esc_html__('申诉通过(解封该用户)', 'aksrt') . '</label></p>';
|
||||
echo '<p><input type="radio" name="decision" id="aksrt-process-2-' . $uid . '" value="reject"><label for="aksrt-process-2-' . $uid . '" style="color:#eb1b65;">' . esc_html__('申诉拒绝(保持原禁封状态)', 'aksrt') . '</label></p>';
|
||||
echo '<p><input type="radio" name="decision" id="aksrt-process-1-' . $uid . '" value="approve" checked="checked"><label for="aksrt-process-1-' . $uid . '" class="aksrt-native-text--info">' . esc_html__('申诉通过(解封该用户)', 'aksrt') . '</label></p>';
|
||||
echo '<p><input type="radio" name="decision" id="aksrt-process-2-' . $uid . '" value="reject"><label for="aksrt-process-2-' . $uid . '" class="aksrt-native-text--danger">' . esc_html__('申诉拒绝(保持原禁封状态)', 'aksrt') . '</label></p>';
|
||||
echo '</td></tr>';
|
||||
|
||||
echo '<tr><th>' . esc_html__('处理留言', 'aksrt') . '</th><td>';
|
||||
echo '<textarea style="width:95%;max-width:500px;" rows="2" cols="40" name="note" placeholder="' . esc_attr__('给用户留言', 'aksrt') . '"></textarea>';
|
||||
echo '<textarea rows="2" cols="40" name="note" placeholder="' . esc_attr__('给用户留言', 'aksrt') . '"></textarea>';
|
||||
echo '<p class="description">' . esc_html__('如需给用户留言请填写此处,如果拒绝请填写拒绝原因', 'aksrt') . '</p>';
|
||||
echo '</td></tr>';
|
||||
|
||||
|
||||
@@ -54,19 +54,24 @@ trait UsersMenusTicketsCenter {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
|
||||
echo '<div class="wrap aksrt-tickets-admin">';
|
||||
echo '<h1 class="wp-heading-inline">' . esc_html__('工单管理', 'aksrt') . '</h1>';
|
||||
printf(
|
||||
'<a class="page-title-action" href="%s">%s</a>',
|
||||
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 '<hr class="wp-header-end">';
|
||||
|
||||
$this->render_ticket_notices();
|
||||
$this->render_ticket_create_form();
|
||||
$this->render_ticket_list_tab();
|
||||
echo '</div>';
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
/** 写操作后的 redirect 提示:成功绿色、失败红色。 */
|
||||
|
||||
@@ -93,7 +93,7 @@ trait UsersMenusTicketsDetail {
|
||||
|
||||
$html .= '<section class="aksrt-ticket-detail__card">';
|
||||
$html .= '<h4 class="aksrt-ticket-detail__heading">' . esc_html__('内部备注', 'aksrt')
|
||||
. ' <span class="aksrt-tickets-admin__muted">' . esc_html__('用户不可见', 'aksrt') . '</span></h4>';
|
||||
. ' <span class="aksrt-native-muted">' . esc_html__('用户不可见', 'aksrt') . '</span></h4>';
|
||||
if ($notes === []) {
|
||||
$html .= '<p class="aksrt-ticket-detail__empty">' . esc_html__('暂无备注', 'aksrt') . '</p>';
|
||||
} 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'])
|
||||
: '<span class="aksrt-tickets-admin__muted">' . esc_html__('未指派', 'aksrt') . '</span>',
|
||||
: '<span class="aksrt-native-muted">' . esc_html__('未指派', 'aksrt') . '</span>',
|
||||
];
|
||||
if ((string) $ticket['order_number'] !== '') {
|
||||
$rows[__('关联订单', 'aksrt')] = '<code>' . esc_html((string) $ticket['order_number']) . '</code>';
|
||||
@@ -170,7 +170,7 @@ trait UsersMenusTicketsDetail {
|
||||
/* translators: %d urge count */
|
||||
__('%d 次', 'aksrt'),
|
||||
(int) $ticket['urge_count']
|
||||
)) . '</span> <span class="aksrt-tickets-admin__muted">' . esc_html(sprintf(
|
||||
)) . '</span> <span class="aksrt-native-muted">' . esc_html(sprintf(
|
||||
/* translators: %s last urge time */
|
||||
__('最近 %s', 'aksrt'),
|
||||
(string) $ticket['last_urge_time']
|
||||
@@ -268,7 +268,7 @@ trait UsersMenusTicketsDetail {
|
||||
return '<header class="aksrt-ticket-detail__msg-head">'
|
||||
. ($avatar !== '' ? '<img src="' . esc_url($avatar) . '" alt="" width="32" height="32" />' : '')
|
||||
. '<strong>' . esc_html($name !== '' ? $name : '#' . $user_id) . '</strong>'
|
||||
. '<span class="aksrt-tickets-admin__muted">' . esc_html($role) . '</span>'
|
||||
. '<span class="aksrt-native-muted">' . esc_html($role) . '</span>'
|
||||
. '<time>' . esc_html($time) . '</time></header>';
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,18 @@ defined('ABSPATH') || exit;
|
||||
|
||||
trait UsersMenusTicketsList {
|
||||
|
||||
/** 代客提交:原生 <details> 折叠,失败回跳时自动展开。 */
|
||||
/** 代客提交:共享折叠卡片,失败回跳时自动展开。 */
|
||||
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 '<details class="aksrt-tickets-admin__create"' . ($failed ? ' open' : '') . '>';
|
||||
echo '<details class="aksrt-native-card aksrt-native-collapse"' . ($failed ? ' open' : '') . '>';
|
||||
echo '<summary><span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>' . esc_html__('代客提交工单', 'aksrt') . '</summary>';
|
||||
echo '<div class="aksrt-native-card__body">';
|
||||
|
||||
if ($types === []) {
|
||||
printf(
|
||||
'<p class="aksrt-tickets-admin__create-empty">%s <a href="%s">%s</a></p></details>',
|
||||
'<p class="aksrt-native-muted">%s <a href="%s">%s</a></p></div></details>',
|
||||
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 '<div class="aksrt-tickets-admin__create-grid">';
|
||||
echo '<div class="aksrt-native-form-grid">';
|
||||
printf(
|
||||
'<label><span>%s</span><input type="number" name="user_id" min="1" required class="regular-text" placeholder="%s" /></label>',
|
||||
'<label class="aksrt-native-field"><span class="aksrt-native-field__label">%s</span><input type="number" name="user_id" min="1" required placeholder="%s" /></label>',
|
||||
esc_html__('用户 ID', 'aksrt'),
|
||||
esc_attr__('工单归属用户', 'aksrt')
|
||||
);
|
||||
echo '<label><span>' . esc_html__('部门', 'aksrt') . '</span><select name="ticket_type_id" required>';
|
||||
echo '<label class="aksrt-native-field"><span class="aksrt-native-field__label">' . esc_html__('部门', 'aksrt') . '</span><select name="ticket_type_id" required>';
|
||||
foreach ($types as $row) {
|
||||
printf('<option value="%d">%s</option>', (int) $row['id'], esc_html((string) $row['name']));
|
||||
}
|
||||
echo '</select></label>';
|
||||
printf(
|
||||
'<label class="is-wide"><span>%s</span><input type="text" name="title" maxlength="%d" required class="large-text" /></label>',
|
||||
'<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">%s</span><input type="text" name="title" maxlength="%d" required /></label>',
|
||||
esc_html__('标题', 'aksrt'),
|
||||
\AKSRT\Ticket\TicketSettings::title_max()
|
||||
);
|
||||
if (\AKSRT\Ticket\TicketSettings::order_link_enabled()) {
|
||||
printf(
|
||||
'<label class="is-wide"><span>%s</span><input type="text" name="order_number" class="regular-text" placeholder="%s" /></label>',
|
||||
'<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">%s</span><input type="text" name="order_number" placeholder="%s" /></label>',
|
||||
esc_html__('关联订单', 'aksrt'),
|
||||
esc_attr__('可选', 'aksrt')
|
||||
);
|
||||
}
|
||||
printf(
|
||||
'<label class="is-wide"><span>%s</span><textarea name="content" rows="4" maxlength="%d" required class="large-text"></textarea></label>',
|
||||
'<label class="aksrt-native-field is-wide"><span class="aksrt-native-field__label">%s</span><textarea name="content" rows="4" maxlength="%d" required></textarea></label>',
|
||||
esc_html__('问题描述', 'aksrt'),
|
||||
\AKSRT\Ticket\TicketSettings::content_max()
|
||||
);
|
||||
echo '</div>';
|
||||
printf(
|
||||
'<p class="aksrt-tickets-admin__create-foot"><span class="description">%s</span><button type="submit" class="button button-primary">%s</button></p>',
|
||||
'<div class="aksrt-native-form-foot"><span class="aksrt-native-muted">%s</span><button type="submit" class="button button-primary">%s</button></div>',
|
||||
esc_html__('创建后状态为「已回复」并由你跟进,用户会收到通知。', 'aksrt'),
|
||||
esc_html__('创建工单', 'aksrt')
|
||||
);
|
||||
echo '</form></details>';
|
||||
echo '</form></div></details>';
|
||||
}
|
||||
|
||||
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 '<div class="aksrt-tickets-admin__table-wrap">';
|
||||
NativeUi::table_card_open(__('工单列表', 'aksrt'));
|
||||
echo '<table class="widefat striped aksrt-tickets-admin__table">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th class="column-title">' . esc_html__('工单', 'aksrt') . '</th>';
|
||||
@@ -113,17 +113,14 @@ trait UsersMenusTicketsList {
|
||||
$hint = $filters === []
|
||||
? __('暂无进行中的工单,已关闭工单可在「已关闭」视图查看。', 'aksrt')
|
||||
: __('当前筛选条件下没有工单。', 'aksrt');
|
||||
printf(
|
||||
'<tr class="no-items"><td colspan="6"><div class="aksrt-tickets-admin__empty"><strong>%s</strong><span>%s</span></div></td></tr>',
|
||||
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 '</tbody></table></div>';
|
||||
echo '</tbody></table>';
|
||||
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 '<div class="aksrt-native-toolbar">';
|
||||
$this->render_ticket_status_views($status);
|
||||
$this->render_ticket_filter_form($status, $type_id, $s);
|
||||
echo '</div>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
|
||||
/** 状态视图:未关闭(默认)+ 各进行中状态 + 完结态,均带计数。 */
|
||||
@@ -152,7 +160,7 @@ trait UsersMenusTicketsList {
|
||||
}
|
||||
array_unshift($views, ['', __('未关闭', 'aksrt'), $open_total, '']);
|
||||
|
||||
echo '<ul class="subsubsub aksrt-tickets-admin__views">';
|
||||
echo '<ul class="subsubsub">';
|
||||
$last = count($views) - 1;
|
||||
foreach ($views as $i => [$key, $label, $count, $color]) {
|
||||
printf(
|
||||
@@ -170,7 +178,7 @@ trait UsersMenusTicketsList {
|
||||
|
||||
/** 部门 + 关键词筛选(状态由视图链接决定,以隐藏域保留)。 */
|
||||
private function render_ticket_filter_form(string $status, int $type_id, string $s): void {
|
||||
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-tickets-admin__filters">';
|
||||
echo '<form method="get" action="' . esc_url(admin_url('users.php')) . '" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="' . esc_attr(self::TICKET_PAGE_SLUG) . '" />';
|
||||
if ($status !== '') {
|
||||
echo '<input type="hidden" name="status" value="' . esc_attr($status) . '" />';
|
||||
@@ -240,7 +248,7 @@ trait UsersMenusTicketsList {
|
||||
$this->ticket_user_chip((int) $ticket['user_id'], (string) $ticket['user_name']), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
(int) $ticket['admin_id'] > 0
|
||||
? esc_html((string) $ticket['admin_name'])
|
||||
: '<span class="aksrt-tickets-admin__muted">' . esc_html__('未指派', 'aksrt') . '</span>',
|
||||
: '<span class="aksrt-native-muted">' . esc_html__('未指派', 'aksrt') . '</span>',
|
||||
esc_html($last),
|
||||
esc_html(sprintf(
|
||||
/* translators: %s submit time */
|
||||
|
||||
@@ -25,13 +25,23 @@ trait UsersMenusVerify {
|
||||
$page = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$data = $this->query_verify_requests($filters, $page, 20);
|
||||
$counts = $this->get_verify_status_counts();
|
||||
echo '<div class="wrap"><h1>' . esc_html__('身份认证', 'aksrt') . '</h1>';
|
||||
NativeUi::page_open();
|
||||
NativeUi::header(__('身份认证', 'aksrt'), '', 'dashicons-id');
|
||||
$this->flash();
|
||||
printf('<p>%s</p>', esc_html(sprintf(__('待审核 %1$d · 已通过 %2$d · 已拒绝 %3$d', 'aksrt'), $counts['pending'], $counts['approved'], $counts['rejected'])));
|
||||
NativeUi::stats([
|
||||
['label' => __('待审核', 'aksrt'), 'value' => $counts['pending'], 'icon' => 'dashicons-clock', 'tone' => 'warning'],
|
||||
['label' => __('已通过', 'aksrt'), 'value' => $counts['approved'], 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
|
||||
['label' => __('已拒绝', 'aksrt'), 'value' => $counts['rejected'], 'icon' => 'dashicons-dismiss', 'tone' => 'danger'],
|
||||
]);
|
||||
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
|
||||
$this->render_verify_filters($filters);
|
||||
NativeUi::card_close();
|
||||
NativeUi::table_card_open(__('认证申请', 'aksrt'));
|
||||
$this->render_verify_table($data['items']);
|
||||
NativeUi::table_wrap_close();
|
||||
$this->pagination($data['pages'], $page);
|
||||
echo '</div>';
|
||||
NativeUi::card_close();
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
/** @return array{status:string,keyword:string} */
|
||||
@@ -80,20 +90,22 @@ trait UsersMenusVerify {
|
||||
}
|
||||
|
||||
private function render_verify_filters(array $filters): void {
|
||||
echo '<form method="get" style="margin:12px 0;display:flex;gap:8px;align-items:end;flex-wrap:wrap;">';
|
||||
echo '<form method="get" class="aksrt-native-filters">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_user_auth" />';
|
||||
echo '<label>' . esc_html__('状态', 'aksrt') . '<br><select name="status">';
|
||||
echo '<label>' . esc_html__('状态', 'aksrt') . '<select name="status">';
|
||||
foreach (['pending' => __('待审核', 'aksrt'), 'approved' => __('已通过', 'aksrt'), 'rejected' => __('已拒绝', 'aksrt'), '' => __('全部', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['status'], $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label><label>' . esc_html__('搜索', 'aksrt') . '<br><input type="search" name="keyword" value="' . esc_attr($filters['keyword']) . '" /></label>';
|
||||
echo '</select></label><label>' . esc_html__('搜索', 'aksrt') . '<input type="search" name="keyword" value="' . esc_attr($filters['keyword']) . '" /></label>';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
private function render_verify_table(array $items): void {
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th>用户</th><th>状态</th><th>认证名称</th><th>认证简介 / 申请说明</th><th>证明材料</th><th>审核</th></tr></thead><tbody>';
|
||||
if ($items === []) echo '<tr><td colspan="6">' . esc_html__('暂无认证申请', 'aksrt') . '</td></tr>';
|
||||
if ($items === []) {
|
||||
echo NativeUi::empty_row(6, __('暂无认证申请', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
}
|
||||
foreach ($items as $row) $this->render_verify_row($row);
|
||||
echo '</tbody></table>';
|
||||
}
|
||||
@@ -102,8 +114,12 @@ trait UsersMenusVerify {
|
||||
$user_id = (int) $row['user_id'];
|
||||
$status = (string) $row['status'];
|
||||
$application = (new \AKSRT\Identity\ApplicationRepository())->find_by_id((int) $row['id']) ?: $row;
|
||||
echo '<tr><td><strong><a href="' . esc_url(get_edit_user_link($user_id)) . '">' . esc_html((string) $row['display_name']) . '</a></strong><div>@' . esc_html((string) $row['user_login']) . ' · UID ' . $user_id . '</div><div>' . esc_html((string) $row['user_email']) . '</div></td>';
|
||||
echo '<td>' . esc_html($this->verify_status_label($status)) . '<div class="description">' . esc_html((string) $row['created_at']) . '</div></td>';
|
||||
echo '<tr><td>' . NativeUi::user_cell($user_id, (string) $row['display_name']) . '<div class="aksrt-native-muted">' . esc_html((string) $row['user_email']) . '</div></td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo '<td>' . NativeUi::status_badge($status, [
|
||||
'manual_pending' => [__('待审核', 'aksrt'), 'warning'],
|
||||
'verified' => [__('已通过', 'aksrt'), 'success'],
|
||||
'rejected' => [__('已拒绝', 'aksrt'), 'danger'],
|
||||
]) . '<div class="aksrt-native-muted">' . esc_html((string) $row['created_at']) . '</div></td>'; // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
echo '<td><strong>' . esc_html((string) $row['public_name']) . '</strong></td>';
|
||||
echo '<td>' . esc_html((string) $row['description']) . '<p class="description">' . esc_html((string) $row['manual_reason']) . '</p>';
|
||||
if (!empty($row['review_note'])) echo '<p class="description">' . esc_html__('处理留言:', 'aksrt') . esc_html((string) $row['review_note']) . '</p>';
|
||||
@@ -116,11 +132,6 @@ trait UsersMenusVerify {
|
||||
echo '</td></tr>';
|
||||
}
|
||||
|
||||
private function verify_status_label(string $status): string {
|
||||
$labels = ['manual_pending' => __('待审核', 'aksrt'), 'verified' => __('已通过', 'aksrt'), 'rejected' => __('已拒绝', 'aksrt')];
|
||||
return (string) ($labels[$status] ?? $status);
|
||||
}
|
||||
|
||||
private function render_verify_actions(array $row): void {
|
||||
$status = (string) $row['status'];
|
||||
if ($status === 'manual_pending') {
|
||||
|
||||
@@ -15,7 +15,8 @@ trait UsersMenusVip {
|
||||
$this->ensure_cap();
|
||||
$tab = $this->vip_tab();
|
||||
$page = max(1, absint($_GET['paged'] ?? 1));
|
||||
echo '<div class="wrap"><h1>' . esc_html__('用户会员', 'aksrt') . '</h1>';
|
||||
NativeUi::page_open();
|
||||
NativeUi::header(__('用户会员', 'aksrt'), '', 'dashicons-awards');
|
||||
$this->flash();
|
||||
$this->render_vip_tabs($tab);
|
||||
if ($tab === 'history') {
|
||||
@@ -23,12 +24,17 @@ trait UsersMenusVip {
|
||||
} else {
|
||||
$filters = $this->vip_filters();
|
||||
$result = $this->query_vip_users($filters, $page, 20);
|
||||
NativeUi::card_open('', '', false, 'aksrt-native-toolbar');
|
||||
$this->render_vip_filters($filters);
|
||||
NativeUi::card_close();
|
||||
$this->render_vip_bulk_form($filters);
|
||||
NativeUi::table_card_open(__('会员列表', 'aksrt'));
|
||||
$this->render_vip_table($result['items'], $filters);
|
||||
NativeUi::table_wrap_close();
|
||||
$this->pagination($result['pages'], $page);
|
||||
NativeUi::card_close();
|
||||
}
|
||||
echo '</div>';
|
||||
NativeUi::page_close();
|
||||
}
|
||||
|
||||
public function handle_mutate_vip(): void {
|
||||
@@ -165,37 +171,38 @@ trait UsersMenusVip {
|
||||
|
||||
private function render_vip_filters(array $filters): void {
|
||||
$names = $this->vip_level_names();
|
||||
echo '<form method="get" style="margin:12px 0;display:flex;gap:8px;align-items:end;flex-wrap:wrap"><input type="hidden" name="page" value="aksrt_user_vip">';
|
||||
echo '<label>UID<br><input type="number" min="1" name="user_id" value="' . esc_attr((string) $filters['user_id']) . '"></label><label>' . esc_html__('档位', 'aksrt') . '<br><select name="level">';
|
||||
echo '<form method="get" class="aksrt-native-filters"><input type="hidden" name="page" value="aksrt_user_vip">';
|
||||
echo '<label>UID<input type="number" min="1" name="user_id" value="' . esc_attr((string) $filters['user_id']) . '"></label><label>' . esc_html__('档位', 'aksrt') . '<select name="level">';
|
||||
foreach (['all' => __('全部', 'aksrt'), '0' => __('普通用户', 'aksrt'), '1' => $names[1], '2' => $names[2]] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['level'], $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label><label>' . esc_html__('状态', 'aksrt') . '<br><select name="state">';
|
||||
echo '</select></label><label>' . esc_html__('状态', 'aksrt') . '<select name="state">';
|
||||
foreach (['all' => __('全部', 'aksrt'), 'active' => __('有效', 'aksrt'), 'permanent' => __('永久', 'aksrt'), 'expired' => __('已过期', 'aksrt'), 'none' => __('无会员', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($filters['state'], $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select></label><label>' . esc_html__('到期从', 'aksrt') . '<br><input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '"></label><label>' . esc_html__('到期至', 'aksrt') . '<br><input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '"></label>';
|
||||
echo '</select></label><label>' . esc_html__('到期从', 'aksrt') . '<input type="date" name="date_from" value="' . esc_attr($filters['date_from']) . '"></label><label>' . esc_html__('到期至', 'aksrt') . '<input type="date" name="date_to" value="' . esc_attr($filters['date_to']) . '"></label>';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
|
||||
private function render_vip_bulk_form(array $filters): void {
|
||||
$names = $this->vip_level_names();
|
||||
echo '<form id="aksrt-vip-bulk" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:14px 0;padding:12px;background:#fff;border:1px solid #ccd0d4;display:flex;align-items:center;gap:8px;flex-wrap:wrap">';
|
||||
NativeUi::card_open(__('批量操作', 'aksrt'));
|
||||
echo '<form id="aksrt-vip-bulk" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-inline">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_users_mutate_vip">';
|
||||
wp_nonce_field('aksrt_users_mutate_vip');
|
||||
$this->render_vip_return_fields($filters);
|
||||
echo '<strong>' . esc_html__('批量操作', 'aksrt') . '</strong> ';
|
||||
$this->render_vip_operation_fields($names);
|
||||
echo ' <label style="display:inline-flex;align-items:center;gap:4px;min-width:0;flex:1 1 240px">' . esc_html__('原因', 'aksrt') . ' <input required name="reason" class="regular-text" style="width:100%;max-width:360px;min-width:0;box-sizing:border-box"></label> ';
|
||||
echo ' <label>' . esc_html__('原因', 'aksrt') . ' <input required name="reason" class="regular-text"></label> ';
|
||||
submit_button(__('应用到选中用户', 'aksrt'), 'primary', '', false);
|
||||
echo '</form>';
|
||||
NativeUi::card_close();
|
||||
}
|
||||
|
||||
private function render_vip_table(array $items, array $filters): void {
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th><input type="checkbox" aria-label="' . esc_attr__('选择全部用户', 'aksrt') . '" onclick="document.querySelectorAll(\'.aksrt-vip-check\').forEach((element) => element.checked = this.checked)"></th><th>UID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('当前会员', 'aksrt') . '</th><th>' . esc_html__('到期时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><td class="manage-column column-cb check-column"><input type="checkbox" aria-label="' . esc_attr__('选择全部用户', 'aksrt') . '" onclick="document.querySelectorAll(\'.aksrt-vip-check\').forEach((element) => element.checked = this.checked)"></td><th>UID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('当前会员', 'aksrt') . '</th><th>' . esc_html__('到期时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th></tr></thead><tbody>';
|
||||
if ($items === []) {
|
||||
echo '<tr><td colspan="6">' . esc_html__('暂无用户', 'aksrt') . '</td></tr>';
|
||||
echo NativeUi::empty_row(6, __('暂无用户', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$this->render_vip_row($row, $filters);
|
||||
@@ -215,11 +222,12 @@ trait UsersMenusVip {
|
||||
if ((int) $row['raw_level'] !== $level || (int) $row['raw_expires'] !== $expires) {
|
||||
$label .= ' (' . __('基础档位恢复后', 'aksrt') . ')';
|
||||
}
|
||||
echo '<tr><td><input class="aksrt-vip-check" form="aksrt-vip-bulk" type="checkbox" name="user_ids[]" value="' . $id . '"></td><td>' . $id . '</td><td><a href="' . esc_url(get_edit_user_link($id)) . '">' . esc_html((string) $row['display_name']) . '</a><div class="description">@' . esc_html((string) $row['user_login']) . '</div></td><td>' . esc_html($label) . '</td><td>' . esc_html($expiry) . '</td><td><details><summary>' . esc_html__('调整', 'aksrt') . '</summary><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-top:8px"><input type="hidden" name="action" value="aksrt_users_mutate_vip"><input type="hidden" name="user_ids[]" value="' . $id . '">';
|
||||
$tone = $level > 0 ? ((int) $row['raw_level'] !== $level || (int) $row['raw_expires'] !== $expires ? 'warning' : 'success') : 'neutral';
|
||||
echo '<tr><td class="check-column"><input class="aksrt-vip-check" form="aksrt-vip-bulk" type="checkbox" name="user_ids[]" value="' . $id . '"></td><td>' . $id . '</td><td>' . NativeUi::user_cell($id, (string) $row['display_name']) . '<div class="aksrt-native-muted">@' . esc_html((string) $row['user_login']) . '</div></td><td>' . NativeUi::badge($label, $tone) . '</td><td>' . esc_html($expiry) . '</td><td><details><summary class="button button-small">' . esc_html__('调整', 'aksrt') . '</summary><form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-native-form-foot"><input type="hidden" name="action" value="aksrt_users_mutate_vip"><input type="hidden" name="user_ids[]" value="' . $id . '">'; // phpcs:ignore WordPress.Security.EscapeOutput -- builders escape.
|
||||
wp_nonce_field('aksrt_users_mutate_vip');
|
||||
$this->render_vip_return_fields($filters);
|
||||
$this->render_vip_operation_fields($names);
|
||||
echo '<p style="margin-bottom:8px"><label style="display:block">' . esc_html__('原因', 'aksrt') . '<br><input required name="reason" class="regular-text" style="width:100%;max-width:360px;box-sizing:border-box"></label></p>';
|
||||
echo '<label class="aksrt-native-field is-wide">' . esc_html__('原因', 'aksrt') . '<input required name="reason" class="regular-text"></label>';
|
||||
submit_button(__('保存', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form></details></td></tr>';
|
||||
}
|
||||
@@ -228,7 +236,7 @@ trait UsersMenusVip {
|
||||
echo '<select class="aksrt-vip-operation" name="vip_operation"><option value="set">' . esc_html__('设置会员', 'aksrt') . '</option><option value="extend">' . esc_html__('延长有效期', 'aksrt') . '</option><option value="restore">' . esc_html__('取消限时升级/恢复回退会员', 'aksrt') . '</option><option value="disable">' . esc_html__('停用会员', 'aksrt') . '</option></select> ';
|
||||
echo '<span class="aksrt-vip-set-fields"><label>' . esc_html__('档位', 'aksrt') . ' <select name="level"><option value="1">' . esc_html($names[1]) . '</option><option value="2">' . esc_html($names[2]) . '</option></select></label> ';
|
||||
echo '<label><input type="checkbox" name="permanent" value="1">' . esc_html__('永久', 'aksrt') . '</label> <label>' . esc_html__('绝对到期日', 'aksrt') . ' <input type="date" name="expires_date"></label></span> ';
|
||||
echo '<span class="aksrt-vip-extend-fields"><label>' . esc_html__('延长', 'aksrt') . ' <input class="aksrt-vip-amount" type="number" name="amount" min="1" max="3650" value="1" style="width:72px"><select name="unit"><option value="day">' . esc_html__('天', 'aksrt') . '</option><option value="month">' . esc_html__('月', 'aksrt') . '</option></select></label></span>';
|
||||
echo '<span class="aksrt-vip-extend-fields"><label>' . esc_html__('延长', 'aksrt') . ' <input class="aksrt-vip-amount small-text" type="number" name="amount" min="1" max="3650" value="1"><select name="unit"><option value="day">' . esc_html__('天', 'aksrt') . '</option><option value="month">' . esc_html__('月', 'aksrt') . '</option></select></label></span>';
|
||||
echo '<script>document.querySelectorAll(".aksrt-vip-operation").forEach(function(select){function sync(){var form=select.closest("form");if(!form)return;var set=form.querySelector(".aksrt-vip-set-fields"),extend=form.querySelector(".aksrt-vip-extend-fields"),danger=select.value==="disable"||select.value==="restore";if(set)set.hidden=select.value!=="set";if(extend)extend.hidden=select.value!=="extend";if(danger&&!form.dataset.confirmBound){form.addEventListener("submit",function(e){if((select.value==="disable"&&!window.confirm("停用后将立即撤销该用户的前端会员权益,并清理恢复状态,确定继续吗?"))||(select.value==="restore"&&!window.confirm("将恢复回退会员并取消当前限时升级,确定继续吗?")))e.preventDefault();});form.dataset.confirmBound="1";}}sync();select.addEventListener("change",sync);});</script>';
|
||||
}
|
||||
|
||||
@@ -262,16 +270,19 @@ trait UsersMenusVip {
|
||||
|
||||
private function render_vip_history(int $page): void {
|
||||
$result = (new \AKSRT\Content\VipMembershipEventRepository())->query($page, 20);
|
||||
NativeUi::table_card_open(__('变更记录', 'aksrt'));
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th><th>' . esc_html__('操作人', 'aksrt') . '</th><th>' . esc_html__('原因', 'aksrt') . '</th><th>' . esc_html__('结果', 'aksrt') . '</th></tr></thead><tbody>';
|
||||
if ($result['items'] === []) {
|
||||
echo '<tr><td colspan="6">' . esc_html__('暂无变更记录', 'aksrt') . '</td></tr>';
|
||||
echo NativeUi::empty_row(6, __('暂无变更记录', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- builder escapes.
|
||||
}
|
||||
foreach ($result['items'] as $event) {
|
||||
$user = (string) ($event['display_name'] ?: $event['user_login']);
|
||||
echo '<tr><td>' . esc_html((string) $event['created_at']) . '</td><td>' . esc_html($user) . ' (#' . (int) $event['user_id'] . ')</td><td>' . esc_html((string) $event['operation']) . '</td><td>' . esc_html((string) ($event['actor_name'] ?: '—')) . '</td><td>' . esc_html((string) $event['reason']) . '</td><td>' . esc_html((string) $event['status']) . '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
NativeUi::table_wrap_close();
|
||||
$this->pagination($result['pages'], $page);
|
||||
NativeUi::card_close();
|
||||
}
|
||||
|
||||
/** @return array<int,string> */
|
||||
|
||||
@@ -23,7 +23,20 @@ const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const page = fs.readFileSync(path.join(root, 'src/Admin/Native/PayChargeCards.php'), 'utf8');
|
||||
|
||||
// PayChargeCards 已按关注点拆成 flat sibling traits。契约按方法名切片,依赖「类体 + trait 体」
|
||||
// 在同一文本里保持原有相邻顺序,因此这里按固定顺序把壳类与三个 trait 文件拼接后再切片:
|
||||
// 壳类(render_charge_cards_page / 动作 / 数据辅助)
|
||||
// → List(render_list / list_query_args)
|
||||
// → Assets(print_assets 的 <style>/<script>)
|
||||
// → Forms(render_add_form / render_export_form / 依赖显隐助手 / button_set / spinner / page_notice)
|
||||
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
||||
const page = [
|
||||
'src/Admin/Native/PayChargeCards.php',
|
||||
'src/Admin/Native/PayChargeCardsList.php',
|
||||
'src/Admin/Native/PayChargeCardsAssets.php',
|
||||
'src/Admin/Native/PayChargeCardsForms.php',
|
||||
].map(read).join('\n');
|
||||
|
||||
/** 依赖条目:checked / unchecked 不写 val,与 wp_json_encode 的输出保持一致。 */
|
||||
const DEP_ENTRY = /\['key'\s*=>\s*'([^']*)'\s*,\s*'op'\s*=>\s*'([^']*)'(?:\s*,\s*'val'\s*=>\s*'([^']*)')?\]/g;
|
||||
|
||||
@@ -35,7 +35,15 @@ const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const page = fs.readFileSync(path.join(root, 'src/Admin/Native/PayChargeCards.php'), 'utf8');
|
||||
|
||||
// PayChargeCards 拆成 flat sibling traits;按原有相邻顺序拼接壳类与 trait 后再扫描/切片。
|
||||
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
||||
const page = [
|
||||
'src/Admin/Native/PayChargeCards.php',
|
||||
'src/Admin/Native/PayChargeCardsList.php',
|
||||
'src/Admin/Native/PayChargeCardsAssets.php',
|
||||
'src/Admin/Native/PayChargeCardsForms.php',
|
||||
].map(read).join('\n');
|
||||
|
||||
/**
|
||||
* 去掉 PHP 注释后的源码(字符串字面量原样保留)。
|
||||
|
||||
@@ -16,7 +16,15 @@ const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const page = fs.readFileSync(path.join(root, 'src/Admin/Native/PayChargeCards.php'), 'utf8');
|
||||
|
||||
// PayChargeCards 已拆成 flat sibling traits;按方法名切片需要「类体 + trait 体」仍在同一文本里
|
||||
// 保持原有相邻顺序,故按固定顺序拼接壳类与三个 trait(壳类 → List → Assets → Forms)。
|
||||
const page = [
|
||||
'src/Admin/Native/PayChargeCards.php',
|
||||
'src/Admin/Native/PayChargeCardsList.php',
|
||||
'src/Admin/Native/PayChargeCardsAssets.php',
|
||||
'src/Admin/Native/PayChargeCardsForms.php',
|
||||
].map((rel) => fs.readFileSync(path.join(root, rel), 'utf8')).join('\n');
|
||||
|
||||
const count = (src, pattern) => (src.match(pattern) || []).length;
|
||||
|
||||
|
||||
@@ -27,7 +27,15 @@ const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const page = fs.readFileSync(path.join(root, 'src/Admin/Native/PayChargeCards.php'), 'utf8');
|
||||
|
||||
// PayChargeCards 已拆成 flat sibling traits;按方法名切片需要「类体 + trait 体」仍在同一文本里
|
||||
// 保持原有相邻顺序,故按固定顺序拼接壳类与三个 trait(壳类 → List → Assets → Forms)。
|
||||
const page = [
|
||||
'src/Admin/Native/PayChargeCards.php',
|
||||
'src/Admin/Native/PayChargeCardsList.php',
|
||||
'src/Admin/Native/PayChargeCardsAssets.php',
|
||||
'src/Admin/Native/PayChargeCardsForms.php',
|
||||
].map((rel) => fs.readFileSync(path.join(root, rel), 'utf8')).join('\n');
|
||||
|
||||
const count = (src, pattern) => (src.match(pattern) || []).length;
|
||||
|
||||
|
||||
@@ -30,13 +30,21 @@ const root = path.resolve(__dirname, '..', '..');
|
||||
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
||||
|
||||
const helper = read('src/Admin/Native/AdminListPagination.php');
|
||||
// 卡密管理已拆成 flat sibling traits;分页相关逻辑现落在 List trait 内,故把壳类与三个
|
||||
// trait 拼接后再做契约断言(顺序:壳类 → List → Assets → Forms)。
|
||||
const chargeCards = [
|
||||
'src/Admin/Native/PayChargeCards.php',
|
||||
'src/Admin/Native/PayChargeCardsList.php',
|
||||
'src/Admin/Native/PayChargeCardsAssets.php',
|
||||
'src/Admin/Native/PayChargeCardsForms.php',
|
||||
].map(read).join('\n');
|
||||
const pages = {
|
||||
orders: read('src/Admin/Native/PayOrders.php'),
|
||||
products: read('src/Admin/Native/PayProducts.php'),
|
||||
coupons: read('src/Admin/Native/PayCoupons.php'),
|
||||
income: read('src/Admin/Native/PayIncome.php'),
|
||||
withdrawals: read('src/Admin/Native/PayWithdrawals.php'),
|
||||
chargeCards: read('src/Admin/Native/PayChargeCards.php'),
|
||||
chargeCards,
|
||||
};
|
||||
|
||||
const labels = {
|
||||
|
||||
Reference in new issue
Block a user