/** * AKSRT Ticket Center — 用户中心「工单中心」面板。 * * - 列表 / 统计 / 详情全部走 REST,模板只输出容器与首屏骨架(profile-tickets.php) * - 统计卡兼作状态快捷筛选,与状态下拉双向同步;搜索输入防抖,旧请求结果按序号丢弃 * - 弹窗经 AksrtModalRuntime.open 打开(运行时自带标题栏 / 焦点陷阱 / 移动端底部抽屉), * 先出运行时骨架,数据到达后填充,避免点击后无反馈 * - 详情为对话时间线:问题描述作为首条消息,用户靠右、客服靠左;回复后原地刷新而不是关窗 * - 详情三段式:顶部可折叠概要(默认收起)+ 唯一滚动区时间线 + 底部回复框; * 回复框工具栏(图片 / 催单 / 关闭)在输入框上方,待发送图片以缩略图条展示 * - 提交工单与回复前先过人机验证(ticket_create / ticket_reply,场景未启用自动放行) * - 图片走 /aksrt/v1/ticket/upload-attachment,上传中显示占位格,未传完禁止提交 * * @package AKSRT */ (function () { 'use strict'; var core = window.AksrtTicketCore; if (!core) return; var settings = core.settings; var attachmentEnabled = core.attachmentEnabled; var attachmentMax = core.attachmentMax; var lengthMax = core.lengthMax; var escapeHtml = core.escapeHtml; var notyf = core.notyf; var request = core.request; var expectSuccess = core.expectSuccess; var runCaptcha = core.runCaptcha; var failMessage = core.failMessage; var timeTag = core.timeTag; var icon = core.icon; var statusBadge = core.statusBadge; var emptyHtml = core.emptyHtml; var setBusy = core.setBusy; var markInvalid = core.markInvalid; var openModal = core.openModal; var alive = core.alive; var setModalContent = core.setModalContent; var modalError = core.modalError; var collectAttachments = core.collectAttachments; var isUploading = core.isUploading; var bindShell = core.bindShell; 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, perPage: 10, totalPages: 1, status: '', type: '', keywords: '', meta: null, listSeq: 0, }; /* ------------------------------------------------------------------ * * 列表 / 统计 / 筛选 * ------------------------------------------------------------------ */ function panel() { return document.querySelector('[data-aksrt-ticket-center]'); } function listNode() { var root = panel(); return root ? root.querySelector('[data-aksrt-ticket-list]') : null; } function hasFilters() { return !!(state.status || state.type || state.keywords); } function skeletonHtml() { var card = '
' + '
' + '
'; return ''; } function loadList() { var list = listNode(); if (!list) return; var seq = ++state.listSeq; // 已有卡片时只做淡化,避免整块替换造成跳动;空态 / 错误态则换回骨架。 if (!list.querySelector('[data-aksrt-ticket-id]')) list.innerHTML = skeletonHtml(); list.classList.add('is-busy'); list.setAttribute('aria-busy', 'true'); var params = ['page=' + state.page, 'per_page=' + state.perPage]; if (state.status) params.push('status=' + encodeURIComponent(state.status)); if (state.type) params.push('ticket_type_id=' + encodeURIComponent(state.type)); if (state.keywords) params.push('keywords=' + encodeURIComponent(state.keywords)); request('/tickets?' + params.join('&')).then(expectSuccess).then(function (result) { if (seq !== state.listSeq) return; var data = result.data || {}; state.totalPages = Math.max(1, parseInt(data.total_pages, 10) || 1); if (state.page > state.totalPages) { state.page = state.totalPages; loadList(); return; } renderList(Array.isArray(data.items) ? data.items : []); renderPagination(); }).catch(function (error) { if (seq !== state.listSeq) return; list.innerHTML = emptyHtml('fa-exclamation-circle', '工单列表加载失败', failMessage(error, '网络异常,请稍后重试。'), ''); renderPagination(true); }).then(function () { if (seq !== state.listSeq) return; list.classList.remove('is-busy'); list.setAttribute('aria-busy', 'false'); }); } function renderList(items) { var list = listNode(); if (!list) return; if (!items.length) { list.innerHTML = hasFilters() ? emptyHtml('fa-search', '没有符合条件的工单', '换个关键词,或清除筛选后再试。', '') : emptyHtml('fa-headset', '还没有工单', '遇到问题时提交工单,我们会尽快处理。', ''); return; } list.innerHTML = items.map(cardHtml).join(''); } function cardHtml(ticket) { var id = escapeHtml(ticket.id); var meta = []; if (ticket.ticket_type_name) meta.push('' + icon('fa-layer-group') + escapeHtml(ticket.ticket_type_name) + ''); if (ticket.order_number) meta.push('' + icon('fa-receipt') + '订单 ' + escapeHtml(ticket.order_number) + ''); meta.push('' + icon('fa-clock') + '更新于 ' + timeTag(ticket.updated_at || ticket.created_at) + ''); // 标题按钮的 ::after 铺满整卡,整张卡可点且只有一个可聚焦目标。 return '
' + '
' + statusBadge(ticket) + '#' + escapeHtml(ticket.ticket_num) + '
' + '

' + '
' + meta.join('') + '
' + '' + '
'; } /** 1 … 4 5 6 … 20 窗口分页。 */ function pageList(current, total) { var out = [1]; var from = Math.max(2, current - 1); var to = Math.min(total - 1, current + 1); if (from > 2) out.push('gap'); for (var i = from; i <= to; i++) out.push(i); if (to < total - 1) out.push('gap'); if (total > 1) out.push(total); return out; } function stepButton(page, label, iconName, disabled) { return ''; } function renderPagination(hide) { var root = panel(); var nav = root && root.querySelector('[data-aksrt-ticket-pagination]'); if (!nav) return; if (hide || state.totalPages <= 1) { nav.hidden = true; nav.innerHTML = ''; return; } var html = stepButton(state.page - 1, '上一页', 'fa-chevron-left', state.page <= 1); pageList(state.page, state.totalPages).forEach(function (page) { if (page === 'gap') { html += ''; return; } var active = page === state.page; html += ''; }); html += stepButton(state.page + 1, '下一页', 'fa-chevron-right', state.page >= state.totalPages); nav.innerHTML = html; nav.hidden = false; } function loadStatistic() { request('/tickets/statistic').then(expectSuccess).then(function (result) { var root = panel(); var data = result.data || {}; if (!root) return; var total = 0; Object.keys(data).forEach(function (key) { var count = parseInt(data[key], 10) || 0; total += count; var node = root.querySelector('[data-aksrt-ticket-stat="' + key + '"] strong'); if (node) node.textContent = String(count); }); var all = root.querySelector('[data-aksrt-ticket-stat=""] strong'); if (all) all.textContent = String(total); }).catch(function () { /* 统计失败不阻塞列表 */ }); } function syncFilters() { var root = panel(); if (!root) return; root.querySelectorAll('[data-aksrt-ticket-stat]').forEach(function (button) { var active = button.getAttribute('data-aksrt-ticket-stat') === state.status; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', active ? 'true' : 'false'); }); var select = root.querySelector('[data-aksrt-ticket-filter="status"]'); if (select) select.value = state.status; var reset = root.querySelector('[data-aksrt-tickets-search] [data-aksrt-ticket-reset]'); if (reset) reset.hidden = !hasFilters(); } function applyFilters(patch) { Object.assign(state, patch, { page: 1 }); syncFilters(); loadList(); } function resetFilters() { var root = panel(); if (root) { var input = root.querySelector('[data-aksrt-tickets-search] input'); if (input) input.value = ''; var type = root.querySelector('[data-aksrt-ticket-filter="ticket_type_id"]'); if (type) type.value = ''; } applyFilters({ status: '', type: '', keywords: '' }); } function refreshPanel() { loadList(); loadStatistic(); } function scrollPanelIntoView() { var root = panel(); if (root && root.getBoundingClientRect().top < 0) root.scrollIntoView({ behavior: 'smooth', block: 'start' }); } /* ------------------------------------------------------------------ * * 详情弹窗(对话时间线 + 回复框) * ------------------------------------------------------------------ */ function openDetail(ticketId) { var shell = openModal({ title: '工单详情', className: 'aksrt-ticket-modal aksrt-ticket-detail-modal', kind: 'ticket-detail' }); if (shell) fetchDetail(shell, ticketId, { initial: true }); } function fetchDetail(shell, ticketId, opts) { return request('/tickets/' + ticketId).then(expectSuccess).then(function (result) { if (!alive(shell)) return; if (!result.data) return modalError(shell, '工单不存在'); renderDetail(shell, result.data, opts || {}); }).catch(function (error) { if (alive(shell)) modalError(shell, failMessage(error, '工单详情加载失败,请重试')); }); } function renderDetail(shell, ticket, opts) { 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, !!ctx.summaryOpen)); // 有沟通记录或刚发完回复时定位到最新一条(聊天类产品惯例)。 var thread = shell.querySelector('[data-aksrt-ticket-thread]'); if (thread && (opts.scrollEnd || (ticket.replies || []).length)) stickToEnd(thread); var textarea = shell.querySelector('[data-aksrt-ticket-reply-form] textarea'); if (opts.scrollEnd && textarea) textarea.focus(); if (textarea) { textarea.addEventListener('keydown', function (event) { if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { event.preventDefault(); 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) { event.preventDefault(); submitReply(shell, ctx); }); } } function fact(label, value) { return '
' + label + '
' + value + '
'; } // 概要条:状态 + 单行标题 + 展开按钮;展开后显示完整标题与要素,状态跨重绘保留。 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)) : '') + fact('处理客服', ticket.admin_name ? escapeHtml(ticket.admin_name) : '待分配'); return '
' + '' + '
' + '

' + escapeHtml(ticket.title) + '

' + '
' + facts + '
'; } 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 = '
' + summaryHtml(ticket, summaryOpen); html += '
    '; html += messageHtml({ id: 0, author_type: 'user', author_name: ticket.user_name, content: ticket.content, attachment: ticket.attachment, created_at: ticket.created_at, isOrigin: true, }, false); (ticket.replies || []).forEach(function (reply) { html += messageHtml(reply, !ticket.is_closed); }); html += '
'; html += ticket.is_closed ? '

' + icon('fa-lock') + '工单已关闭,如需继续沟通请提交新工单。

' : composerHtml(ticket); return html + '
'; } function attachmentPreview(images) { if (!images || !images.length) return ''; return '
' + images.map(function (url) { return '' + '附件图片'; }).join('') + '
'; } function quoteBlock(quote) { if (!quote) return ''; var text = quote.is_deleted ? '原回复已删除' : (quote.content || ''); return '
' + (quote.author_name ? '' + escapeHtml(quote.author_name) + '' : '') + '' + escapeHtml(text) + '
'; } function excerpt(text) { var flat = String(text || '').replace(/\s+/g, ' ').trim(); return flat.length > 60 ? flat.slice(0, 60) + '…' : flat; } function messageHtml(msg, canQuote) { if (msg.author_type === 'system') { // 系统记录(如催单):居中提示,不可引用。 return '
  • ' + icon('fa-bell') + escapeHtml(msg.content) + timeTag(msg.created_at) + '
  • '; } var admin = msg.author_type === 'admin'; var name = msg.author_name || (admin ? '客服' : '我'); var avatar = admin ? icon('fa-headset') : escapeHtml(Array.from(name)[0] || '我'); var tags = (admin ? '客服' : '') + (msg.isOrigin ? '问题描述' : ''); return '
  • ' + '' + '
    ' + '
    ' + escapeHtml(name) + '' + tags + timeTag(msg.created_at) + '
    ' + '
    ' + quoteBlock(msg.quote) + '
    ' + escapeHtml(msg.content) + '
    ' + attachmentPreview(msg.attachment) + '
    ' + (canQuote && msg.id ? '' : '') + '
  • '; } function composerHtml(ticket) { var fieldId = 'aksrt-ticket-reply-' + escapeHtml(ticket.id); // 自上而下:工具栏(图片 | 催单 · 关闭)→ 引用条 → 待发送缩略图 → 输入框 → 发送行。 return '
    ' + '' + '' + attachTrayHtml() + '
    ' + '' + '
    ' + '
    ' + 'Ctrl + Enter 发送' + '0 / ' + lengthMax('reply') + '' + '' + '
    '; } function setQuote(shell, ctx, button) { var preview = shell.querySelector('[data-aksrt-ticket-quote-preview]'); if (!preview) return; ctx.quoteId = button ? parseInt(button.getAttribute('data-aksrt-ticket-quote'), 10) || 0 : 0; if (!ctx.quoteId) { preview.hidden = true; preview.innerHTML = ''; return; } preview.innerHTML = icon('fa-reply') + '回复 ' + escapeHtml(button.getAttribute('data-name')) + ':' + escapeHtml(button.getAttribute('data-excerpt')) + '' + ''; preview.hidden = false; var textarea = shell.querySelector('[data-aksrt-ticket-reply-form] textarea'); if (textarea) textarea.focus(); } 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, shell, ctx); var close = event.target.closest('[data-aksrt-ticket-close]'); if (close) handleClose(close, shell, ctx); } function submitReply(shell, ctx) { var form = shell.querySelector('[data-aksrt-ticket-reply-form]'); var button = form && form.querySelector('[data-aksrt-ticket-submit]'); if (!form || !button || button.disabled) return; var textarea = form.querySelector('textarea'); var content = (textarea.value || '').trim(); if (!content) return markInvalid(textarea, '请填写回复内容'); if (isUploading(form)) return notyf('图片仍在上传,请稍候', 'danger'); setBusy(button, true, '发送中…'); runCaptcha('ticket_reply').then(function () { return sendReply(ctx.ticket, content, ctx.quoteId, collectAttachments(form)); }).then(function () { notyf('回复已发送', 'success'); refreshPanel(); return fetchDetail(shell, ctx.ticket.id, { scrollEnd: true }); }).catch(function (error) { if (error && error.cancelled) return; notyf(failMessage(error, '回复提交失败'), 'danger'); }).then(function () { setBusy(button, false); }); } function sendReply(ticket, content, quoteReplyId, attachment) { return request('/tickets/' + ticket.id + '/reply', { method: 'POST', body: { content: content, quote_reply_id: quoteReplyId || 0, attachment: attachment }, }).then(expectSuccess); } /* 催单冷却:服务端下发剩余秒数(详情 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; 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'); // 重新拉取详情,让时间线出现本次的「用户催单」系统记录。 return fetchDetail(shell, ctx.ticket.id, { scrollEnd: true }); }).catch(function (error) { 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: 'POST' }).then(expectSuccess); } // 两步确认:首次点击变为「再次点击确认」,超时自动复原,替代阻塞式 window.confirm。 function handleClose(button, shell, ctx) { if (button.disabled) return; var label = button.querySelector('span'); if (!button.classList.contains('is-confirming')) { button.classList.add('is-confirming'); if (label) label.textContent = '再次点击确认关闭'; ctx.closeTimer = window.setTimeout(function () { button.classList.remove('is-confirming'); if (label) label.textContent = '关闭工单'; }, CLOSE_CONFIRM_MS); return; } window.clearTimeout(ctx.closeTimer); button.classList.remove('is-confirming'); setBusy(button, true, '关闭中…'); sendClose(ctx.ticket).then(function () { notyf('工单已关闭', 'success'); refreshPanel(); return fetchDetail(shell, ctx.ticket.id); }).catch(function (error) { setBusy(button, false); notyf(failMessage(error, '关闭失败'), 'danger'); }); } function sendClose(ticket) { return request('/tickets/' + ticket.id + '/close', { method: 'POST' }).then(expectSuccess); } /* ------------------------------------------------------------------ * * 新建工单弹窗 * ------------------------------------------------------------------ */ function loadMeta() { if (state.meta) return Promise.resolve(state.meta); return request('/tickets/meta').then(expectSuccess).then(function (result) { state.meta = result.data || {}; return state.meta; }); } function openCreate() { var shell = openModal({ title: '提交工单', className: 'aksrt-ticket-modal aksrt-ticket-create-modal', kind: 'ticket-create' }); if (!shell) return; loadMeta().then(function (meta) { if (!alive(shell)) return; if (!(meta.types || []).length) { return modalError(shell, '暂无可提交的工单部门,请稍后再试。'); } setModalContent(shell, buildCreateHtml(meta)); bindCreate(shell); var title = shell.querySelector('[name="title"]'); if (title) title.focus(); }).catch(function (error) { if (alive(shell)) modalError(shell, failMessage(error, '表单加载失败,请重试')); }); } function field(label, control, extra) { return '
    ' + label + control + (extra || '') + '
    '; } function buildCreateHtml(meta) { var types = meta.types || []; var orders = meta.orders || []; var html = '
    '; html += field( '', '', '0 / ' + lengthMax('title') + '' ); html += '
    ' + '工单部门
    ' + types.map(function (type) { return ''; }).join('') + '
    '; if (orders.length) { html += field( '', '' ); } html += field( '', '', '0 / ' + lengthMax('content') + '' ); if (attachmentEnabled()) { html += '
    图片附件可选' + attachTrayHtml() + '
    ' + attachButtonHtml() + '

    支持 JPG / PNG / GIF / WebP,可直接粘贴截图,最多 ' + attachmentMax() + ' 张

    '; } html += '
    ' + '' + '
    '; return html + '
    '; } function bindCreate(shell) { var form = shell.querySelector('[data-aksrt-ticket-create-form]'); if (!form) return; bindShell(shell, function (event) { var view = event.target.closest('[data-aksrt-ticket-view]'); if (view) openDetail(view.getAttribute('data-aksrt-ticket-view')); }); form.addEventListener('submit', function (event) { event.preventDefault(); submitCreate(shell, form); }); } function submitCreate(shell, form) { var button = form.querySelector('[data-aksrt-ticket-submit]'); if (!button || button.disabled) return; var titleField = form.querySelector('[name="title"]'); var contentField = form.querySelector('[name="content"]'); var typeField = form.querySelector('[name="ticket_type_id"]:checked'); var orderField = form.querySelector('[name="order_number"]'); var title = (titleField.value || '').trim(); var content = (contentField.value || '').trim(); if (!title) return markInvalid(titleField, '请填写工单标题'); if (!typeField) return markInvalid(form.querySelector('[data-aksrt-ticket-types]'), '请选择工单部门'); if (!content) return markInvalid(contentField, '请填写问题描述'); if (isUploading(form)) return notyf('图片仍在上传,请稍候', 'danger'); setBusy(button, true, '提交中…'); runCaptcha('ticket_create').then(function () { return request('/tickets', { method: 'POST', body: { title: title, ticket_type_id: parseInt(typeField.value, 10), content: content, order_number: orderField ? orderField.value : '', attachment: collectAttachments(form), }, }).then(expectSuccess); }).then(function (result) { state.page = 1; refreshPanel(); if (alive(shell)) renderCreated(shell, result.data && result.data.id); }).catch(function (error) { setBusy(button, false); if (error && error.cancelled) return; notyf(failMessage(error, '工单提交失败'), 'danger'); }); } // 成功页:承接后台「提交后显示平台公告」(notice_text 已经 wp_kses_post 过滤),并给出下一步。 function renderCreated(shell, id) { var notice = (state.meta && state.meta.notice) || settings().notice || {}; var html = '
    ' + '' + icon('fa-check') + '' + '

    工单已提交

    ' + '

    我们会尽快处理,处理进度可随时在工单中心查看。

    ' + (notice.open && notice.text ? '
    ' + notice.text + '
    ' : '') + '
    ' + '' + (id ? '' : '') + '
    '; setModalContent(shell, html); if (!id) return; var view = shell.querySelector('[data-aksrt-ticket-view]'); if (view) view.focus(); } /* ------------------------------------------------------------------ * * 面板初始化 * ------------------------------------------------------------------ */ function initPanel() { var root = panel(); if (!root || root.dataset.aksrtTicketReady === '1') return; root.dataset.aksrtTicketReady = '1'; root.addEventListener('click', function (event) { var target = event.target; var view = target.closest('[data-aksrt-ticket-view]'); if (view) return openDetail(view.getAttribute('data-aksrt-ticket-view')); var stat = target.closest('[data-aksrt-ticket-stat]'); if (stat) { var key = stat.getAttribute('data-aksrt-ticket-stat') || ''; return applyFilters({ status: key === state.status ? '' : key }); } var page = target.closest('[data-aksrt-ticket-page]'); if (page) { var next = parseInt(page.getAttribute('data-aksrt-ticket-page'), 10) || 1; if (page.disabled || next < 1 || next > state.totalPages || next === state.page) return; state.page = next; loadList(); return scrollPanelIntoView(); } if (target.closest('[data-aksrt-ticket-reset]')) return resetFilters(); if (target.closest('[data-aksrt-ticket-retry]')) return refreshPanel(); if (target.closest('[data-aksrt-ticket-create]')) openCreate(); }); var search = root.querySelector('[data-aksrt-tickets-search]'); if (search) { var input = search.querySelector('input'); var timer = 0; var commit = function () { window.clearTimeout(timer); var keywords = (input.value || '').trim(); if (keywords !== state.keywords) applyFilters({ keywords: keywords }); }; search.addEventListener('submit', function (event) { event.preventDefault(); commit(); }); input.addEventListener('input', function (event) { if (event.isComposing) return; window.clearTimeout(timer); timer = window.setTimeout(commit, SEARCH_DEBOUNCE); }); input.addEventListener('compositionend', function () { window.clearTimeout(timer); timer = window.setTimeout(commit, SEARCH_DEBOUNCE); }); } root.querySelectorAll('[data-aksrt-ticket-filter]').forEach(function (select) { select.addEventListener('change', function () { var key = select.getAttribute('data-aksrt-ticket-filter'); applyFilters(key === 'status' ? { status: select.value } : { type: select.value }); }); }); refreshPanel(); } function boot() { initPanel(); // 面板是 AJAX 延迟加载的:user-center.js 每次切 Tab 都会触发 auto_fun。 if (window.jQuery) { window.jQuery(document).off('auto_fun.aksrt-ticket').on('auto_fun.aksrt-ticket', initPanel); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();