feat(frontend): upgrade report dialog experience
Replace manual report links with automatic target association, improve validation and evidence uploads, and remove retired report UI code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59ddf677b6
commit
80aecab586
10 files changed
+1121
-462
No files matched your search
@@ -5697,41 +5697,6 @@ body.aksrt-modal-runtime-open {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
/* ---- 举报弹窗(参考主题报告弹窗几何 1:1)----
|
||||
宽度对齐参考主题 Bootstrap 默认模态(>=768px 600px),圆角/底色走
|
||||
--main-radius / --main-bg-color;移动端仍由 --bottom 底部抽屉接管。 */
|
||||
.aksrt-report-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||
width: min(600px, calc(100vw - 32px));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
background: var(--main-bg-color, #fff);
|
||||
}
|
||||
|
||||
/* edit-header:参考主题弹窗头(关闭钮靠右,红色警示徽标 + 标题居左)。 */
|
||||
.aksrt-report-modal .edit-header {
|
||||
position: relative;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.aksrt-report-modal .edit-header .close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
float: none;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
/* 原因徽章行:与参考主题一致,徽章可换行且间距走 mr6/mb6 工具类。 */
|
||||
.aksrt-report-modal .form-but-radio .badg {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ---- 禁封用户弹窗(参考主题 set_user_ban 模态框几何 1:1)----
|
||||
参考主题 zib_get_edit_user_ban_link 未传 data-class → 走 Bootstrap 默认
|
||||
.modal-dialog(>=768px 宽 600px);data-height=360 → .modal-content 初始
|
||||
@@ -5973,14 +5938,14 @@ body.aksrt-modal-runtime-open {
|
||||
color: var(--color-ink, #111);
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview {
|
||||
.order-comment-from .form-upload .preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item {
|
||||
.order-comment-from .form-upload .preview .add,
|
||||
.order-comment-from .form-upload .preview .preview-item {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 10px;
|
||||
@@ -5996,14 +5961,14 @@ body.aksrt-modal-runtime-open {
|
||||
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
|
||||
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item img {
|
||||
.order-comment-from .form-upload .preview .preview-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item .remove {
|
||||
.order-comment-from .form-upload .preview .preview-item .remove {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
@@ -6018,7 +5983,7 @@ body.aksrt-modal-runtime-open {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .hide {
|
||||
.order-comment-from .form-upload .hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
||||
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
|
||||
@@ -286,41 +286,6 @@ body.aksrt-modal-runtime-open {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
/* ---- 举报弹窗(参考主题报告弹窗几何 1:1)----
|
||||
宽度对齐参考主题 Bootstrap 默认模态(>=768px 600px),圆角/底色走
|
||||
--main-radius / --main-bg-color;移动端仍由 --bottom 底部抽屉接管。 */
|
||||
.aksrt-report-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||
width: min(600px, calc(100vw - 32px));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
background: var(--main-bg-color, #fff);
|
||||
}
|
||||
|
||||
/* edit-header:参考主题弹窗头(关闭钮靠右,红色警示徽标 + 标题居左)。 */
|
||||
.aksrt-report-modal .edit-header {
|
||||
position: relative;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.aksrt-report-modal .edit-header .close {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
float: none;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
/* 原因徽章行:与参考主题一致,徽章可换行且间距走 mr6/mb6 工具类。 */
|
||||
.aksrt-report-modal .form-but-radio .badg {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ---- 禁封用户弹窗(参考主题 set_user_ban 模态框几何 1:1)----
|
||||
参考主题 zib_get_edit_user_ban_link 未传 data-class → 走 Bootstrap 默认
|
||||
.modal-dialog(>=768px 宽 600px);data-height=360 → .modal-content 初始
|
||||
@@ -562,14 +527,14 @@ body.aksrt-modal-runtime-open {
|
||||
color: var(--color-ink, #111);
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview {
|
||||
.order-comment-from .form-upload .preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .add,
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item {
|
||||
.order-comment-from .form-upload .preview .add,
|
||||
.order-comment-from .form-upload .preview .preview-item {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 10px;
|
||||
@@ -585,14 +550,14 @@ body.aksrt-modal-runtime-open {
|
||||
此前这里的 left/top:50% + translate 版本与该规则叠加导致过约束,
|
||||
加号被推到 75%/75% 只剩“L”形,故整体删除、统一走主样式包。 */
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item img {
|
||||
.order-comment-from .form-upload .preview .preview-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .preview .preview-item .remove {
|
||||
.order-comment-from .form-upload .preview .preview-item .remove {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
@@ -607,7 +572,7 @@ body.aksrt-modal-runtime-open {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:is(.order-comment-from, .aksrt-report-modal) .form-upload .hide {
|
||||
.order-comment-from .form-upload .hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Report form inside the shared modal runtime.
|
||||
* @package AKSRT
|
||||
*/
|
||||
.aksrt-report-modal .aksrt-modal-runtime__dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
max-height: min(90vh, 820px);
|
||||
max-height: min(90dvh, 820px);
|
||||
background: var(--modal-surface);
|
||||
color: var(--modal-ink);
|
||||
}
|
||||
|
||||
.aksrt-report-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
|
||||
width: min(600px, calc(100vw - 32px));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
}
|
||||
|
||||
.aksrt-report-modal .aksrt-modal-runtime__header {
|
||||
position: relative;
|
||||
flex: none;
|
||||
padding: 24px 64px 18px 24px;
|
||||
border-bottom: 1px solid var(--modal-line-soft);
|
||||
}
|
||||
|
||||
.aksrt-report-modal .aksrt-modal-runtime__title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.aksrt-report-modal .aksrt-modal-runtime__close {
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 26px;
|
||||
color: var(--modal-muted);
|
||||
}
|
||||
|
||||
.aksrt-report-modal .aksrt-modal-runtime__content {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.aksrt-report-form {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.aksrt-report-body {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 22px 24px 24px;
|
||||
}
|
||||
|
||||
.aksrt-report-target {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 8px;
|
||||
background: var(--modal-surface-3);
|
||||
}
|
||||
|
||||
.aksrt-report-target > div { min-width: 0; }
|
||||
|
||||
.aksrt-report-target__icon {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid var(--modal-line);
|
||||
border-radius: 8px;
|
||||
background: var(--modal-surface);
|
||||
color: var(--modal-ink-2);
|
||||
}
|
||||
|
||||
.aksrt-report-target__name {
|
||||
display: block;
|
||||
margin: 2px 0 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.aksrt-report-hint,
|
||||
.aksrt-report-count {
|
||||
color: var(--modal-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.aksrt-report-form p { margin: 6px 0 0; }
|
||||
.aksrt-report-target p { margin: 0; }
|
||||
.aksrt-report-count { font-variant-numeric: tabular-nums; }
|
||||
|
||||
.aksrt-report-reasons {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0 0 22px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.aksrt-report-reasons legend {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
margin: 0 0 10px;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.aksrt-report-label .aksrt-report-hint,
|
||||
.aksrt-report-reasons legend .aksrt-report-hint { margin-left: 8px; }
|
||||
|
||||
.aksrt-report-reasons__options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aksrt-report-reason {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-height: 44px;
|
||||
padding: 10px 12px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--modal-line);
|
||||
border-radius: 8px;
|
||||
background: var(--modal-surface);
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.aksrt-report-reason input[type="radio"] {
|
||||
position: static;
|
||||
display: block;
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
opacity: 1;
|
||||
appearance: auto;
|
||||
accent-color: var(--modal-primary);
|
||||
}
|
||||
|
||||
.aksrt-report-reason:has(input:checked) {
|
||||
border-color: var(--modal-primary);
|
||||
background: color-mix(in srgb, var(--modal-primary) 6%, var(--modal-surface));
|
||||
}
|
||||
|
||||
.aksrt-report-reason input:checked + span { font-weight: 600; }
|
||||
.aksrt-report-other { margin-top: 12px; }
|
||||
.aksrt-report-field + .aksrt-report-field { margin-top: 22px; }
|
||||
|
||||
.aksrt-report-label-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.aksrt-report-label {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-report-form textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
max-height: 240px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid var(--modal-line);
|
||||
border-radius: 8px;
|
||||
background: var(--modal-surface);
|
||||
color: var(--modal-ink);
|
||||
font: inherit;
|
||||
line-height: 1.65;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.aksrt-report-form textarea::placeholder { color: var(--modal-muted); }
|
||||
.aksrt-report-other textarea { margin-top: 8px; }
|
||||
.aksrt-report-form [aria-invalid="true"] { border-color: var(--modal-danger); }
|
||||
|
||||
.aksrt-report-images,
|
||||
.aksrt-report-images__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.aksrt-report-images__list:empty { display: none; }
|
||||
|
||||
.aksrt-report-add,
|
||||
.aksrt-report-image {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
flex: none;
|
||||
border: 1px solid var(--modal-line);
|
||||
border-radius: 8px;
|
||||
background: var(--modal-surface-3);
|
||||
}
|
||||
|
||||
.aksrt-report-add {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-style: dashed;
|
||||
color: var(--modal-ink-2);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aksrt-report-add i { font-size: 18px; }
|
||||
.aksrt-report-image img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
|
||||
|
||||
.aksrt-report-image__state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
color: var(--modal-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.aksrt-report-image[data-upload-state="ready"] .aksrt-report-image__state { display: none; }
|
||||
.aksrt-report-image[data-upload-state="error"] { border-color: var(--modal-danger); }
|
||||
.aksrt-report-image [data-aksrt-report-retry] { min-width: 44px; min-height: 28px; border: 0; background: transparent; color: var(--modal-primary); text-decoration: underline; cursor: pointer; }
|
||||
|
||||
.aksrt-report-image__remove {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--modal-line);
|
||||
border-radius: 50%;
|
||||
background: var(--modal-surface);
|
||||
color: var(--modal-ink-2);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aksrt-report-notice {
|
||||
margin-top: 22px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--modal-line-soft);
|
||||
color: var(--modal-ink-2);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.aksrt-report-notice > :first-child { margin-top: 0; }
|
||||
.aksrt-report-notice > :last-child { margin-bottom: 0; }
|
||||
|
||||
.aksrt-report-footer {
|
||||
flex: none;
|
||||
padding: 0 24px max(20px, env(safe-area-inset-bottom));
|
||||
border-top: 1px solid var(--modal-line-soft);
|
||||
background: var(--modal-surface);
|
||||
}
|
||||
|
||||
.aksrt-report-form .aksrt-report-error { margin: 12px 0 0; color: var(--modal-danger); font-size: 13px; }
|
||||
.aksrt-report-form .aksrt-report-status { color: var(--modal-muted); font-size: 12px; }
|
||||
.aksrt-report-form .aksrt-report-status:empty { display: none; }
|
||||
|
||||
.aksrt-report-actions { display: flex; justify-content: flex-end; gap: 10px; padding-top: 16px; }
|
||||
|
||||
.aksrt-report-actions button {
|
||||
min-width: 96px;
|
||||
min-height: 44px;
|
||||
padding: 10px 20px;
|
||||
border: 1px solid var(--modal-line);
|
||||
border-radius: 8px;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aksrt-report-cancel { color: var(--modal-ink-2); background: var(--modal-surface); }
|
||||
.aksrt-report-actions .aksrt-report-submit { color: var(--modal-on-primary); background: var(--modal-primary); border-color: transparent; }
|
||||
.aksrt-report-form button:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
.aksrt-report-form textarea:disabled { opacity: 0.65; }
|
||||
.aksrt-report-modal :is(button, input, textarea, a):focus-visible { outline: 2px solid var(--modal-primary); outline-offset: 3px; }
|
||||
.aksrt-report-modal [hidden] { display: none !important; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.aksrt-report-modal .aksrt-modal-runtime__header { padding: 20px 64px 16px 20px; }
|
||||
.aksrt-report-body { padding: 18px 20px 22px; }
|
||||
.aksrt-report-footer { padding-right: 20px; padding-left: 20px; }
|
||||
.aksrt-report-actions button { flex: 1; min-width: 0; }
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.aksrt-report-reasons__options { grid-template-columns: 1fr; }
|
||||
}
|
||||
+266
-346
@@ -1,393 +1,313 @@
|
||||
/**
|
||||
* AKSRT Report — 前台统一举报弹窗
|
||||
*
|
||||
* 监听 [data-aksrt-report] 点击,弹出对齐参考主题的举报弹窗(像素级同构):
|
||||
* edit-header 红色警示头 + 举报原因徽章(active 状态走 .active.badg 焦点色)
|
||||
* + 违规链接 + 详细描述 + 图片举证(后台开启 user_report_img_s 时)
|
||||
* + 举报说明(后台 user_report_desc,支持 HTML)+ but-average modal-buts 底栏。
|
||||
* 提交走 REST /aksrt/v1/report/submit(user/comment/post 三类目标统一入口),
|
||||
* 举证图片走 /aksrt/v1/report/upload-image(最多 3 张,URL 拼入举报内容)。
|
||||
*
|
||||
* Frontend reporting for articles, comments and users.
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var API_ROOT = (function () {
|
||||
var d = window.aksrtData || {};
|
||||
var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/aksrt/v1';
|
||||
return String(raw).replace(/\/$/, '');
|
||||
})();
|
||||
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
|
||||
|
||||
var TARGET_TITLES = {
|
||||
user: '举报用户',
|
||||
post: '举报文章',
|
||||
comment: '举报评论',
|
||||
};
|
||||
|
||||
var config = window.aksrtData || {};
|
||||
var API_ROOT = String(config.apiUrl || '/index.php?rest_route=/aksrt/v1').replace(/\/$/, '');
|
||||
var NONCE = config.restNonce || '';
|
||||
var MAX_IMAGES = 3;
|
||||
|
||||
function getReportCategories() {
|
||||
// 预置原因:后台“用户&互动 → 禁封/举报 → 预置原因”(ban_preset_reason,
|
||||
// 由 UserRuntime::ban_settings 解析,aksrtData.reportCategories 注入)。
|
||||
var list = window.aksrtData && window.aksrtData.reportCategories;
|
||||
if (Array.isArray(list)) {
|
||||
var items = list
|
||||
.map(function (cat) {
|
||||
return {
|
||||
slug: String((cat && (cat.slug || cat.value)) || ''),
|
||||
name: String((cat && (cat.name || cat.label || cat.t)) || ''),
|
||||
};
|
||||
})
|
||||
.filter(function (cat) { return cat.name !== ''; });
|
||||
if (items.length) return items;
|
||||
}
|
||||
return [
|
||||
{ slug: 'spam', name: '垃圾信息' },
|
||||
{ slug: 'abuse', name: '人身攻击' },
|
||||
{ slug: 'copyright', name: '版权投诉' },
|
||||
];
|
||||
}
|
||||
|
||||
function reportDescHtml() {
|
||||
// 举报说明:后台富文本(服务端已 wp_kses_post),直接注入。
|
||||
var d = window.aksrtData && window.aksrtData.report;
|
||||
return (d && typeof d.desc === 'string') ? d.desc : '';
|
||||
}
|
||||
|
||||
function imgUploadEnabled() {
|
||||
var d = window.aksrtData && window.aksrtData.report;
|
||||
return !!(d && d.imgEnabled);
|
||||
}
|
||||
var sequence = 0;
|
||||
var TARGETS = {
|
||||
user: { title: '举报用户', label: '用户', icon: 'fa-user' },
|
||||
post: { title: '举报文章', label: '文章', icon: 'fa-file-alt' },
|
||||
comment: { title: '举报评论', label: '评论', icon: 'fa-comment' },
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function buildReportModalHtml(data) {
|
||||
var categories = getReportCategories();
|
||||
var title = TARGET_TITLES[data.target_type] || '举报不良信息';
|
||||
function getReportCategories() {
|
||||
return (Array.isArray(config.reportCategories) ? config.reportCategories : [])
|
||||
.filter(function (cat) { return cat && cat.name; });
|
||||
}
|
||||
|
||||
var html = '';
|
||||
|
||||
// 弹窗头:参考主题 edit-header 同构(关闭钮 + 红色警示徽标 + 标题)。
|
||||
html += '<div class="mb10 edit-header touch">';
|
||||
html += '<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>';
|
||||
html += '<b class="flex ac c-red"><span class="toggle-radius mr10 c-red"><i class="fa fas fa-exclamation-triangle" aria-hidden="true"></i></span>' + escapeHtml(title) + '</b>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<form class="aksrt-report-form dependency-box">';
|
||||
html += '<div class="scroll-y mini-scrollbar max-vh7">';
|
||||
html += '<div class="mt10 mb20">';
|
||||
|
||||
// 被举报对象(头像 + 名称),由服务端渲染进 data-aksrt-report。
|
||||
if (data.target_avatar || data.target_name) {
|
||||
html += '<div class="flex ac mb6">';
|
||||
if (data.target_avatar) {
|
||||
html += '<img class="avatar-mini mr6" src="' + escapeHtml(data.target_avatar) + '" alt="">';
|
||||
}
|
||||
html += '<b>' + escapeHtml(data.target_name || '') + '</b>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// 举报原因(徽章单选,active 走参考主题焦点色;含“其它原因”展开自定义文本)。
|
||||
html += '<div class="form-radio mb20 form-but-radio">';
|
||||
html += '<div class="em09 muted-2-color mb6">举报原因</div>';
|
||||
categories.forEach(function (cat) {
|
||||
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-report-reason="' + escapeHtml(cat.name) + '" data-reason-slug="' + escapeHtml(cat.slug) + '">' + escapeHtml(cat.name) + '</span>';
|
||||
function buildReportModalHtml(data, categories) {
|
||||
var target = TARGETS[data.target_type];
|
||||
var report = config.report || {};
|
||||
var id = 'aksrt-report-' + (++sequence);
|
||||
var html = '<form class="aksrt-report-form" novalidate>';
|
||||
html += '<div class="aksrt-report-body mini-scrollbar">';
|
||||
html += '<div class="aksrt-report-target">';
|
||||
html += '<span class="aksrt-report-target__icon" aria-hidden="true"><i class="fas ' + target.icon + '"></i></span>';
|
||||
html += '<div><span class="aksrt-report-hint">举报对象 · ' + target.label + '</span>';
|
||||
html += '<strong class="aksrt-report-target__name">' + escapeHtml(data.target_name || target.label + ' #' + data.target_id) + '</strong>';
|
||||
html += '<p class="aksrt-report-hint">已自动关联该' + target.label + ',无需填写链接</p></div></div>';
|
||||
html += '<fieldset class="aksrt-report-reasons" aria-describedby="' + id + '-reason-error">';
|
||||
html += '<legend>举报原因<span class="aksrt-report-hint">必选一项</span></legend>';
|
||||
html += '<div class="aksrt-report-reasons__options">';
|
||||
categories.forEach(function (cat, index) {
|
||||
html += '<label class="aksrt-report-reason"><input type="radio" name="report_reason" required value="' + index + '">';
|
||||
html += '<span>' + escapeHtml(cat.name) + '</span></label>';
|
||||
});
|
||||
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-report-reason="other">其它原因</span>';
|
||||
html += '<div class="aksrt-report-other mb20" style="display: none;">';
|
||||
html += '<textarea class="form-control" name="reason_other" rows="2" placeholder="请填写举报原因"></textarea>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
// 违规链接。
|
||||
html += '<div class="mb20">';
|
||||
html += '<div class="em09 muted-2-color mb6">违规链接</div>';
|
||||
html += '<input type="text" class="form-control" name="report_url" placeholder="请输入违规链接">';
|
||||
html += '</div>';
|
||||
|
||||
// 详细描述。
|
||||
html += '<div class="mb20">';
|
||||
html += '<div class="em09 muted-2-color mb6">详细描述</div>';
|
||||
html += '<textarea class="form-control" name="report_desc" rows="2" placeholder="请详细描述违规事项"></textarea>';
|
||||
html += '</div>';
|
||||
|
||||
// 图片举证(后台 user_report_img_s 开启时渲染,最多 3 张)。
|
||||
if (imgUploadEnabled()) {
|
||||
html += '<div class="form-upload mb10">';
|
||||
html += '<div class="em09 muted-2-color mb6">添加图片举证</div>';
|
||||
html += '<div class="preview">';
|
||||
html += '<div class="add" data-aksrt-report-add role="button" aria-label="添加图片举证"></div>';
|
||||
html += '</div>';
|
||||
html += '<input class="hide" type="file" accept="image/gif,image/jpeg,image/jpg,image/png" data-aksrt-report-file>';
|
||||
html += '</div>';
|
||||
html += '<label class="aksrt-report-reason"><input type="radio" name="report_reason" required value="other"><span>其他原因</span></label>';
|
||||
html += '</div><div class="aksrt-report-other" hidden>';
|
||||
html += '<label class="aksrt-report-label" for="' + id + '-other">请说明举报原因</label>';
|
||||
html += '<textarea id="' + id + '-other" name="reason_other" rows="2" maxlength="200" placeholder="请简要说明你认为存在的问题" aria-describedby="' + id + '-reason-error"></textarea>';
|
||||
html += '</div><p class="aksrt-report-error" id="' + id + '-reason-error" data-aksrt-report-reason-error role="alert" hidden></p></fieldset>';
|
||||
html += '<div class="aksrt-report-field"><div class="aksrt-report-label-row">';
|
||||
html += '<label class="aksrt-report-label" for="' + id + '-desc">补充说明<span class="aksrt-report-hint">选填</span></label>';
|
||||
html += '<span class="aksrt-report-count" data-aksrt-report-count aria-hidden="true">0 / 2000</span></div>';
|
||||
html += '<textarea id="' + id + '-desc" name="report_desc" rows="3" maxlength="2000" placeholder="例如:问题出现在什么位置,具体有哪些违规内容"></textarea></div>';
|
||||
if (report.imgEnabled) {
|
||||
html += '<section class="aksrt-report-field" aria-label="图片举证">';
|
||||
html += '<div class="aksrt-report-label-row"><span class="aksrt-report-label">图片举证<span class="aksrt-report-hint">选填</span></span>';
|
||||
html += '<span class="aksrt-report-count" data-aksrt-report-image-count aria-live="polite">0 / ' + MAX_IMAGES + '</span></div>';
|
||||
html += '<div class="aksrt-report-images"><div class="aksrt-report-images__list" data-aksrt-report-images></div>';
|
||||
html += '<button type="button" class="aksrt-report-add" data-aksrt-report-add><i class="fas fa-plus" aria-hidden="true"></i><span>添加图片</span></button></div>';
|
||||
html += '<p class="aksrt-report-hint">支持 JPG、PNG、GIF,最多 ' + MAX_IMAGES + ' 张</p>';
|
||||
html += '<input type="file" accept="image/gif,image/jpeg,image/png" multiple data-aksrt-report-file hidden></section>';
|
||||
}
|
||||
|
||||
// 举报说明(后台 user_report_desc,支持 HTML)。
|
||||
var desc = reportDescHtml();
|
||||
if (desc) {
|
||||
html += '<div class="em09 padding-h10 muted-box mb10">' + desc + '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// 底栏:参考主题 but-average modal-buts 双按钮。
|
||||
html += '<div class="but-average modal-buts">';
|
||||
html += '<button type="button" class="but" data-aksrt-modal-close>取消</button>';
|
||||
html += '<button type="button" class="but c-blue" data-aksrt-report-submit><i class="fa fas fa-check" aria-hidden="true"></i>确认提交</button>';
|
||||
html += '</div>';
|
||||
html += '</form>';
|
||||
|
||||
// The settings reader sanitizes this administrator-authored notice with wp_kses_post.
|
||||
if (report.desc) html += '<div class="aksrt-report-notice">' + report.desc + '</div>';
|
||||
html += '</div><footer class="aksrt-report-footer">';
|
||||
html += '<p class="aksrt-report-error" data-aksrt-report-error role="alert" hidden></p>';
|
||||
html += '<p class="aksrt-report-status" data-aksrt-report-status role="status"></p>';
|
||||
html += '<div class="aksrt-report-actions"><button type="button" class="aksrt-report-cancel" data-aksrt-modal-close>取消</button>';
|
||||
html += '<button type="submit" class="aksrt-report-submit" data-aksrt-report-submit>提交举报</button></div>';
|
||||
html += '</footer></form>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function selectedReason(shell) {
|
||||
var active = shell.querySelector('[data-aksrt-report-reason].active');
|
||||
if (!active) return { label: '', slug: '' };
|
||||
return {
|
||||
label: active.getAttribute('data-aksrt-report-reason') || '',
|
||||
slug: active.getAttribute('data-reason-slug') || '',
|
||||
};
|
||||
function showError(element, message) {
|
||||
element.textContent = message;
|
||||
element.hidden = !message;
|
||||
}
|
||||
|
||||
function collectImages(shell) {
|
||||
return Array.prototype.map.call(
|
||||
shell.querySelectorAll('.preview .preview-item img'),
|
||||
function (img) { return img.getAttribute('src') || ''; }
|
||||
).filter(function (url) { return url !== ''; });
|
||||
}
|
||||
function bindReportModal(shell, data, categories) {
|
||||
var form = shell.querySelector('.aksrt-report-form');
|
||||
var submit = form.querySelector('[data-aksrt-report-submit]');
|
||||
var reasonError = form.querySelector('[data-aksrt-report-reason-error]');
|
||||
var error = form.querySelector('[data-aksrt-report-error]');
|
||||
var status = form.querySelector('[data-aksrt-report-status]');
|
||||
var otherBox = form.querySelector('.aksrt-report-other');
|
||||
var otherInput = form.querySelector('[name="reason_other"]');
|
||||
var descInput = form.querySelector('[name="report_desc"]');
|
||||
var fileInput = form.querySelector('[data-aksrt-report-file]');
|
||||
var images = [];
|
||||
var submitting = false;
|
||||
var closed = false;
|
||||
|
||||
function uploadImage(shell, file) {
|
||||
var preview = shell.querySelector('.form-upload .preview');
|
||||
if (!preview) return Promise.resolve();
|
||||
|
||||
var existing = shell.querySelectorAll('.preview .preview-item').length;
|
||||
if (existing + collectImages(shell).length >= MAX_IMAGES) {
|
||||
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
|
||||
return Promise.resolve();
|
||||
function isActive() {
|
||||
return !closed && window.AksrtModalRuntime.getActive() === shell;
|
||||
}
|
||||
|
||||
var body = new FormData();
|
||||
body.append('image', file);
|
||||
function syncState() {
|
||||
var uploading = images.some(function (image) { return image.status === 'uploading'; });
|
||||
submit.disabled = submitting || uploading;
|
||||
submit.textContent = submitting ? '正在提交…' : uploading ? '图片上传中…' : '提交举报';
|
||||
form.setAttribute('aria-busy', String(submitting || uploading));
|
||||
status.textContent = submitting ? '正在提交举报,请稍候' : uploading ? '图片正在上传,完成后即可提交' : '';
|
||||
var add = form.querySelector('[data-aksrt-report-add]');
|
||||
if (add) {
|
||||
add.disabled = submitting || images.length >= MAX_IMAGES;
|
||||
form.querySelector('[data-aksrt-report-image-count]').textContent = images.length + ' / ' + MAX_IMAGES;
|
||||
}
|
||||
}
|
||||
|
||||
return window.fetch(API_ROOT + '/report/upload-image', {
|
||||
method: 'POST',
|
||||
headers: { 'X-WP-Nonce': NONCE },
|
||||
body: body,
|
||||
})
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (result) {
|
||||
var url = result && result.success && result.data && result.data.url;
|
||||
if (!url) {
|
||||
notyf((result && result.message) || '图片上传失败', 'danger');
|
||||
function deletePending(image) {
|
||||
if (!image.attachmentId) return;
|
||||
window.fetch(API_ROOT + '/upload/pending/' + encodeURIComponent(image.attachmentId), {
|
||||
method: 'DELETE', headers: { 'X-WP-Nonce': NONCE },
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function renderImage(image) {
|
||||
var html = image.url ? '<img src="' + escapeHtml(image.url) + '" alt="举证图片">' : '';
|
||||
html += '<div class="aksrt-report-image__state">';
|
||||
if (image.status === 'uploading') html += '<span role="status">上传中…</span>';
|
||||
if (image.status === 'error') {
|
||||
html += '<span>上传失败</span><button type="button" data-aksrt-report-retry>重试</button>';
|
||||
}
|
||||
html += '</div><button type="button" class="aksrt-report-image__remove" data-aksrt-report-remove aria-label="删除举证图片"' + (submitting ? ' disabled' : '') + '>×</button>';
|
||||
image.node.innerHTML = html;
|
||||
image.node.setAttribute('data-upload-state', image.status);
|
||||
}
|
||||
|
||||
function uploadImage(image) {
|
||||
image.status = 'uploading';
|
||||
renderImage(image);
|
||||
syncState();
|
||||
var body = new FormData();
|
||||
body.append('image', image.file);
|
||||
window.fetch(API_ROOT + '/report/upload-image', {
|
||||
method: 'POST', headers: { 'X-WP-Nonce': NONCE }, body: body,
|
||||
}).then(function (response) { return response.json(); }).then(function (result) {
|
||||
if (!result || !result.success || !result.data || !result.data.url) {
|
||||
throw new Error((result && result.message) || '图片上传失败,请重试');
|
||||
}
|
||||
image.attachmentId = result.data.id;
|
||||
if (closed || images.indexOf(image) === -1) {
|
||||
deletePending(image);
|
||||
return;
|
||||
}
|
||||
var item = document.createElement('div');
|
||||
item.className = 'preview-item';
|
||||
var img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.alt = '';
|
||||
var remove = document.createElement('button');
|
||||
remove.type = 'button';
|
||||
remove.className = 'remove';
|
||||
remove.setAttribute('aria-label', '删除图片');
|
||||
remove.innerHTML = '×';
|
||||
var uploadId = result.data && result.data.id;
|
||||
remove.addEventListener('click', function () {
|
||||
item.remove();
|
||||
// 删除服务端待认领附件;失败由定时任务兜底清理。
|
||||
if (uploadId) {
|
||||
window.fetch(API_ROOT + '/upload/pending/' + encodeURIComponent(uploadId), {
|
||||
method: 'DELETE',
|
||||
headers: { 'X-WP-Nonce': NONCE },
|
||||
}).catch(function () {});
|
||||
}
|
||||
});
|
||||
item.appendChild(img);
|
||||
item.appendChild(remove);
|
||||
preview.insertBefore(item, preview.querySelector('.add'));
|
||||
})
|
||||
.catch(function () {
|
||||
notyf('图片上传失败,请重试', 'danger');
|
||||
image.url = result.data.url;
|
||||
image.status = 'ready';
|
||||
renderImage(image);
|
||||
}).catch(function (failure) {
|
||||
if (closed || images.indexOf(image) === -1) return;
|
||||
image.status = 'error';
|
||||
renderImage(image);
|
||||
showError(error, failure.message || '图片上传失败,请重试');
|
||||
}).then(function () {
|
||||
if (!closed) syncState();
|
||||
});
|
||||
}
|
||||
|
||||
function submitReport(data, shell) {
|
||||
var form = shell.querySelector('.aksrt-report-form');
|
||||
if (!form) return;
|
||||
|
||||
var submitBtn = form.querySelector('[data-aksrt-report-submit]');
|
||||
var descInput = form.querySelector('[name="report_desc"]');
|
||||
var otherInput = form.querySelector('[name="reason_other"]');
|
||||
var urlInput = form.querySelector('[name="report_url"]');
|
||||
|
||||
var selected = selectedReason(shell);
|
||||
var other = (otherInput && otherInput.value || '').trim();
|
||||
|
||||
if (!selected.label && !other) {
|
||||
notyf('请选择或输入举报原因', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// “其它原因”以自定义文本为举报原因;预置徽章取徽章文案(参考主题同语义)。
|
||||
var reason = selected.label === 'other' || !selected.label ? other : selected.label;
|
||||
var slug = selected.label && selected.label !== 'other' ? selected.slug : '';
|
||||
|
||||
if (!reason) {
|
||||
notyf('请填写举报原因', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
var link = (urlInput && urlInput.value || '').trim();
|
||||
if (link) {
|
||||
reason += (reason ? '\n' : '') + '违规链接:' + link;
|
||||
}
|
||||
var images = collectImages(shell);
|
||||
if (images.length) {
|
||||
reason += '\n图片举证:';
|
||||
images.forEach(function (url) { reason += '\n' + url; });
|
||||
}
|
||||
|
||||
submitBtn.disabled = true;
|
||||
notyf('正在提交举报…', 'load', 3000, 'aksrt-report-submit');
|
||||
|
||||
window.fetch(API_ROOT + '/report/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': NONCE,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
target_type: data.target_type,
|
||||
target_id: data.target_id,
|
||||
category: slug,
|
||||
reason: reason,
|
||||
}),
|
||||
})
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (result) {
|
||||
if (result && result.success) {
|
||||
window.AksrtModalRuntime.close('submit');
|
||||
notyf(result.message || '举报已提交,我们会尽快处理', 'success');
|
||||
} else {
|
||||
submitBtn.disabled = false;
|
||||
notyf((result && result.message) || '举报提交失败', 'danger');
|
||||
form.addEventListener('change', function (event) {
|
||||
if (event.target.name === 'report_reason') {
|
||||
var other = event.target.value === 'other';
|
||||
otherBox.hidden = !other;
|
||||
otherInput.disabled = !other;
|
||||
otherInput.required = other;
|
||||
otherInput.removeAttribute('aria-invalid');
|
||||
showError(reasonError, '');
|
||||
if (other) otherInput.focus();
|
||||
}
|
||||
if (event.target !== fileInput || submitting) return;
|
||||
var files = Array.prototype.slice.call(fileInput.files || []);
|
||||
showError(error, '');
|
||||
files.forEach(function (file) {
|
||||
if (images.length >= MAX_IMAGES) {
|
||||
showError(error, '最多上传 ' + MAX_IMAGES + ' 张图片,请先删除不需要的图片');
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
submitBtn.disabled = false;
|
||||
notyf('网络错误,请重试', 'danger');
|
||||
})
|
||||
.then(function () {
|
||||
var toast = document.getElementById('aksrt-report-submit');
|
||||
if (toast) toast.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function bindAutoHeight(textarea) {
|
||||
// 对齐参考主题 autoHeight 行为:随内容自动增高,最小保持初始行数高度。
|
||||
// 隐藏中的 textarea(如“其它原因”)跳过首次计算,待可见后按实际高度增长。
|
||||
if (!textarea || textarea.dataset.aksrtAutoHeight === '1') return;
|
||||
textarea.dataset.aksrtAutoHeight = '1';
|
||||
var minHeight = 0;
|
||||
var grow = function () {
|
||||
if (!minHeight) minHeight = textarea.offsetHeight || 0;
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.max(textarea.scrollHeight, minHeight) + 'px';
|
||||
};
|
||||
textarea.addEventListener('input', grow);
|
||||
if (textarea.offsetHeight > 0) grow();
|
||||
}
|
||||
|
||||
function bindReportModal(shell) {
|
||||
shell.querySelectorAll('textarea.form-control').forEach(bindAutoHeight);
|
||||
|
||||
shell.addEventListener('click', function (event) {
|
||||
// 原因徽章:单选 active(参考主题 .active.badg 焦点色),其它原因展开文本框。
|
||||
// 参考主题行为:重复点击已选徽章保持选中(先全清再激活),不做反选。
|
||||
var chip = event.target.closest('[data-aksrt-report-reason]');
|
||||
if (chip) {
|
||||
event.preventDefault();
|
||||
shell.querySelectorAll('[data-aksrt-report-reason].active').forEach(function (el) {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
chip.classList.add('active');
|
||||
var otherBox = shell.querySelector('.aksrt-report-other');
|
||||
if (otherBox) otherBox.style.display = chip.getAttribute('data-aksrt-report-reason') === 'other' ? '' : 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// 图片举证:点击加号或预览补位触发文件选择。
|
||||
var addBtn = event.target.closest('[data-aksrt-report-add]');
|
||||
if (addBtn) {
|
||||
event.preventDefault();
|
||||
var fileInput = shell.querySelector('[data-aksrt-report-file]');
|
||||
if (fileInput) fileInput.click();
|
||||
return;
|
||||
}
|
||||
|
||||
var submitBtn = event.target.closest('[data-aksrt-report-submit]');
|
||||
if (submitBtn) {
|
||||
event.preventDefault();
|
||||
submitReport(shell.getAttribute('data-aksrt-report-target') ? JSON.parse(shell.getAttribute('data-aksrt-report-target')) : {}, shell);
|
||||
}
|
||||
});
|
||||
|
||||
shell.addEventListener('change', function (event) {
|
||||
var fileInput = event.target.closest('[data-aksrt-report-file]');
|
||||
if (!fileInput || !fileInput.files || !fileInput.files.length) return;
|
||||
Array.prototype.forEach.call(fileInput.files, function (file) {
|
||||
uploadImage(shell, file);
|
||||
if (!/^image\/(jpeg|png|gif)$/.test(file.type)) {
|
||||
showError(error, '请选择 JPG、PNG 或 GIF 图片');
|
||||
return;
|
||||
}
|
||||
var image = { file: file, status: 'uploading', url: '', attachmentId: 0, node: document.createElement('div') };
|
||||
image.node.className = 'aksrt-report-image';
|
||||
images.push(image);
|
||||
form.querySelector('[data-aksrt-report-images]').appendChild(image.node);
|
||||
uploadImage(image);
|
||||
});
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
form.addEventListener('input', function (event) {
|
||||
if (event.target === descInput) form.querySelector('[data-aksrt-report-count]').textContent = descInput.value.length + ' / 2000';
|
||||
if (event.target === otherInput) {
|
||||
otherInput.removeAttribute('aria-invalid');
|
||||
showError(reasonError, '');
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('click', function (event) {
|
||||
var add = event.target.closest('[data-aksrt-report-add]');
|
||||
if (add && !add.disabled && !submitting) fileInput.click();
|
||||
var imageNode = event.target.closest('.aksrt-report-image');
|
||||
if (!imageNode || submitting) return;
|
||||
var image = images.find(function (entry) { return entry.node === imageNode; });
|
||||
if (!image) return;
|
||||
if (event.target.closest('[data-aksrt-report-remove]')) {
|
||||
images.splice(images.indexOf(image), 1);
|
||||
image.node.remove();
|
||||
deletePending(image);
|
||||
showError(error, '');
|
||||
syncState();
|
||||
} else if (event.target.closest('[data-aksrt-report-retry]') && image.status === 'error') {
|
||||
showError(error, '');
|
||||
uploadImage(image);
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
if (submitting || closed) return;
|
||||
showError(error, '');
|
||||
var selected = form.querySelector('[name="report_reason"]:checked');
|
||||
if (!selected) {
|
||||
showError(reasonError, '请选择一项举报原因');
|
||||
form.querySelector('[name="report_reason"]').focus();
|
||||
return;
|
||||
}
|
||||
var category = selected.value === 'other' ? null : categories[Number(selected.value)];
|
||||
var reason = category ? String(category.name) : otherInput.value.trim();
|
||||
if (!reason) {
|
||||
showError(reasonError, '请填写举报原因');
|
||||
otherInput.setAttribute('aria-invalid', 'true');
|
||||
otherInput.focus();
|
||||
return;
|
||||
}
|
||||
if (images.some(function (image) { return image.status !== 'ready'; })) {
|
||||
showError(error, '请等待图片上传完成,或重试、删除上传失败的图片');
|
||||
return;
|
||||
}
|
||||
var description = descInput.value.trim();
|
||||
if (description) reason += '\n详细描述:\n' + description;
|
||||
if (images.length) reason += '\n图片举证:\n' + images.map(function (image) { return image.url; }).join('\n');
|
||||
submitting = true;
|
||||
syncState();
|
||||
form.querySelectorAll('input, textarea, [data-aksrt-report-remove], [data-aksrt-report-retry]').forEach(function (element) { element.disabled = true; });
|
||||
window.fetch(API_ROOT + '/report/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE },
|
||||
body: JSON.stringify({ target_type: data.target_type, target_id: data.target_id, category: category ? category.slug : '', reason: reason }),
|
||||
}).then(function (response) { return response.json(); }).then(function (result) {
|
||||
if (!result || !result.success) throw new Error((result && result.message) || '举报提交失败,请重试');
|
||||
// Submitted evidence belongs to the report even if its dialog has already closed.
|
||||
images = [];
|
||||
form.setAttribute('aria-busy', 'false');
|
||||
submit.textContent = '举报已提交';
|
||||
status.textContent = result.message || '举报已提交,我们会尽快处理';
|
||||
form.querySelector('[data-aksrt-modal-close]').textContent = '关闭';
|
||||
if (isActive()) window.AksrtModalRuntime.close('submit');
|
||||
notyf(result.message || '举报已提交,我们会尽快处理', 'success');
|
||||
}).catch(function (failure) {
|
||||
submitting = false;
|
||||
if (closed) {
|
||||
images.forEach(deletePending);
|
||||
return;
|
||||
}
|
||||
form.querySelectorAll('input, textarea, [data-aksrt-report-remove], [data-aksrt-report-retry]').forEach(function (element) { element.disabled = false; });
|
||||
otherInput.disabled = otherBox.hidden;
|
||||
syncState();
|
||||
showError(error, failure.message || '网络错误,请重试');
|
||||
});
|
||||
});
|
||||
|
||||
function onClose(event) {
|
||||
if (!event.detail || event.detail.shell !== shell) return;
|
||||
closed = true;
|
||||
document.removeEventListener('aksrt:modal:close', onClose);
|
||||
// While submitting, the response decides whether pending evidence is still needed.
|
||||
if (!submitting) images.forEach(deletePending);
|
||||
}
|
||||
document.addEventListener('aksrt:modal:close', onClose);
|
||||
otherInput.disabled = true;
|
||||
syncState();
|
||||
}
|
||||
|
||||
function openReportModal(data) {
|
||||
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') {
|
||||
function openReportModal(data, opener) {
|
||||
var runtime = window.AksrtModalRuntime;
|
||||
if (!runtime || typeof runtime.open !== 'function') {
|
||||
notyf('举报组件加载失败,请刷新页面后重试', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
var shell = window.AksrtModalRuntime.open({
|
||||
html: buildReportModalHtml(data),
|
||||
className: 'aksrt-report-modal',
|
||||
kind: 'report',
|
||||
mobileBottom: true,
|
||||
var categories = getReportCategories();
|
||||
var shell = runtime.open({
|
||||
html: buildReportModalHtml(data, categories), title: TARGETS[data.target_type].title,
|
||||
className: 'aksrt-report-modal', kind: 'report', mobileBottom: true, focusReturn: opener,
|
||||
});
|
||||
shell.setAttribute('data-aksrt-report-target', JSON.stringify({
|
||||
target_type: data.target_type,
|
||||
target_id: data.target_id,
|
||||
}));
|
||||
bindReportModal(shell);
|
||||
bindReportModal(shell, data, categories);
|
||||
}
|
||||
|
||||
function initReportButtons() {
|
||||
if (document.documentElement.dataset.aksrtReportInitialized === '1') return;
|
||||
document.documentElement.dataset.aksrtReportInitialized = '1';
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-aksrt-report]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
var data = JSON.parse(btn.getAttribute('data-aksrt-report'));
|
||||
if (data && data.target_type && data.target_id) {
|
||||
openReportModal(data);
|
||||
}
|
||||
} catch {
|
||||
// invalid data attribute
|
||||
}
|
||||
document.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-aksrt-report]');
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
var data;
|
||||
try { data = JSON.parse(button.getAttribute('data-aksrt-report')); } catch (error) { return; }
|
||||
if (data && TARGETS[data.target_type] && Number(data.target_id) > 0) openReportModal(data, button);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -177,10 +177,9 @@ if (!function_exists('aksrt_get_comment_report_html')) :
|
||||
}
|
||||
|
||||
$report_payload = wp_json_encode([
|
||||
'target_type' => 'comment',
|
||||
'target_id' => (int) $comment->comment_ID,
|
||||
'target_name' => get_comment_author($comment),
|
||||
'target_avatar' => get_avatar_url((int) $comment->user_id > 0 ? (int) $comment->user_id : 0, ['size' => 40]) ?: '',
|
||||
'target_type' => 'comment',
|
||||
'target_id' => (int) $comment->comment_ID,
|
||||
'target_name' => get_comment_author($comment),
|
||||
]);
|
||||
|
||||
return '<li><a class="comment-report-link" href="javascript:;" data-aksrt-report="' . esc_attr((string) $report_payload) . '"><i class="fa fas fa-exclamation-triangle mr10 fa-fw c-red" aria-hidden="true"></i>' . esc_html__('举报', 'aksrt') . '</a></li>';
|
||||
|
||||
@@ -300,6 +300,13 @@ trait ScriptsPart1b {
|
||||
if (is_singular() || $is_profile_page) {
|
||||
$report_js_path = get_template_directory() . '/assets/js/report.js';
|
||||
if (file_exists($report_js_path)) {
|
||||
$report_css_path = get_template_directory() . '/assets/css/frontend/report.css';
|
||||
wp_enqueue_style(
|
||||
'aksrt-report',
|
||||
$base_uri . '/assets/css/frontend/report.css',
|
||||
[$core_dep],
|
||||
file_exists($report_css_path) ? (string) filemtime($report_css_path) : $version
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'aksrt-report',
|
||||
$base_uri . '/assets/js/report.js',
|
||||
|
||||
@@ -107,12 +107,10 @@ $show_cover_menu = $has_more_details || $show_cover_report || $cover_ban_html !=
|
||||
<?php if ($has_more_details) : ?><button type="button" role="menuitem" data-aksrt-profile-details><i class="fa fas fa-address-card" aria-hidden="true"></i><?php esc_html_e('更多资料', 'aksrt'); ?></button><?php endif; ?>
|
||||
<?php if ($show_cover_report) : ?>
|
||||
<?php
|
||||
// 统一举报契约:report.js 弹窗展示被举报对象(头像 + 名称)。
|
||||
$report_payload = [
|
||||
'target_type' => 'user',
|
||||
'target_id' => $user_id,
|
||||
'target_name' => $user->display_name,
|
||||
'target_avatar' => get_avatar_url($user_id, ['size' => 40]),
|
||||
'target_type' => 'user',
|
||||
'target_id' => $user_id,
|
||||
'target_name' => $user->display_name,
|
||||
];
|
||||
?>
|
||||
<?php if (is_user_logged_in()) : ?><button type="button" role="menuitem" data-aksrt-report="<?php echo esc_attr(wp_json_encode($report_payload)); ?>"><i class="fa fas fa-flag" aria-hidden="true"></i><?php esc_html_e('举报用户', 'aksrt'); ?></button>
|
||||
|
||||
@@ -391,35 +391,32 @@ test('native ban page uses the scoped template-and-dialog admin flow', () => {
|
||||
assert.match(banJs, /if \(!submitting\) dialog\.close\(\)/);
|
||||
assert.match(banCss, /html\.aksrt-ban-modal-open \{ overflow: hidden/);
|
||||
});
|
||||
test('report modal mirrors the reference theme: preset reasons, image evidence, notice, geometry', () => {
|
||||
test('report modal uses accessible reasons, automatic target association and optional evidence', () => {
|
||||
const scripts = read('src/Core/ScriptsPart1c.php')
|
||||
// 预置原因真实数据源是 ban_preset_reason(UserRuntime::ban_settings),
|
||||
// 不再读取 report_settings 中不存在的 categories 键(短路已修)。
|
||||
assert.match(scripts, /UserRuntime::ban_settings\(\)\['reasons'\]/)
|
||||
assert.ok(!scripts.includes("$report['categories']"), 'dead report.categories source must be removed')
|
||||
// 图片举证开关与举报说明随 aksrtData 注入。
|
||||
assert.match(scripts, /imgEnabled/)
|
||||
assert.match(scripts, /report_settings\(\)/)
|
||||
|
||||
const reportJs = read('assets/js/report.js')
|
||||
// Zibll 同构标记:edit-header 警示头、原因徽章 active 焦点色、其它原因、
|
||||
// 图片举证入口(后台开启时)、举报说明注入、but-average modal-buts 底栏。
|
||||
for (const part of ['edit-header', 'toggle-radius mr10 c-red', 'data-aksrt-report-reason', 'form-but-radio', '其它原因', '违规链接', '详细描述', '添加图片举证', 'but-average modal-buts']) {
|
||||
assert.ok(reportJs.includes(part), `report.js missing reference-theme part: ${part}`)
|
||||
for (const part of ['aksrt-report-target', 'name="report_reason"', 'type="radio"', '其他原因', '补充说明', '图片举证', '提交举报', '无需填写链接']) {
|
||||
assert.ok(reportJs.includes(part), `report.js missing form contract: ${part}`)
|
||||
}
|
||||
assert.doesNotMatch(reportJs, /report_url|违规链接|data-aksrt-report-reason=|bindAutoHeight/)
|
||||
for (const source of [reportJs, commentItem, authorHero]) assert.doesNotMatch(source, /target_avatar/)
|
||||
assert.match(reportJs, /description\) reason \+= '\\n详细描述:\\n' \+ description/)
|
||||
assert.match(reportJs, /report\/upload-image/)
|
||||
assert.ok(!reportJs.includes('is-active'), 'chips must toggle the reference .active class, not a custom one')
|
||||
|
||||
// 举报图片上传端点存在,复用评论上传回调、独立权限(登录 + 举报 + 图片开关)。
|
||||
assert.match(reportJs, /window\.AksrtModalRuntime\.getActive\(\) === shell/)
|
||||
assert.match(userController, /'\/report\/upload-image'/)
|
||||
|
||||
// 几何 1:1:桌面端 600px(参考主题 Bootstrap 默认模态宽)。
|
||||
const css = read('assets/css/frontend/modal-runtime.css')
|
||||
const css = read('assets/css/frontend/report.css')
|
||||
assert.match(css, /\.aksrt-report-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
|
||||
assert.match(css, /600px/)
|
||||
// 加号图标单一来源:main.css 的 .preview .add:before/:after(inset:0 + margin:auto),
|
||||
// modal-runtime 不得重复定义(否则过约束冲突,加号偏移只剩“L”形)。
|
||||
assert.ok(!/form-upload \.preview \.add::?(before|after)/.test(css), 'duplicated add plus rules must stay out of modal-runtime.css')
|
||||
assert.match(css, /:focus-visible/)
|
||||
assert.match(css, /safe-area-inset-bottom/)
|
||||
assert.match(css, /\[hidden\] \{ display: none !important;/)
|
||||
assert.doesNotMatch(modalRuntimeCss, /aksrt-report-modal/, 'retired report styles must not remain in the shared stylesheet')
|
||||
assert.match(scriptsPart1b, /assets\/css\/frontend\/report\.css/)
|
||||
})
|
||||
|
||||
test('comments expose a report entry for other users when reporting is enabled', () => {
|
||||
@@ -525,15 +522,13 @@ test('suspend (小黑屋) enforcement blocks publishing end to end', () => {
|
||||
assert.match(restriction, /您已被关入小黑屋,暂时无法评论/)
|
||||
})
|
||||
|
||||
test('report modal close icon and textarea autoHeight match the reference theme', () => {
|
||||
test('report modal delegates its labelled title, close button and focus return to the shared runtime', () => {
|
||||
const reportJs = read('assets/js/report.js')
|
||||
// 关闭按钮走 SVG 雪碧图 icon-close(与参考主题 .ic-close 同构,fa-times 退役)。
|
||||
assert.match(reportJs, /<svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"><\/use><\/svg>/)
|
||||
assert.ok(!reportJs.includes('fa-times'), 'close icon must use the SVG sprite, not FontAwesome')
|
||||
// textarea 自动增高(参考主题 autoHeight 行为),隐藏中的“其它原因”跳过首次计算。
|
||||
assert.match(reportJs, /function bindAutoHeight\(/)
|
||||
assert.match(reportJs, /querySelectorAll\('textarea\.form-control'\)\.forEach\(bindAutoHeight\)/)
|
||||
assert.match(reportJs, /offsetHeight > 0\) grow\(\)/)
|
||||
assert.match(reportJs, /title: TARGETS\[data\.target_type\]\.title/)
|
||||
assert.match(reportJs, /mobileBottom: true, focusReturn: opener/)
|
||||
assert.match(reportJs, /<label class="aksrt-report-label" for=/)
|
||||
assert.match(reportJs, /data-aksrt-report-reason-error role="alert"/)
|
||||
assert.doesNotMatch(reportJs, /class="close"|style="display:|setTimeout/)
|
||||
})
|
||||
|
||||
test('wechat template messages cover report and account restriction types', () => {
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const test = require('node:test');
|
||||
const { MiniDocument, MiniElement } = require('../helpers/mini-dom.cjs');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
|
||||
class ReportElement extends MiniElement {
|
||||
constructor(tagName) {
|
||||
super(tagName);
|
||||
this._value = null;
|
||||
this.files = [];
|
||||
}
|
||||
|
||||
get value() { return this._value ?? this.getAttribute('value') ?? ''; }
|
||||
set value(value) { this._value = String(value ?? ''); }
|
||||
get name() { return this.getAttribute('name') || ''; }
|
||||
get type() { return this.getAttribute('type') || ''; }
|
||||
get checked() { return this.hasAttribute('checked'); }
|
||||
set checked(value) { value ? this.setAttribute('checked', '') : this.removeAttribute('checked'); }
|
||||
get disabled() { return this.hasAttribute('disabled'); }
|
||||
set disabled(value) { value ? this.setAttribute('disabled', '') : this.removeAttribute('disabled'); }
|
||||
get hidden() { return this.hasAttribute('hidden'); }
|
||||
set hidden(value) { value ? this.setAttribute('hidden', '') : this.removeAttribute('hidden'); }
|
||||
|
||||
focus() { this.ownerDocument.activeElement = this; }
|
||||
click() { this.dispatch('click'); }
|
||||
|
||||
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
|
||||
|
||||
querySelectorAll(selector) {
|
||||
const groups = String(selector).split(',');
|
||||
return [...new Set(groups.flatMap((group) => {
|
||||
const checked = /:checked\b/.test(group);
|
||||
const disabled = /:disabled\b/.test(group);
|
||||
const plainSelector = group.replace(/:(?:checked|disabled)\b/g, '').trim();
|
||||
return super.querySelectorAll(plainSelector).filter((element) =>
|
||||
(!checked || element.checked) && (!disabled || element.disabled));
|
||||
}))];
|
||||
}
|
||||
}
|
||||
|
||||
class ReportDocument extends MiniDocument {
|
||||
constructor() {
|
||||
super();
|
||||
this.documentElement = this;
|
||||
this.activeElement = null;
|
||||
}
|
||||
|
||||
createElement(tagName) {
|
||||
const element = new ReportElement(tagName);
|
||||
element.ownerDocument = this;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
class MiniFormData {
|
||||
constructor() { this.values = []; }
|
||||
append(name, value) { this.values.push([name, value]); }
|
||||
get(name) { return this.values.find(([key]) => key === name)?.[1] ?? null; }
|
||||
}
|
||||
|
||||
function createRuntime({ imgEnabled = true, categories = [
|
||||
{ slug: 'spam', name: '垃圾信息' },
|
||||
{ slug: 'abuse', name: '人身攻击' },
|
||||
] } = {}) {
|
||||
const document = new ReportDocument();
|
||||
const modals = [];
|
||||
const fetchCalls = [];
|
||||
const notyfCalls = [];
|
||||
const closeCalls = [];
|
||||
let activeModal = null;
|
||||
let autoInit = null;
|
||||
|
||||
function fetch(url, options = {}) {
|
||||
let resolveRequest;
|
||||
let rejectRequest;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
resolveRequest = resolve;
|
||||
rejectRequest = reject;
|
||||
});
|
||||
const call = {
|
||||
url: String(url),
|
||||
options,
|
||||
resolve(data) {
|
||||
resolveRequest({ json: () => Promise.resolve(data) });
|
||||
},
|
||||
reject(error) { rejectRequest(error); },
|
||||
};
|
||||
fetchCalls.push(call);
|
||||
if (options.method === 'DELETE') call.resolve({ success: true });
|
||||
return promise;
|
||||
}
|
||||
|
||||
const modalRuntime = {
|
||||
open(options) {
|
||||
const shell = document.createElement('div');
|
||||
shell.className = options.className;
|
||||
shell.innerHTML = options.html;
|
||||
document.body.appendChild(shell);
|
||||
activeModal = shell;
|
||||
modals.push({ shell, options });
|
||||
return shell;
|
||||
},
|
||||
getActive() { return activeModal; },
|
||||
close(reason, shell = activeModal) {
|
||||
closeCalls.push({ reason, shell });
|
||||
if (!shell) return;
|
||||
if (activeModal === shell) activeModal = null;
|
||||
shell.remove();
|
||||
document.dispatch('aksrt:modal:close', { detail: { shell } });
|
||||
},
|
||||
};
|
||||
|
||||
const window = {
|
||||
aksrtData: {
|
||||
apiUrl: '/wp-json/aksrt/v1',
|
||||
restNonce: 'test-nonce',
|
||||
reportCategories: categories,
|
||||
report: { imgEnabled },
|
||||
},
|
||||
AksrtModalRuntime: modalRuntime,
|
||||
fetch,
|
||||
};
|
||||
const jquery = () => ({
|
||||
off() { return this; },
|
||||
on(event, callback) {
|
||||
if (event === 'auto_fun.aksrt-report') autoInit = callback;
|
||||
return this;
|
||||
},
|
||||
});
|
||||
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root, 'assets/js/report.js'), 'utf8'), {
|
||||
window,
|
||||
document,
|
||||
jQuery: jquery,
|
||||
FormData: MiniFormData,
|
||||
Promise,
|
||||
console,
|
||||
notyf: (...args) => notyfCalls.push(args),
|
||||
});
|
||||
assert.equal(typeof autoInit, 'function');
|
||||
autoInit();
|
||||
|
||||
return {
|
||||
document,
|
||||
window,
|
||||
modalRuntime,
|
||||
modals,
|
||||
fetchCalls,
|
||||
notyfCalls,
|
||||
closeCalls,
|
||||
get activeModal() { return activeModal; },
|
||||
openReport(target = { target_type: 'post', target_id: 42, target_name: 'Article' }) {
|
||||
const opener = document.createElement('button');
|
||||
opener.setAttribute('data-aksrt-report', JSON.stringify(target));
|
||||
document.body.appendChild(opener);
|
||||
const event = opener.dispatch('click');
|
||||
return { ...modals.at(-1), opener, event };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function selectReason(modal, value) {
|
||||
const radios = modal.shell.querySelectorAll('[name="report_reason"]');
|
||||
const selected = radios.find((radio) => radio.value === String(value));
|
||||
assert.ok(selected, `missing reason ${value}`);
|
||||
radios.forEach((radio) => { radio.checked = radio === selected; });
|
||||
selected.dispatch('change');
|
||||
return selected;
|
||||
}
|
||||
|
||||
function submit(modal) {
|
||||
return modal.shell.querySelector('.aksrt-report-form').dispatch('submit');
|
||||
}
|
||||
|
||||
function callsFor(runtime, endpoint) {
|
||||
return runtime.fetchCalls.filter((call) => call.url.includes(endpoint));
|
||||
}
|
||||
|
||||
function payloadFor(call) {
|
||||
assert.equal(typeof call.options.body, 'string');
|
||||
return JSON.parse(call.options.body);
|
||||
}
|
||||
|
||||
async function flushPromises() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
test('opens from an article button, associates the target, omits URL input, and escapes rendered names', () => {
|
||||
const runtime = createRuntime({ categories: [{ slug: 'unsafe', name: '<svg onload="run()">reason</svg>' }] });
|
||||
const targetName = '<img src=x onerror="run()"> &';
|
||||
const modal = runtime.openReport({ target_type: 'post', target_id: 73, target_name: targetName });
|
||||
const target = modal.shell.querySelector('.aksrt-report-target__name');
|
||||
const reason = modal.shell.querySelector('.aksrt-report-reason span');
|
||||
|
||||
assert.equal(modal.event.defaultPrevented, true);
|
||||
assert.equal(modal.options.title, '举报文章');
|
||||
assert.equal(modal.options.focusReturn, modal.opener);
|
||||
assert.equal(runtime.activeModal, modal.shell);
|
||||
assert.ok(target.textContent.includes('<img'));
|
||||
assert.equal(target.querySelector('img'), null);
|
||||
assert.equal(reason.querySelector('svg'), null);
|
||||
assert.equal(modal.shell.querySelector('[name="report_url"]'), null);
|
||||
assert.equal(modal.shell.querySelector('input[type="url"]'), null);
|
||||
assert.ok(modal.shell.querySelector('.aksrt-report-target'));
|
||||
});
|
||||
|
||||
test('requires a radio reason and other text, then sends canonical fields with description', async () => {
|
||||
const runtime = createRuntime();
|
||||
const modal = runtime.openReport();
|
||||
const form = modal.shell.querySelector('.aksrt-report-form');
|
||||
const reasonError = form.querySelector('[data-aksrt-report-reason-error]');
|
||||
const otherBox = form.querySelector('.aksrt-report-other');
|
||||
const otherInput = form.querySelector('[name="reason_other"]');
|
||||
const description = form.querySelector('[name="report_desc"]');
|
||||
|
||||
submit(modal);
|
||||
assert.equal(reasonError.hidden, false);
|
||||
assert.equal(reasonError.textContent, '请选择一项举报原因');
|
||||
assert.equal(runtime.document.activeElement, form.querySelector('[name="report_reason"]'));
|
||||
assert.equal(callsFor(runtime, '/report/submit').length, 0);
|
||||
|
||||
selectReason(modal, 'other');
|
||||
assert.equal(otherBox.hidden, false);
|
||||
assert.equal(otherInput.disabled, false);
|
||||
submit(modal);
|
||||
assert.equal(reasonError.textContent, '请填写举报原因');
|
||||
assert.equal(otherInput.getAttribute('aria-invalid'), 'true');
|
||||
assert.equal(runtime.document.activeElement, otherInput);
|
||||
assert.equal(callsFor(runtime, '/report/submit').length, 0);
|
||||
|
||||
otherInput.value = '自定义原因';
|
||||
otherInput.dispatch('input');
|
||||
description.value = '补充背景';
|
||||
description.dispatch('input');
|
||||
submit(modal);
|
||||
const call = callsFor(runtime, '/report/submit')[0];
|
||||
const payload = payloadFor(call);
|
||||
|
||||
assert.deepEqual(Object.keys(payload).sort(), ['category', 'reason', 'target_id', 'target_type']);
|
||||
assert.deepEqual(payload, {
|
||||
target_type: 'post',
|
||||
target_id: 42,
|
||||
category: '',
|
||||
reason: '自定义原因\n详细描述:\n补充背景',
|
||||
});
|
||||
call.resolve({ success: true, message: '已提交' });
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
test('preset reason ignores hidden custom text while including description in reason', () => {
|
||||
const runtime = createRuntime();
|
||||
const modal = runtime.openReport();
|
||||
const otherInput = modal.shell.querySelector('[name="reason_other"]');
|
||||
const description = modal.shell.querySelector('[name="report_desc"]');
|
||||
otherInput.value = 'hidden custom text';
|
||||
description.value = 'More context';
|
||||
selectReason(modal, 1);
|
||||
|
||||
assert.equal(modal.shell.querySelector('.aksrt-report-other').hidden, true);
|
||||
assert.equal(otherInput.disabled, true);
|
||||
submit(modal);
|
||||
const payload = payloadFor(callsFor(runtime, '/report/submit')[0]);
|
||||
|
||||
assert.deepEqual(payload, {
|
||||
target_type: 'post',
|
||||
target_id: 42,
|
||||
category: 'abuse',
|
||||
reason: '人身攻击\n详细描述:\nMore context',
|
||||
});
|
||||
});
|
||||
|
||||
test('omits all image controls when image evidence is disabled', () => {
|
||||
const runtime = createRuntime({ imgEnabled: false });
|
||||
const modal = runtime.openReport();
|
||||
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-file]'), null);
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-add]'), null);
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-images]'), null);
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]'), null);
|
||||
});
|
||||
|
||||
test('counts in-flight images once, accepts exactly three, and blocks submit until ready', async () => {
|
||||
const runtime = createRuntime();
|
||||
const modal = runtime.openReport();
|
||||
selectReason(modal, 0);
|
||||
const fileInput = modal.shell.querySelector('[data-aksrt-report-file]');
|
||||
fileInput.files = [1, 2, 3].map((index) => ({ name: `evidence-${index}.png`, type: 'image/png' }));
|
||||
fileInput.dispatch('change');
|
||||
|
||||
assert.equal(callsFor(runtime, '/report/upload-image').length, 3);
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]').textContent, '3 / 3');
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-submit]').disabled, true);
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-status]').textContent, '图片正在上传,完成后即可提交');
|
||||
submit(modal);
|
||||
assert.equal(callsFor(runtime, '/report/submit').length, 0);
|
||||
|
||||
callsFor(runtime, '/report/upload-image').forEach((call, index) => call.resolve({
|
||||
success: true,
|
||||
data: { id: 800 + index, url: `https://evidence.test/${index + 1}.png` },
|
||||
}));
|
||||
await flushPromises();
|
||||
assert.equal(modal.shell.querySelectorAll('.aksrt-report-image').length, 3);
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]').textContent, '3 / 3');
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-add]').disabled, true);
|
||||
|
||||
fileInput.files = [{ name: 'fourth.png', type: 'image/png' }];
|
||||
fileInput.dispatch('change');
|
||||
assert.equal(callsFor(runtime, '/report/upload-image').length, 3);
|
||||
|
||||
submit(modal);
|
||||
const call = callsFor(runtime, '/report/submit')[0];
|
||||
const payload = payloadFor(call);
|
||||
for (const index of [1, 2, 3]) assert.ok(payload.reason.includes(`https://evidence.test/${index}.png`));
|
||||
call.resolve({ success: true });
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
test('retries a failed upload and deletes its pending attachment when removed', async () => {
|
||||
const runtime = createRuntime();
|
||||
const modal = runtime.openReport();
|
||||
const fileInput = modal.shell.querySelector('[data-aksrt-report-file]');
|
||||
fileInput.files = [{ name: 'retry.png', type: 'image/png' }];
|
||||
fileInput.dispatch('change');
|
||||
|
||||
callsFor(runtime, '/report/upload-image')[0].resolve({ success: false, message: 'Upload failed' });
|
||||
await flushPromises();
|
||||
const image = modal.shell.querySelector('.aksrt-report-image');
|
||||
assert.equal(image.getAttribute('data-upload-state'), 'error');
|
||||
assert.match(modal.shell.querySelector('[data-aksrt-report-error]').textContent, /Upload failed/);
|
||||
|
||||
image.querySelector('[data-aksrt-report-retry]').dispatch('click');
|
||||
const retry = callsFor(runtime, '/report/upload-image')[1];
|
||||
assert.ok(retry);
|
||||
retry.resolve({ success: true, data: { id: 901, url: 'https://evidence.test/retry.png' } });
|
||||
await flushPromises();
|
||||
assert.equal(image.getAttribute('data-upload-state'), 'ready');
|
||||
|
||||
image.querySelector('[data-aksrt-report-remove]').dispatch('click');
|
||||
const deletion = callsFor(runtime, '/upload/pending/901')[0];
|
||||
assert.ok(deletion);
|
||||
assert.equal(deletion.options.method, 'DELETE');
|
||||
assert.equal(modal.shell.querySelectorAll('.aksrt-report-image').length, 0);
|
||||
assert.equal(modal.shell.querySelector('[data-aksrt-report-image-count]').textContent, '0 / 3');
|
||||
});
|
||||
|
||||
test('server failure preserves values, reenables controls, and allows a retry', async () => {
|
||||
const runtime = createRuntime();
|
||||
const modal = runtime.openReport();
|
||||
selectReason(modal, 'other');
|
||||
const form = modal.shell.querySelector('.aksrt-report-form');
|
||||
const otherInput = form.querySelector('[name="reason_other"]');
|
||||
const description = form.querySelector('[name="report_desc"]');
|
||||
const submitButton = form.querySelector('[data-aksrt-report-submit]');
|
||||
otherInput.value = 'A custom report reason';
|
||||
description.value = 'Additional context';
|
||||
|
||||
submit(modal);
|
||||
const failedRequest = callsFor(runtime, '/report/submit')[0];
|
||||
assert.equal(submitButton.disabled, true);
|
||||
assert.equal(otherInput.disabled, true);
|
||||
failedRequest.resolve({ success: false, message: 'Server rejected report' });
|
||||
await flushPromises();
|
||||
|
||||
assert.equal(otherInput.value, 'A custom report reason');
|
||||
assert.equal(description.value, 'Additional context');
|
||||
assert.equal(form.querySelector('[name="report_reason"]:checked').value, 'other');
|
||||
assert.equal(otherInput.disabled, false);
|
||||
assert.equal(description.disabled, false);
|
||||
assert.equal(submitButton.disabled, false);
|
||||
assert.equal(form.querySelector('[data-aksrt-report-error]').textContent, 'Server rejected report');
|
||||
|
||||
submit(modal);
|
||||
const retry = callsFor(runtime, '/report/submit')[1];
|
||||
assert.equal(payloadFor(retry).reason, 'A custom report reason\n详细描述:\nAdditional context');
|
||||
retry.resolve({ success: true });
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
test('blocks duplicate submits and closing an old submitting dialog cannot close the new one', async () => {
|
||||
const runtime = createRuntime();
|
||||
const oldModal = runtime.openReport();
|
||||
selectReason(oldModal, 0);
|
||||
submit(oldModal);
|
||||
submit(oldModal);
|
||||
assert.equal(callsFor(runtime, '/report/submit').length, 1);
|
||||
|
||||
const newModal = runtime.openReport({ target_type: 'comment', target_id: 88, target_name: 'Comment' });
|
||||
runtime.modalRuntime.close('dismiss', oldModal.shell);
|
||||
callsFor(runtime, '/report/submit')[0].resolve({ success: true });
|
||||
await flushPromises();
|
||||
|
||||
assert.equal(runtime.activeModal, newModal.shell);
|
||||
assert.equal(runtime.closeCalls.length, 1);
|
||||
assert.equal(runtime.closeCalls[0].shell, oldModal.shell);
|
||||
});
|
||||
|
||||
test('cleans pending attachments on close and when an upload completes after close', async () => {
|
||||
const runtime = createRuntime();
|
||||
const readyModal = runtime.openReport();
|
||||
const readyFile = readyModal.shell.querySelector('[data-aksrt-report-file]');
|
||||
readyFile.files = [{ name: 'ready.png', type: 'image/png' }];
|
||||
readyFile.dispatch('change');
|
||||
callsFor(runtime, '/report/upload-image')[0].resolve({
|
||||
success: true,
|
||||
data: { id: 1001, url: 'https://evidence.test/ready.png' },
|
||||
});
|
||||
await flushPromises();
|
||||
runtime.modalRuntime.close('cancel', readyModal.shell);
|
||||
|
||||
const readyDeletion = callsFor(runtime, '/upload/pending/1001')[0];
|
||||
assert.ok(readyDeletion);
|
||||
assert.equal(readyDeletion.options.method, 'DELETE');
|
||||
|
||||
const pendingModal = runtime.openReport();
|
||||
const pendingFile = pendingModal.shell.querySelector('[data-aksrt-report-file]');
|
||||
pendingFile.files = [{ name: 'pending.png', type: 'image/png' }];
|
||||
pendingFile.dispatch('change');
|
||||
const pendingUpload = callsFor(runtime, '/report/upload-image')[1];
|
||||
runtime.modalRuntime.close('cancel', pendingModal.shell);
|
||||
assert.equal(callsFor(runtime, '/upload/pending/1002').length, 0);
|
||||
|
||||
pendingUpload.resolve({
|
||||
success: true,
|
||||
data: { id: 1002, url: 'https://evidence.test/pending.png' },
|
||||
});
|
||||
await flushPromises();
|
||||
const lateDeletion = callsFor(runtime, '/upload/pending/1002')[0];
|
||||
assert.ok(lateDeletion);
|
||||
assert.equal(lateDeletion.options.method, 'DELETE');
|
||||
});
|
||||
|
||||
test('keeps a live child modal open and marks the finished report after success', async () => {
|
||||
const runtime = createRuntime();
|
||||
const report = runtime.openReport();
|
||||
selectReason(report, 0);
|
||||
submit(report);
|
||||
const request = callsFor(runtime, '/report/submit')[0];
|
||||
const child = runtime.modalRuntime.open({ className: 'aksrt-child-modal', html: '<p>Child dialog</p>' });
|
||||
request.resolve({ success: true });
|
||||
await flushPromises();
|
||||
|
||||
const form = report.shell.querySelector('.aksrt-report-form');
|
||||
assert.equal(runtime.activeModal, child);
|
||||
assert.equal(runtime.closeCalls.length, 0);
|
||||
assert.equal(form.getAttribute('aria-busy'), 'false');
|
||||
assert.equal(form.querySelector('[data-aksrt-report-submit]').textContent, '举报已提交');
|
||||
assert.ok(report.shell.querySelector('[data-aksrt-report-status]'));
|
||||
});
|
||||
|
||||
test('retains claimed evidence on post-close success and deletes it on post-close failure', async () => {
|
||||
const runtime = createRuntime();
|
||||
async function submitClosedReport(id, result) {
|
||||
const modal = runtime.openReport();
|
||||
selectReason(modal, 0);
|
||||
const fileInput = modal.shell.querySelector('[data-aksrt-report-file]');
|
||||
fileInput.files = [{ name: `${id}.png`, type: 'image/png' }];
|
||||
fileInput.dispatch('change');
|
||||
callsFor(runtime, '/report/upload-image').at(-1).resolve({
|
||||
success: true,
|
||||
data: { id, url: `https://evidence.test/${id}.png` },
|
||||
});
|
||||
await flushPromises();
|
||||
submit(modal);
|
||||
const request = callsFor(runtime, '/report/submit').at(-1);
|
||||
runtime.modalRuntime.close('dismiss', modal.shell);
|
||||
request.resolve(result);
|
||||
await flushPromises();
|
||||
}
|
||||
|
||||
await submitClosedReport(1101, { success: true });
|
||||
assert.equal(callsFor(runtime, '/upload/pending/1101').length, 0);
|
||||
await submitClosedReport(1102, { success: false, message: 'Rejected' });
|
||||
const deletion = callsFor(runtime, '/upload/pending/1102')[0];
|
||||
assert.ok(deletion);
|
||||
assert.equal(deletion.options.method, 'DELETE');
|
||||
});
|
||||
Reference in new issue
Block a user