fix(ticket): rework detail modal layout and restore urge

- Detail modal: fixed-height dialog, thread is the only (thin) scroll area
- Collapsible pinned summary bar (collapsed by default, state kept across re-render)
- Composer: toolbar (image / urge / close) above input, pending image tray with remove
- Paste screenshots to upload; create form uses the same button + tray
- Urge/close now POST (routes are CREATABLE); urge cooldown countdown via urge_wait
- Notify admins on urge when ticket is unassigned
- Split composer/form styles into tickets-compose.css; drop dead uploader code

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-27 21:37:03 +08:00
1 parent b74381cfa2
commit 13bde11dda
7 files changed
+713 -392

No files matched your search

+398
View File
@@ -0,0 +1,398 @@
/* User center ticket (工单) — 回复框 / 新建表单 / 图片待发送区 / 提交成功
*
* 依赖 tickets.css 定义的 --tickets-* token(同面板同条件入队,先于本文件)。
* 回复框布局自上而下:工具栏(图片 | 催单 · 关闭)→ 引用条 → 待发送缩略图 →
* 输入框 → 发送行;作为详情弹窗 flex 列的最后一段固定在底部,不参与滚动。
*/
/* ------------------------------------------------------------------ *
* 已关闭提示(占据回复框位置)
* ------------------------------------------------------------------ */
.aksrt-ticket-detail__closed {
display: flex;
flex: 0 0 auto;
gap: 8px;
align-items: center;
justify-content: center;
margin: 0;
padding: 14px 18px 16px;
border-top: 1px solid var(--tickets-line);
color: var(--tickets-muted);
font-size: 13px;
}
/* ------------------------------------------------------------------ *
* 回复框
* ------------------------------------------------------------------ */
.aksrt-ticket-composer {
display: grid;
flex: 0 0 auto;
gap: 8px;
padding: 10px 18px 14px;
border-top: 1px solid var(--tickets-line);
background: var(--surface-1, #fff);
}
.aksrt-ticket-composer__toolbar {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
justify-content: space-between;
}
.aksrt-ticket-composer__tools,
.aksrt-ticket-composer__actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.aksrt-ticket-composer__quote {
display: flex;
gap: 8px;
align-items: center;
padding: 7px 10px;
border-radius: 8px;
background: color-mix(in srgb, var(--tickets-primary) 8%, transparent);
color: var(--tickets-ink-2);
font-size: 12px;
}
.aksrt-ticket-composer__quote[hidden] { display: none; }
.aksrt-ticket-composer__quote > .fa { color: var(--tickets-primary); }
.aksrt-ticket-composer__quote > span { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.aksrt-ticket-composer__quote button {
flex: 0 0 auto;
padding: 2px 4px;
border: 0;
background: transparent;
color: var(--tickets-muted);
cursor: pointer;
}
.aksrt-ticket-composer__input textarea {
display: block;
width: 100%;
min-height: 64px;
max-height: 160px;
padding: 9px 12px;
border: 1px solid var(--tickets-line);
border-radius: 10px;
background: var(--surface-1, #fff);
color: inherit;
font-family: inherit;
font-size: 13px;
line-height: 1.6;
resize: none;
scrollbar-width: thin;
transition: border-color .15s ease, box-shadow .15s ease;
}
.aksrt-ticket-composer__input textarea:focus {
border-color: var(--tickets-primary);
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--tickets-primary) 16%, transparent);
}
.aksrt-ticket-composer__input textarea[aria-invalid="true"] { border-color: var(--tickets-danger); }
.aksrt-ticket-composer__input .aksrt-ticket-field__error { margin-top: 4px; }
.aksrt-ticket-composer__bar { display: flex; gap: 10px; align-items: center; justify-content: flex-end; }
.aksrt-ticket-composer__hint { margin-right: auto; color: var(--tickets-muted); font-size: 11px; }
.aksrt-ticket-composer__bar .btn-primary { min-width: 88px; }
/* 工具按钮:图片为幽灵按钮;催单 / 关闭为描边胶囊,与输入区视觉分层。 */
.aksrt-ticket-tool {
display: inline-flex;
align-items: center;
gap: 5px;
height: 30px;
padding: 0 10px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: var(--tickets-muted);
font-size: 12px;
white-space: nowrap;
cursor: pointer;
transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.aksrt-ticket-tool:hover:not(:disabled) { background: var(--tickets-soft); color: var(--tickets-ink); }
.aksrt-ticket-tool:focus-visible { outline: 2px solid var(--tickets-primary); outline-offset: 1px; }
.aksrt-ticket-tool:disabled { opacity: .5; cursor: not-allowed; }
.aksrt-ticket-tool__count {
color: var(--tickets-muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.aksrt-ticket-tool__count.is-zero { display: none; }
.aksrt-ticket-tool.is-chip {
height: 28px;
border-color: var(--tickets-line);
border-radius: 999px;
}
.aksrt-ticket-tool.is-chip:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--tickets-primary) 40%, transparent);
background: color-mix(in srgb, var(--tickets-primary) 7%, transparent);
color: var(--tickets-primary);
}
.aksrt-ticket-tool.is-danger:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--tickets-danger) 40%, transparent);
background: color-mix(in srgb, var(--tickets-danger) 7%, transparent);
color: var(--tickets-danger);
}
.aksrt-ticket-tool.is-confirming,
.aksrt-ticket-tool.is-confirming:hover:not(:disabled) {
border-color: var(--tickets-danger);
background: color-mix(in srgb, var(--tickets-danger) 12%, transparent);
color: var(--tickets-danger);
}
/* 催单冷却:保持可读的倒计时,而不是灰掉不解释。 */
.aksrt-ticket-tool.is-cooling:disabled {
opacity: 1;
border-style: dashed;
color: var(--tickets-muted);
font-variant-numeric: tabular-nums;
cursor: default;
}
/* ------------------------------------------------------------------ *
* 新建表单
* ------------------------------------------------------------------ */
.aksrt-ticket-form { display: grid; gap: 16px; text-align: left; }
.aksrt-ticket-field {
position: relative;
display: grid;
gap: 6px;
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
.aksrt-ticket-field > label,
.aksrt-ticket-field > legend,
.aksrt-ticket-field__label {
display: flex;
gap: 6px;
align-items: center;
padding: 0;
color: var(--tickets-ink-2);
font-size: 13px;
font-weight: 600;
}
.aksrt-ticket-field em { color: var(--tickets-danger); font-style: normal; }
.aksrt-ticket-field__optional { color: var(--tickets-muted); font-size: 12px; font-weight: 400; }
.aksrt-ticket-field input[type="text"],
.aksrt-ticket-field select,
.aksrt-ticket-field textarea {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--tickets-line);
border-radius: 9px;
background: var(--surface-1, #fff);
color: inherit;
font-family: inherit;
font-size: 13px;
line-height: 1.6;
transition: border-color .15s ease, box-shadow .15s ease;
}
.aksrt-ticket-field textarea { min-height: 120px; padding-bottom: 24px; resize: vertical; }
.aksrt-ticket-field input[type="text"]:focus,
.aksrt-ticket-field select:focus,
.aksrt-ticket-field textarea:focus {
border-color: var(--tickets-primary);
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--tickets-primary) 16%, transparent);
}
.aksrt-ticket-field [aria-invalid="true"],
.aksrt-ticket-field[aria-invalid="true"] .aksrt-ticket-type span {
border-color: var(--tickets-danger) !important;
}
.aksrt-ticket-field__error { margin: 0; color: var(--tickets-danger); font-size: 12px; }
.aksrt-ticket-field__hint { margin: 0; color: var(--tickets-muted); font-size: 12px; }
.aksrt-ticket-field__row { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
/* 新建表单的「添加图片」用描边按钮,和其他表单控件同一视觉重量。 */
.aksrt-ticket-field__row .aksrt-ticket-tool { margin-left: -1px; border-color: var(--tickets-line); }
.aksrt-ticket-counter { color: var(--tickets-muted); font-size: 11px; font-variant-numeric: tabular-nums; }
.aksrt-ticket-counter.is-near { color: var(--color-warning, #f59e0b); }
/* 新建表单内计数器贴在输入框右下角;回复框的计数器在发送行里。 */
.aksrt-ticket-form .aksrt-ticket-field > .aksrt-ticket-counter {
position: absolute;
right: 10px;
bottom: 7px;
pointer-events: none;
}
.aksrt-ticket-form .aksrt-ticket-field:has(.aksrt-ticket-field__error) > .aksrt-ticket-counter { bottom: 27px; }
.aksrt-ticket-form .aksrt-ticket-field input[type="text"] { padding-right: 72px; }
/* 部门:单选卡片(替代下拉,选项一目了然)。 */
.aksrt-ticket-types__list { display: flex; flex-wrap: wrap; gap: 8px; }
.aksrt-ticket-type { position: relative; margin: 0; cursor: pointer; }
.aksrt-ticket-type input { position: absolute; opacity: 0; pointer-events: none; }
.aksrt-ticket-type span {
display: inline-flex;
align-items: center;
height: 34px;
padding: 0 14px;
border: 1px solid var(--tickets-line);
border-radius: 999px;
background: var(--surface-1, #fff);
font-size: 13px;
transition: border-color .15s ease, background-color .15s ease, color .15s ease;
}
.aksrt-ticket-type:hover span { border-color: color-mix(in srgb, var(--tickets-primary) 45%, transparent); }
.aksrt-ticket-type input:checked + span {
border-color: var(--tickets-primary);
background: color-mix(in srgb, var(--tickets-primary) 10%, transparent);
color: var(--tickets-primary);
font-weight: 600;
}
.aksrt-ticket-type input:focus-visible + span { outline: 2px solid var(--tickets-primary); outline-offset: 2px; }
.aksrt-ticket-form__actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 4px; }
.aksrt-ticket-form__actions > button { min-width: 96px; }
/* ------------------------------------------------------------------ *
* 待发送图片条(回复框与新建表单共用)
* ------------------------------------------------------------------ */
.aksrt-ticket-tray {
display: flex;
gap: 8px;
padding: 2px 0;
overflow-x: auto;
scrollbar-width: thin;
}
.aksrt-ticket-tray:empty { display: none; }
.aksrt-ticket-image {
position: relative;
flex: 0 0 auto;
width: 64px;
height: 64px;
border: 1px solid var(--tickets-line);
border-radius: 9px;
overflow: hidden;
background: var(--tickets-soft);
animation: aksrt-ticket-pop .18s ease-out;
}
.aksrt-ticket-image img { width: 100%; height: 100%; object-fit: cover; }
.aksrt-ticket-image.is-uploading { display: grid; place-items: center; }
.aksrt-ticket-image__spinner {
width: 20px;
height: 20px;
border: 2px solid var(--tickets-line);
border-top-color: var(--tickets-primary);
border-radius: 50%;
animation: aksrt-ticket-spin .8s linear infinite;
}
.aksrt-ticket-image__remove {
position: absolute;
top: 3px;
right: 3px;
display: grid;
place-items: center;
width: 20px;
height: 20px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgb(0 0 0 / 55%);
color: #fff;
font-size: 10px;
cursor: pointer;
transition: background-color .15s ease;
}
.aksrt-ticket-image__remove:hover { background: rgb(0 0 0 / 75%); }
.aksrt-ticket-image__remove:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
@keyframes aksrt-ticket-spin { to { transform: rotate(360deg); } }
@keyframes aksrt-ticket-pop { from { opacity: 0; transform: scale(.9); } }
/* ------------------------------------------------------------------ *
* 提交成功
* ------------------------------------------------------------------ */
.aksrt-ticket-done { display: grid; gap: 8px; justify-items: center; padding: 12px 4px 4px; text-align: center; }
.aksrt-ticket-done__icon {
display: inline-grid;
place-items: center;
width: 56px;
height: 56px;
margin-bottom: 4px;
border-radius: 50%;
background: color-mix(in srgb, var(--color-success, #10b981) 14%, transparent);
color: var(--color-success, #10b981);
font-size: 24px;
}
.aksrt-ticket-done h4 { margin: 0; font-size: 17px; }
.aksrt-ticket-done > p { margin: 0; color: var(--tickets-muted); font-size: 13px; }
.aksrt-ticket-done__notice {
width: 100%;
margin-top: 6px;
padding: 12px 14px;
border-radius: 10px;
background: var(--tickets-soft);
color: var(--tickets-ink-2);
font-size: 13px;
line-height: 1.7;
text-align: left;
}
.aksrt-ticket-done__notice > :last-child { margin-bottom: 0; }
.aksrt-ticket-done .aksrt-ticket-form__actions { justify-content: center; width: 100%; margin-top: 10px; }
/* 按钮忙碌态(发送 / 提交 / 催单中)。 */
.aksrt-ticket-modal .is-busy { opacity: .7; cursor: progress; }
/* ------------------------------------------------------------------ *
* 窄屏
* ------------------------------------------------------------------ */
@media (max-width: 640px) {
.aksrt-ticket-composer { padding: 8px 12px calc(12px + env(safe-area-inset-bottom, 0px)); }
.aksrt-ticket-composer__hint { display: none; }
.aksrt-ticket-composer__bar { justify-content: space-between; }
.aksrt-ticket-form__actions > button { flex: 1 1 0; }
}
@media (hover: none) {
.aksrt-ticket-composer__hint { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.aksrt-ticket-image { animation: none; }
}
+131 -343
View File
@@ -395,22 +395,113 @@
.aksrt-ticket-modal .aksrt-modal-runtime__title { font-size: 16px; font-weight: 700; }
/* ------------------------------------------------------------------ *
* 详情:概要 + 对话时间线
* ------------------------------------------------------------------ */
.aksrt-ticket-detail { display: grid; gap: 16px; text-align: left; }
.aksrt-ticket-detail__summary {
display: grid;
gap: 8px;
padding: 14px 16px;
border-radius: 10px;
background: var(--tickets-soft);
/* 详情:对话框定高且自身不滚动 —— 概要吸顶、时间线是唯一滚动区、回复框吸底。 */
.aksrt-ticket-detail-modal .aksrt-modal-runtime__dialog {
display: flex;
flex-direction: column;
height: min(88vh, 720px);
overflow: hidden;
}
.aksrt-ticket-detail__head { display: flex; flex-wrap: wrap; gap: 9px; align-items: center; }
.aksrt-ticket-detail__title { margin: 0; font-size: 16px; font-weight: 650; line-height: 1.5; word-break: break-word; }
.aksrt-ticket-detail-modal.aksrt-modal-runtime--bottom .aksrt-modal-runtime__dialog { height: 90vh; }
.aksrt-ticket-detail-modal .aksrt-modal-runtime__header { flex: 0 0 auto; padding-bottom: 12px; }
.aksrt-ticket-detail-modal .aksrt-modal-runtime__content {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
padding: 0;
overflow: hidden;
}
/* 加载骨架 / 错误态不走三段布局,补回内边距。 */
.aksrt-ticket-detail-modal .aksrt-modal-runtime__content > :not(.aksrt-ticket-detail) { margin: 18px; }
/* ------------------------------------------------------------------ *
* 详情:吸顶概要条 + 对话时间线
* ------------------------------------------------------------------ */
.aksrt-ticket-detail {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
text-align: left;
}
/* 概要条默认收起;展开的要素面板浮在时间线之上,不推动对话。 */
.aksrt-ticket-detail__summary {
position: relative;
z-index: 2;
flex: 0 0 auto;
margin: 0 18px;
border: 1px solid var(--tickets-line);
border-radius: 10px;
background: var(--surface-1, #fff);
box-shadow: inset 3px 0 0 var(--aksrt-ticket-color, var(--tickets-primary));
}
.aksrt-ticket-detail__toggle {
display: flex;
gap: 10px;
align-items: center;
width: 100%;
padding: 9px 12px 9px 14px;
border: 0;
border-radius: 10px;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
transition: background-color .15s ease;
}
.aksrt-ticket-detail__toggle:hover { background: var(--tickets-soft); }
.aksrt-ticket-detail__toggle:focus-visible { outline: 2px solid var(--tickets-primary); outline-offset: -2px; }
.aksrt-ticket-detail__title {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
text-overflow: ellipsis;
}
.aksrt-ticket-detail__toggle-text {
display: inline-flex;
flex: 0 0 auto;
gap: 4px;
align-items: center;
color: var(--tickets-muted);
font-size: 12px;
}
.aksrt-ticket-detail__toggle-text .fa { font-size: 10px; transition: transform .2s ease; }
.aksrt-ticket-detail__summary.is-open .aksrt-ticket-detail__toggle-text .fa { transform: rotate(180deg); }
.aksrt-ticket-detail__more {
position: absolute;
top: calc(100% + 6px);
right: 0;
left: 0;
display: grid;
gap: 12px;
max-height: min(50vh, 360px);
padding: 14px 16px;
overflow-y: auto;
border: 1px solid var(--tickets-line);
border-radius: 10px;
background: var(--surface-1, #fff);
box-shadow: 0 12px 32px rgb(15 18 28 / 14%);
scrollbar-width: thin;
}
.aksrt-ticket-detail__more[hidden] { display: none; }
.aksrt-ticket-detail__full-title { margin: 0; font-size: 15px; font-weight: 650; line-height: 1.5; word-break: break-word; }
.aksrt-ticket-detail__facts {
display: grid;
@@ -423,7 +514,32 @@
.aksrt-ticket-detail__facts dt { margin: 0 0 2px; color: var(--tickets-muted); font-size: 11px; }
.aksrt-ticket-detail__facts dd { margin: 0; overflow: hidden; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.aksrt-ticket-thread { display: grid; gap: 16px; margin: 0; padding: 0; list-style: none; }
/* 唯一滚动区:细滚动条,仅悬停时加深。 */
.aksrt-ticket-thread {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 16px;
min-height: 0;
margin: 0;
padding: 16px 18px;
overflow-y: auto;
overscroll-behavior: contain;
list-style: none;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--tickets-ink) 16%, transparent) transparent;
}
.aksrt-ticket-thread:focus-visible { outline: 2px solid var(--tickets-primary); outline-offset: -2px; }
.aksrt-ticket-thread::-webkit-scrollbar { width: 6px; }
.aksrt-ticket-thread::-webkit-scrollbar-track { background: transparent; }
.aksrt-ticket-thread::-webkit-scrollbar-thumb {
border-radius: 3px;
background: color-mix(in srgb, var(--tickets-ink) 16%, transparent);
}
.aksrt-ticket-thread:hover::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--tickets-ink) 28%, transparent); }
/* 用户消息靠右(主色气泡),客服消息靠左(中性气泡)。 */
.aksrt-ticket-msg { display: flex; gap: 10px; align-items: flex-start; }
@@ -546,331 +662,6 @@
.aksrt-ticket-attachment img { width: 100%; height: 100%; object-fit: cover; transition: transform .2s ease; }
.aksrt-ticket-attachment:hover img { transform: scale(1.05); }
.aksrt-ticket-detail__closed {
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin: 0;
padding: 12px;
border-radius: 10px;
background: var(--tickets-soft);
color: var(--tickets-muted);
font-size: 13px;
}
/* ------------------------------------------------------------------ *
* 回复框(吸底)
* ------------------------------------------------------------------ */
.aksrt-ticket-composer {
position: sticky;
bottom: -16px;
display: grid;
gap: 10px;
margin: 0 -18px -16px;
padding: 14px 18px 16px;
border-top: 1px solid var(--tickets-line);
background: var(--surface-1, #fff);
}
.aksrt-ticket-composer__quote {
display: flex;
gap: 8px;
align-items: center;
padding: 7px 10px;
border-radius: 8px;
background: color-mix(in srgb, var(--tickets-primary) 8%, transparent);
color: var(--tickets-ink-2);
font-size: 12px;
}
.aksrt-ticket-composer__quote[hidden] { display: none; }
.aksrt-ticket-composer__quote > .fa { color: var(--tickets-primary); }
.aksrt-ticket-composer__quote > span { flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.aksrt-ticket-composer__quote button {
flex: 0 0 auto;
padding: 2px 4px;
border: 0;
background: transparent;
color: var(--tickets-muted);
cursor: pointer;
}
.aksrt-ticket-composer__bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.aksrt-ticket-composer__tools { display: flex; flex: 1 1 auto; gap: 4px; }
.aksrt-ticket-tool {
display: inline-flex;
align-items: center;
gap: 5px;
height: 32px;
padding: 0 10px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--tickets-muted);
font-size: 12px;
cursor: pointer;
transition: background-color .15s ease, color .15s ease;
}
.aksrt-ticket-tool:hover:not(:disabled) { background: var(--tickets-soft); color: var(--tickets-ink); }
.aksrt-ticket-tool.is-danger:hover:not(:disabled) { color: var(--tickets-danger); }
.aksrt-ticket-tool.is-confirming {
background: color-mix(in srgb, var(--tickets-danger) 12%, transparent);
color: var(--tickets-danger);
}
.aksrt-ticket-tool:disabled { opacity: .6; cursor: default; }
/* ------------------------------------------------------------------ *
* 表单(新建 / 回复共用)
* ------------------------------------------------------------------ */
.aksrt-ticket-form { display: grid; gap: 16px; text-align: left; }
.aksrt-ticket-field {
position: relative;
display: grid;
gap: 6px;
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
.aksrt-ticket-field > label,
.aksrt-ticket-field > legend,
.aksrt-ticket-field__label {
display: flex;
gap: 6px;
align-items: center;
padding: 0;
color: var(--tickets-ink-2);
font-size: 13px;
font-weight: 600;
}
.aksrt-ticket-field em { color: var(--tickets-danger); font-style: normal; }
.aksrt-ticket-field__optional {
color: var(--tickets-muted);
font-size: 12px;
font-weight: 400;
}
.aksrt-ticket-field input[type="text"],
.aksrt-ticket-field select,
.aksrt-ticket-field textarea {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--tickets-line);
border-radius: 9px;
background: var(--surface-1, #fff);
color: inherit;
font-family: inherit;
font-size: 13px;
line-height: 1.6;
transition: border-color .15s ease, box-shadow .15s ease;
}
.aksrt-ticket-field textarea { min-height: 84px; resize: vertical; }
.aksrt-ticket-form .aksrt-ticket-field textarea { min-height: 120px; padding-bottom: 24px; }
.aksrt-ticket-field input[type="text"]:focus,
.aksrt-ticket-field select:focus,
.aksrt-ticket-field textarea:focus {
border-color: var(--tickets-primary);
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--tickets-primary) 16%, transparent);
}
.aksrt-ticket-field [aria-invalid="true"],
.aksrt-ticket-field[aria-invalid="true"] .aksrt-ticket-type span {
border-color: var(--tickets-danger) !important;
}
.aksrt-ticket-field__error {
margin: 0;
color: var(--tickets-danger);
font-size: 12px;
}
.aksrt-ticket-field__hint { margin: 0; color: var(--tickets-muted); font-size: 12px; }
.aksrt-ticket-counter {
color: var(--tickets-muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.aksrt-ticket-counter.is-near { color: var(--color-warning, #f59e0b); }
/* 新建表单内计数器贴在输入框右下角;回复框的计数器在工具栏里。 */
.aksrt-ticket-form .aksrt-ticket-field > .aksrt-ticket-counter {
position: absolute;
right: 10px;
bottom: 7px;
pointer-events: none;
}
.aksrt-ticket-form .aksrt-ticket-field:has(.aksrt-ticket-field__error) > .aksrt-ticket-counter { bottom: 27px; }
.aksrt-ticket-form .aksrt-ticket-field input[type="text"] { padding-right: 72px; }
/* 部门:单选卡片(替代下拉,选项一目了然)。 */
.aksrt-ticket-types__list { display: flex; flex-wrap: wrap; gap: 8px; }
.aksrt-ticket-type { position: relative; margin: 0; cursor: pointer; }
.aksrt-ticket-type input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.aksrt-ticket-type span {
display: inline-flex;
align-items: center;
height: 34px;
padding: 0 14px;
border: 1px solid var(--tickets-line);
border-radius: 999px;
background: var(--surface-1, #fff);
font-size: 13px;
transition: border-color .15s ease, background-color .15s ease, color .15s ease;
}
.aksrt-ticket-type:hover span { border-color: color-mix(in srgb, var(--tickets-primary) 45%, transparent); }
.aksrt-ticket-type input:checked + span {
border-color: var(--tickets-primary);
background: color-mix(in srgb, var(--tickets-primary) 10%, transparent);
color: var(--tickets-primary);
font-weight: 600;
}
.aksrt-ticket-type input:focus-visible + span {
outline: 2px solid var(--tickets-primary);
outline-offset: 2px;
}
.aksrt-ticket-form__actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 4px;
}
.aksrt-ticket-form__actions > button { min-width: 96px; }
/* ------------------------------------------------------------------ *
* 图片上传
* ------------------------------------------------------------------ */
.aksrt-ticket-uploader { display: flex; flex-wrap: wrap; gap: 8px; }
.aksrt-ticket-images { display: contents; }
.aksrt-ticket-image {
position: relative;
width: 68px;
height: 68px;
border: 1px solid var(--tickets-line);
border-radius: 9px;
overflow: hidden;
background: var(--tickets-soft);
}
.aksrt-ticket-image img { width: 100%; height: 100%; object-fit: cover; }
.aksrt-ticket-image.is-add {
display: inline-grid;
place-content: center;
gap: 2px;
padding: 0;
border-style: dashed;
background: transparent;
color: var(--tickets-muted);
font-size: 11px;
text-align: center;
cursor: pointer;
transition: border-color .15s ease, color .15s ease;
}
.aksrt-ticket-image.is-add .fa { font-size: 15px; }
.aksrt-ticket-image.is-add:hover:not(:disabled) { border-color: var(--tickets-primary); color: var(--tickets-primary); }
.aksrt-ticket-image.is-add:disabled { opacity: .45; cursor: not-allowed; }
.aksrt-ticket-image.is-uploading { display: grid; place-items: center; }
.aksrt-ticket-image__spinner {
width: 20px;
height: 20px;
border: 2px solid var(--tickets-line);
border-top-color: var(--tickets-primary);
border-radius: 50%;
animation: aksrt-ticket-spin .8s linear infinite;
}
.aksrt-ticket-image__remove {
position: absolute;
top: 3px;
right: 3px;
display: grid;
place-items: center;
width: 20px;
height: 20px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgb(0 0 0 / 55%);
color: #fff;
font-size: 10px;
cursor: pointer;
}
@keyframes aksrt-ticket-spin { to { transform: rotate(360deg); } }
/* ------------------------------------------------------------------ *
* 提交成功
* ------------------------------------------------------------------ */
.aksrt-ticket-done { display: grid; gap: 8px; justify-items: center; padding: 12px 4px 4px; text-align: center; }
.aksrt-ticket-done__icon {
display: inline-grid;
place-items: center;
width: 56px;
height: 56px;
margin-bottom: 4px;
border-radius: 50%;
background: color-mix(in srgb, var(--color-success, #10b981) 14%, transparent);
color: var(--color-success, #10b981);
font-size: 24px;
}
.aksrt-ticket-done h4 { margin: 0; font-size: 17px; }
.aksrt-ticket-done > p { margin: 0; color: var(--tickets-muted); font-size: 13px; }
.aksrt-ticket-done__notice {
width: 100%;
margin-top: 6px;
padding: 12px 14px;
border-radius: 10px;
background: var(--tickets-soft);
color: var(--tickets-ink-2);
font-size: 13px;
line-height: 1.7;
text-align: left;
}
.aksrt-ticket-done__notice > :last-child { margin-bottom: 0; }
.aksrt-ticket-done .aksrt-ticket-form__actions { justify-content: center; width: 100%; margin-top: 10px; }
/* 按钮忙碌态(发送 / 提交 / 催单中)。 */
.aksrt-ticket-modal .is-busy { opacity: .7; cursor: progress; }
/* ------------------------------------------------------------------ *
* 窄屏
* ------------------------------------------------------------------ */
@@ -885,9 +676,6 @@
.aksrt-tickets-page__card { padding-right: 36px; }
.aksrt-tickets-page__card-arrow { right: 14px; }
.aksrt-ticket-msg__main { max-width: calc(100% - 42px); }
.aksrt-ticket-composer__tools .aksrt-ticket-tool span { display: none; }
.aksrt-ticket-tool.is-confirming span { display: inline; }
.aksrt-ticket-form__actions > button { flex: 1 1 0; }
}
@media (prefers-reduced-motion: reduce) {
+27 -8
View File
@@ -227,7 +227,10 @@
button.disabled = full;
});
var count = root.querySelector('[data-aksrt-ticket-image-count]');
if (count) count.textContent = holder.children.length + ' / ' + attachmentMax();
if (count) {
count.textContent = holder.children.length + ' / ' + attachmentMax();
count.classList.toggle('is-zero', holder.children.length === 0);
}
}
function uploadFiles(root, files) {
@@ -311,16 +314,31 @@
shell.addEventListener('change', function (event) {
clearInvalid(event.target.closest('fieldset') || event.target);
});
// 粘贴截图直接进入待上传区(未开启附件时表单内没有图片容器,自然跳过)。
shell.addEventListener('paste', function (event) {
var root = event.target.closest && event.target.closest('form');
if (!root || !imageHolder(root) || !event.clipboardData) return;
var files = Array.prototype.filter.call(event.clipboardData.files || [], function (file) {
return /^image\//.test(file.type);
});
if (!files.length) return;
event.preventDefault();
uploadFiles(root, files);
});
return ctx;
}
function uploaderHtml() {
// 回复框:工具栏里的「图片」按钮 + 输入框上方的待发送缩略图条(空时由 CSS :empty 收起)。
function attachButtonHtml() {
if (!attachmentEnabled()) return '';
return '<div class="aksrt-ticket-uploader">'
+ '<div class="aksrt-ticket-images" data-aksrt-ticket-images></div>'
+ '<button type="button" class="aksrt-ticket-image is-add" data-aksrt-ticket-upload aria-label="添加图片">'
+ icon('fa-plus') + '<span data-aksrt-ticket-image-count>0 / ' + attachmentMax() + '</span></button>'
+ '</div>';
return '<button type="button" class="aksrt-ticket-tool" data-aksrt-ticket-upload title="添加图片,也可直接粘贴截图">'
+ icon('fa-image') + '<span>图片</span>'
+ '<span class="aksrt-ticket-tool__count is-zero" data-aksrt-ticket-image-count>0 / ' + attachmentMax() + '</span></button>';
}
function attachTrayHtml() {
if (!attachmentEnabled()) return '';
return '<div class="aksrt-ticket-tray" data-aksrt-ticket-images aria-label="待发送图片"></div>';
}
window.AksrtTicketCore = {
@@ -346,6 +364,7 @@
collectAttachments: collectAttachments,
isUploading: isUploading,
bindShell: bindShell,
uploaderHtml: uploaderHtml,
attachButtonHtml: attachButtonHtml,
attachTrayHtml: attachTrayHtml,
};
})();
+126 -37
View File
@@ -6,6 +6,8 @@
* - 弹窗经 AksrtModalRuntime.open 打开(运行时自带标题栏 / 焦点陷阱 / 移动端底部抽屉),
* 先出运行时骨架,数据到达后填充,避免点击后无反馈
* - 详情为对话时间线:问题描述作为首条消息,用户靠右、客服靠左;回复后原地刷新而不是关窗
* - 详情三段式:顶部可折叠概要(默认收起)+ 唯一滚动区时间线 + 底部回复框;
* 回复框工具栏(图片 / 催单 / 关闭)在输入框上方,待发送图片以缩略图条展示
* - 提交工单与回复前先过人机验证(ticket_create / ticket_reply,场景未启用自动放行)
* - 图片走 /aksrt/v1/ticket/upload-attachment,上传中显示占位格,未传完禁止提交
*
@@ -39,10 +41,12 @@
var collectAttachments = core.collectAttachments;
var isUploading = core.isUploading;
var bindShell = core.bindShell;
var uploaderHtml = core.uploaderHtml;
var attachButtonHtml = core.attachButtonHtml;
var attachTrayHtml = core.attachTrayHtml;
var SEARCH_DEBOUNCE = 400;
var CLOSE_CONFIRM_MS = 4000;
var COMPOSER_MAX_HEIGHT = 160;
var state = {
page: 1,
@@ -273,16 +277,16 @@
var ctx = shell.__aksrtTicket || (shell.__aksrtTicket = bindShell(shell, onDetailClick));
ctx.ticket = ticket;
ctx.quoteId = 0;
ctx.urgeUntil = Date.now() + Math.max(0, parseInt(ticket.urge_wait, 10) || 0) * 1000;
window.clearTimeout(ctx.closeTimer);
var title = shell.querySelector('.aksrt-modal-runtime__title');
if (title) title.textContent = '工单 #' + ticket.ticket_num;
setModalContent(shell, buildDetailHtml(ticket));
setModalContent(shell, buildDetailHtml(ticket, !!ctx.summaryOpen));
// 有沟通记录或刚发完回复时定位到最新一条(聊天类产品惯例)。
var thread = shell.querySelector('[data-aksrt-ticket-thread]');
if (thread && (opts.scrollEnd || (ticket.replies || []).length)) stickToEnd(thread);
// 首次打开且沟通较多、或刚发完回复时,滚到最新一条。
var replies = ticket.replies || [];
if (opts.scrollEnd || (opts.initial && replies.length > 3)) {
var last = shell.querySelector('[data-aksrt-ticket-thread] > li:last-child');
if (last) last.scrollIntoView({ block: 'end' });
}
var textarea = shell.querySelector('[data-aksrt-ticket-reply-form] textarea');
if (opts.scrollEnd && textarea) textarea.focus();
if (textarea) {
@@ -292,7 +296,13 @@
submitReply(shell, ctx);
}
});
// 输入框随内容长高,到上限后内部滚动,避免挤压时间线。
textarea.addEventListener('input', function () {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, COMPOSER_MAX_HEIGHT) + 'px';
});
}
startUrgeTicker(shell, ctx);
var form = shell.querySelector('[data-aksrt-ticket-reply-form]');
if (form) {
form.addEventListener('submit', function (event) {
@@ -306,19 +316,54 @@
return '<div><dt>' + label + '</dt><dd>' + value + '</dd></div>';
}
function buildDetailHtml(ticket) {
var facts = fact('提交时间', timeTag(ticket.created_at))
// 概要条:状态 + 单行标题 + 展开按钮;展开后显示完整标题与要素,状态跨重绘保留。
function summaryHtml(ticket, open) {
var panelId = 'aksrt-ticket-summary-' + escapeHtml(ticket.id);
var facts = fact('工单编号', '#' + escapeHtml(ticket.ticket_num))
+ fact('提交时间', timeTag(ticket.created_at))
+ (ticket.ticket_type_name ? fact('工单部门', escapeHtml(ticket.ticket_type_name)) : '')
+ (ticket.order_number ? fact('关联订单', escapeHtml(ticket.order_number)) : '')
+ (ticket.admin_name ? fact('处理客服', escapeHtml(ticket.admin_name)) : '');
+ fact('处理客服', ticket.admin_name ? escapeHtml(ticket.admin_name) : '待分配');
return '<section class="aksrt-ticket-detail__summary' + (open ? ' is-open' : '') + '"'
+ ' style="--aksrt-ticket-color:' + escapeHtml(ticket.status_color || '#6b7280') + '">'
+ '<button type="button" class="aksrt-ticket-detail__toggle" data-aksrt-ticket-summary-toggle'
+ ' aria-expanded="' + (open ? 'true' : 'false') + '" aria-controls="' + panelId + '">'
+ statusBadge(ticket)
+ '<span class="aksrt-ticket-detail__title">' + escapeHtml(ticket.title) + '</span>'
+ '<span class="aksrt-ticket-detail__toggle-text">' + (open ? '收起' : '详情') + icon('fa-chevron-down') + '</span>'
+ '</button>'
+ '<div class="aksrt-ticket-detail__more" id="' + panelId + '"' + (open ? '' : ' hidden') + '>'
+ '<h4 class="aksrt-ticket-detail__full-title">' + escapeHtml(ticket.title) + '</h4>'
+ '<dl class="aksrt-ticket-detail__facts">' + facts + '</dl></div></section>';
}
function toggleSummary(shell, ctx) {
var section = shell.querySelector('.aksrt-ticket-detail__summary');
var button = section && section.querySelector('[data-aksrt-ticket-summary-toggle]');
var more = section && section.querySelector('.aksrt-ticket-detail__more');
if (!button || !more) return;
ctx.summaryOpen = !ctx.summaryOpen;
section.classList.toggle('is-open', ctx.summaryOpen);
button.setAttribute('aria-expanded', ctx.summaryOpen ? 'true' : 'false');
more.hidden = !ctx.summaryOpen;
var text = button.querySelector('.aksrt-ticket-detail__toggle-text');
if (text) text.innerHTML = (ctx.summaryOpen ? '收起' : '详情') + icon('fa-chevron-down');
}
function stickToEnd(thread) {
thread.scrollTop = thread.scrollHeight;
// 图片加载后高度会变,再贴一次底。
thread.querySelectorAll('img').forEach(function (img) {
if (!img.complete) img.addEventListener('load', function () { thread.scrollTop = thread.scrollHeight; }, { once: true });
});
}
function buildDetailHtml(ticket, summaryOpen) {
var html = '<div class="aksrt-ticket-detail" data-aksrt-ticket-detail-id="' + escapeHtml(ticket.id) + '">'
+ '<section class="aksrt-ticket-detail__summary">'
+ '<div class="aksrt-ticket-detail__head">' + statusBadge(ticket) + '</div>'
+ '<h4 class="aksrt-ticket-detail__title">' + escapeHtml(ticket.title) + '</h4>'
+ '<dl class="aksrt-ticket-detail__facts">' + facts + '</dl></section>';
+ summaryHtml(ticket, summaryOpen);
html += '<ol class="aksrt-ticket-thread" data-aksrt-ticket-thread>';
html += '<ol class="aksrt-ticket-thread" data-aksrt-ticket-thread tabindex="0" aria-label="沟通记录">';
html += messageHtml({
id: 0,
author_type: 'user',
@@ -384,18 +429,24 @@
function composerHtml(ticket) {
var fieldId = 'aksrt-ticket-reply-' + escapeHtml(ticket.id);
// 自上而下:工具栏(图片 | 催单 · 关闭)→ 引用条 → 待发送缩略图 → 输入框 → 发送行。
return '<form class="aksrt-ticket-composer" data-aksrt-ticket-reply-form novalidate>'
+ '<div class="aksrt-ticket-composer__toolbar" role="toolbar" aria-label="回复工具">'
+ '<div class="aksrt-ticket-composer__tools">' + attachButtonHtml() + '</div>'
+ '<div class="aksrt-ticket-composer__actions">'
+ '<button type="button" class="aksrt-ticket-tool is-chip" data-aksrt-ticket-urge title="提醒客服尽快处理">'
+ icon('fa-bell') + '<span>催单</span></button>'
+ '<button type="button" class="aksrt-ticket-tool is-chip is-danger" data-aksrt-ticket-close title="问题已解决时可关闭工单">'
+ icon('fa-check-circle') + '<span>关闭工单</span></button>'
+ '</div></div>'
+ '<div class="aksrt-ticket-composer__quote" data-aksrt-ticket-quote-preview hidden></div>'
+ '<div class="aksrt-ticket-field">'
+ attachTrayHtml()
+ '<div class="aksrt-ticket-composer__input">'
+ '<label class="screen-reader-text" for="' + fieldId + '">回复内容</label>'
+ '<textarea id="' + fieldId + '" name="reply" rows="3" maxlength="3000"'
+ ' placeholder="补充说明或回复客服,Ctrl + Enter 发送"></textarea></div>'
+ uploaderHtml()
+ '<textarea id="' + fieldId + '" name="reply" rows="2" maxlength="3000"'
+ ' placeholder="补充说明或回复客服…"></textarea></div>'
+ '<div class="aksrt-ticket-composer__bar">'
+ '<div class="aksrt-ticket-composer__tools">'
+ '<button type="button" class="aksrt-ticket-tool" data-aksrt-ticket-urge title="提醒客服尽快处理">' + icon('fa-bell') + '<span>催单</span></button>'
+ '<button type="button" class="aksrt-ticket-tool is-danger" data-aksrt-ticket-close>' + icon('fa-check-circle') + '<span>关闭工单</span></button>'
+ '</div>'
+ '<span class="aksrt-ticket-composer__hint">Ctrl + Enter 发送</span>'
+ '<span class="aksrt-ticket-counter" data-aksrt-ticket-counter="reply">0 / 3000</span>'
+ '<button type="submit" class="btn-primary" data-aksrt-ticket-submit>' + icon('fa-paper-plane') + '发送</button>'
+ '</div></form>';
@@ -421,11 +472,12 @@
function onDetailClick(event, ctx) {
var shell = event.currentTarget;
if (event.target.closest('[data-aksrt-ticket-summary-toggle]')) return toggleSummary(shell, ctx);
var quote = event.target.closest('[data-aksrt-ticket-quote]');
if (quote) return setQuote(shell, ctx, quote);
if (event.target.closest('[data-aksrt-ticket-quote-cancel]')) return setQuote(shell, ctx, null);
var urge = event.target.closest('[data-aksrt-ticket-urge]');
if (urge) return handleUrge(urge, ctx);
if (urge) return handleUrge(urge, shell, ctx);
var close = event.target.closest('[data-aksrt-ticket-close]');
if (close) handleClose(close, shell, ctx);
}
@@ -459,22 +511,58 @@
}).then(expectSuccess);
}
function handleUrge(button, ctx) {
/* 催单冷却:服务端下发剩余秒数(详情 urge_wait / 催单成功 / 429 错误数据),按钮倒计时。 */
function urgeLeft(ctx) {
return Math.max(0, Math.ceil(((ctx.urgeUntil || 0) - Date.now()) / 1000));
}
function formatWait(seconds) {
var s = seconds % 60;
return Math.floor(seconds / 60) + ':' + (s < 10 ? '0' : '') + s;
}
/** 刷新催单按钮;返回 true 表示仍在冷却、需要继续计时。 */
function syncUrge(shell, ctx) {
var button = shell.querySelector('[data-aksrt-ticket-urge]');
if (!button || button.classList.contains('is-busy')) return !!button;
var left = urgeLeft(ctx);
var label = button.querySelector('span');
button.disabled = left > 0;
button.classList.toggle('is-cooling', left > 0);
if (label) label.textContent = left > 0 ? '已催单 ' + formatWait(left) : '催单';
button.title = left > 0 ? formatWait(left) + ' 后可再次催单' : '提醒客服尽快处理';
return left > 0;
}
function startUrgeTicker(shell, ctx) {
window.clearInterval(ctx.urgeTimer);
if (!syncUrge(shell, ctx)) return;
ctx.urgeTimer = window.setInterval(function () {
if (!alive(shell) || !syncUrge(shell, ctx)) window.clearInterval(ctx.urgeTimer);
}, 1000);
}
function handleUrge(button, shell, ctx) {
if (button.disabled) return;
setBusy(button, true);
sendUrge(ctx.ticket).then(function () {
notyf('已提醒客服,请耐心等待', 'success');
button.classList.remove('is-busy');
button.innerHTML = icon('fa-check') + '<span>已催单</span>';
delete button.dataset.label;
button.disabled = true;
button.classList.add('is-busy');
sendUrge(ctx.ticket).then(function (result) {
ctx.urgeUntil = Date.now() + (parseInt(result.data && result.data.urge_wait, 10) || 0) * 1000;
notyf('已提醒客服优先处理,请耐心等待', 'success');
}).catch(function (error) {
setBusy(button, false);
var wait = parseInt(error && error.data && error.data.urge_wait, 10) || 0;
if (wait) ctx.urgeUntil = Date.now() + wait * 1000;
notyf(failMessage(error, '催单失败'), 'danger');
}).then(function () {
button.classList.remove('is-busy');
button.disabled = false;
startUrgeTicker(shell, ctx);
});
}
function sendUrge(ticket) {
return request('/tickets/' + ticket.id + '/urge', { method: 'PUT' }).then(expectSuccess);
return request('/tickets/' + ticket.id + '/urge', { method: 'POST' }).then(expectSuccess);
}
// 两步确认:首次点击变为「再次点击确认」,超时自动复原,替代阻塞式 window.confirm。
@@ -504,7 +592,7 @@
}
function sendClose(ticket) {
return request('/tickets/' + ticket.id + '/close', { method: 'PUT' }).then(expectSuccess);
return request('/tickets/' + ticket.id + '/close', { method: 'POST' }).then(expectSuccess);
}
/* ------------------------------------------------------------------ *
@@ -578,8 +666,9 @@
if (attachmentEnabled()) {
html += '<div class="aksrt-ticket-field"><span class="aksrt-ticket-field__label">图片附件<span class="aksrt-ticket-field__optional">可选</span></span>'
+ uploaderHtml()
+ '<p class="aksrt-ticket-field__hint">支持 JPG / PNG / GIF / WebP,最多 ' + attachmentMax() + ' 张</p></div>';
+ attachTrayHtml()
+ '<div class="aksrt-ticket-field__row">' + attachButtonHtml()
+ '<p class="aksrt-ticket-field__hint">支持 JPG / PNG / GIF / WebP,可直接粘贴截图,最多 ' + attachmentMax() + ' 张</p></div></div>';
}
html += '<div class="aksrt-ticket-form__actions">'
+10
View File
@@ -424,6 +424,16 @@ trait ScriptsPart1b {
(string) filemtime($tickets_css)
);
}
// 回复框 / 新建表单 / 待发送图片条:依赖 tickets.css 的 --tickets-* token。
$tickets_compose_css = get_template_directory() . '/assets/css/routes/tickets-compose.css';
if (file_exists($tickets_compose_css)) {
wp_enqueue_style(
'aksrt-tickets-compose',
$base_uri . '/assets/css/routes/tickets-compose.css',
['aksrt-tickets'],
(string) filemtime($tickets_compose_css)
);
}
// 共享工具层(请求 / 弹窗 / 上传 / 行内校验)先于面板脚本加载。
$ticket_core_js = get_template_directory() . '/assets/js/ticket-center-core.js';
if (file_exists($ticket_core_js)) {
+6 -2
View File
@@ -42,10 +42,10 @@ final class TicketNotifier {
self::send($user_id, 'ticket_reply', $content, __('工单有新回复', 'aksrt'), self::user_url());
}
/** 用户回复 / 催单 → 通知跟进人。 */
/** 用户回复 / 催单 → 通知跟进人;催单时若尚无跟进人则广播给管理员。 */
public static function admin_activity(array $ticket, string $kind): void {
$admin_id = (int) ($ticket['admin_id'] ?? 0);
if ($admin_id <= 0) {
if ($admin_id <= 0 && $kind !== 'urge') {
return;
}
if ($kind === 'urge') {
@@ -65,6 +65,10 @@ final class TicketNotifier {
);
$title = __('工单有新回复', 'aksrt');
}
if ($admin_id <= 0) {
self::to_admins('ticket_' . $kind, $content, $title);
return;
}
self::send($admin_id, 'ticket_' . $kind, $content, $title, self::admin_url());
}
+15 -2
View File
@@ -226,6 +226,7 @@ final class TicketRest {
}
$ticket['replies'] = $repo->replies($id);
$ticket['notes'] = [];
$ticket['urge_wait'] = empty($ticket['is_closed']) ? $this->urge_wait($id) : 0;
return rest_ensure_response(['success' => true, 'data' => $ticket]);
}
@@ -336,11 +337,14 @@ final class TicketRest {
}
$last = get_transient('aksrt_ticket_urge_' . $id);
if ($last !== false && (int) $last + TicketSettings::urge_interval() * MINUTE_IN_SECONDS > time()) {
return new \WP_Error('aksrt_ticket_urge_limit', __('刚刚已催单,请稍后再试', 'aksrt'), ['status' => 429]);
return new \WP_Error('aksrt_ticket_urge_limit', __('刚刚已催单,请稍后再试', 'aksrt'), [
'status' => 429,
'urge_wait' => $this->urge_wait($id),
]);
}
set_transient('aksrt_ticket_urge_' . $id, time(), TicketSettings::urge_interval() * MINUTE_IN_SECONDS + 60);
TicketNotifier::admin_activity($ticket, 'urge');
return rest_ensure_response(['success' => true]);
return rest_ensure_response(['success' => true, 'data' => ['urge_wait' => $this->urge_wait($id)]]);
}
public function close(\WP_REST_Request $request) {
@@ -363,6 +367,15 @@ final class TicketRest {
* 内部工具
* ------------------------------------------------------------------ */
/** 距离下次可催单的剩余秒数(0 = 可立即催单)。 */
private function urge_wait(int $id): int {
$last = get_transient('aksrt_ticket_urge_' . $id);
if ($last === false) {
return 0;
}
return max(0, (int) $last + TicketSettings::urge_interval() * MINUTE_IN_SECONDS - time());
}
private function ensure_enabled(): bool {
return TicketSettings::enabled();
}