Files
links/patches/public/assets/js/backend/split/ticket.js
T
2026-07-02 03:39:29 +08:00

647 lines
30 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () {
Table.api.init({
extend: {
index_url: 'split.ticket/index' + location.search,
add_url: 'split.ticket/add',
edit_url: 'split.ticket/edit',
del_url: 'split.ticket/del',
multi_url: 'split.ticket/multi',
table: 'split_ticket',
}
});
var table = $("#table");
var searchSelect = Controller.api.searchSelectColumn;
var searchSelectMeta = Controller.api.searchSelectMeta;
table.bootstrapTable({
url: $.fn.bootstrapTable.defaults.extend.index_url,
pk: 'id',
sortName: 'id',
sortOrder: 'desc',
fixedColumns: true,
fixedRightNumber: 1,
searchFormVisible: true,
columns: [
[
{checkbox: true},
searchSelect('split_link_id', __('Split_link_id'), Config.splitLinkFilterList || {}, true),
{
field: 'ticket_type',
title: __('Ticket_type'),
searchList: Config.ticketTypeList,
operate: false,
formatter: Controller.api.formatter.ticketTypePlain
},
{field: 'ticket_name', title: __('Ticket_name'), operate: 'LIKE'},
{
field: 'link_code_text',
title: __('Split_link_id'),
operate: false,
formatter: Controller.api.formatter.splitLinkCode
},
{
field: 'ticket_url',
title: __('Ticket_url'),
operate: 'LIKE',
formatter: Controller.api.formatter.ticketUrlLink
},
{
field: 'start_time_text',
title: __('Start_time'),
operate: 'RANGE',
addclass: 'datetimerange',
autocomplete: false,
sortable: true
},
{
field: 'end_time_text',
title: __('End_time'),
operate: 'RANGE',
addclass: 'datetimerange',
autocomplete: false,
sortable: true
},
{field: 'order_limit', title: __('Order_limit'), operate: false},
{field: 'assign_ratio', title: __('Assign_ratio'), operate: false},
{field: 'ticket_total', title: __('Ticket_total'), operate: false, sortable: true},
{field: 'complete_count', title: __('Complete_count'), operate: false, sortable: true},
{
field: 'ticket_progress_text',
title: __('Ticket_progress'),
operate: false,
formatter: Table.api.formatter.content
},
{
field: 'inbound_ratio_text',
title: __('Inbound_ratio'),
operate: false,
formatter: Table.api.formatter.content
},
{
field: 'speed_per_hour',
title: __('Speed_per_hour'),
operate: false,
formatter: Controller.api.formatter.speedPerHour
},
{
field: 'number_count',
title: __('Number_count'),
operate: false,
formatter: Controller.api.formatter.numberCount
},
{
field: 'sync_display_text',
title: __('Sync_status'),
operate: false,
formatter: Controller.api.formatter.syncDisplay
},
{
field: 'sync_success_text',
title: __('Sync success time'),
operate: false,
formatter: Controller.api.formatter.syncSuccessDisplay
},
$.extend(
{field: 'status', title: __('Status'), searchList: Config.statusList, formatter: Table.api.formatter.toggle, yes: 'normal', no: 'hidden'},
searchSelectMeta(false)
),
{
field: 'createtime',
title: __('Createtime'),
operate: 'RANGE',
addclass: 'datetimerange',
autocomplete: false,
formatter: Table.api.formatter.datetime,
sortable: true
},
{
field: 'operate',
title: __('Operate'),
table: table,
events: Controller.api.events.operate,
formatter: Controller.api.formatter.operate
}
]
]
});
table.on('post-common-search.bs.table', function (e, tbl) {
Controller.api.initCommonSearchSelectpicker(tbl);
});
Table.api.bindevent(table);
Controller.api.syncingTicketIds = [];
window.__splitTicketPendingPostAddSyncIds = window.__splitTicketPendingPostAddSyncIds || [];
table.on('click', '.split-ticket-url-group input, .split-ticket-url-group a', function (e) {
e.stopPropagation();
});
table.on('load-success.bs.table', function (e, res) {
Controller.api.renderSummaryRow(res);
Controller.api.initSyncTooltips(table);
var pendingIds = window.__splitTicketPendingPostAddSyncIds;
if (!pendingIds || !pendingIds.length) {
return;
}
window.__splitTicketPendingPostAddSyncIds = [];
Controller.api.startBackgroundSync(table, pendingIds, false);
});
table.on('check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table', function () {
var ids = Table.api.selectedids(table);
$('.btn-sync').toggleClass('btn-disabled disabled', ids.length === 0);
});
$('.btn-sync').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
var ids = Table.api.selectedids(table);
if (!ids.length) {
Toastr.error(__('Please select at least one record'));
return false;
}
var syncConfirmMsg = (typeof Config.syncConfirmMsg !== 'undefined' && Config.syncConfirmMsg)
? Config.syncConfirmMsg
: '确定同步所选工单吗?任务将在后台执行,完成后自动提示结果。';
var syncBackgroundMsg = (typeof Config.syncBackgroundStartedMsg !== 'undefined' && Config.syncBackgroundStartedMsg)
? Config.syncBackgroundStartedMsg
: '同步任务已在后台执行,请稍候…';
Layer.confirm(syncConfirmMsg, {icon: 3, title: __('Sync_status_btn')}, function (index) {
Layer.close(index);
Toastr.info(syncBackgroundMsg);
Controller.api.startBackgroundSync(table, ids, true);
});
return false;
});
},
add: function () {
Form.api.bindevent($('form[role=form]'), function (data, ret) {
var ticketId = ret.data && ret.data.id ? parseInt(ret.data.id, 10) : 0;
var syncTicketMsg = (typeof Config.syncTicketStartedMsg !== 'undefined' && Config.syncTicketStartedMsg)
? Config.syncTicketStartedMsg
: '正在同步工单';
if (ticketId > 0) {
parent.__splitTicketPendingPostAddSyncIds = parent.__splitTicketPendingPostAddSyncIds || [];
parent.__splitTicketPendingPostAddSyncIds.push(ticketId);
}
if (parent && parent.Toastr) {
parent.Toastr.info(syncTicketMsg);
}
parent.$('.btn-refresh').trigger('click');
if (window.name) {
var layerIndex = parent.Layer.getFrameIndex(window.name);
parent.Layer.close(layerIndex);
}
return false;
});
Controller.api.fixSelectPlaceholder();
Controller.api.bindNumberTypeToggle();
Controller.api.bindEndTimeCheck();
},
edit: function () {
Controller.api.bindevent();
},
api: {
searchSelectColumn: function (field, title, searchList, liveSearch) {
return {
field: field,
title: title,
visible: false,
searchList: searchList || {},
operate: '=',
addclass: 'selectpicker',
extend: Controller.api.searchSelectExtend(liveSearch !== false)
};
},
searchSelectMeta: function (liveSearch) {
return {
addclass: 'selectpicker',
extend: Controller.api.searchSelectExtend(liveSearch !== false)
};
},
searchSelectExtend: function (liveSearch) {
var text = __('Please select');
if (!text || text === 'Please select' || text === 'Please Select') {
text = '请选择';
}
var ext = 'data-none-selected-text="' + text + '" title="' + text + '"';
if (liveSearch) {
ext += ' data-live-search="true"';
}
return ext;
},
initCommonSearchSelectpicker: function (tbl) {
var $form = $('form.form-commonsearch', tbl.$container);
if ($form.length) {
Form.events.selectpicker($form);
}
},
/** @type {number[]} 正在手动同步的工单 ID */
syncingTicketIds: [],
syncPollTimer: null,
syncPollStartedAt: 0,
syncPollGraceMs: 8000,
/**
* 渲染筛选结果汇总行(全量筛选数据,非当前页)
*
* @param {object} res 列表接口响应
*/
renderSummaryRow: function (res) {
var $bar = $('#split-ticket-summary');
if (!$bar.length) {
return;
}
var total = res && res.total ? parseInt(res.total, 10) : 0;
var summary = res && res.summary ? res.summary : null;
if (!summary || total <= 0) {
$bar.addClass('hide');
return;
}
$bar.removeClass('hide');
$bar.find('[data-sum="ticket_total"]').text(summary.ticket_total != null ? summary.ticket_total : 0);
$bar.find('[data-sum="complete_count"]').text(summary.complete_count != null ? summary.complete_count : 0);
$bar.find('[data-sum="ticket_progress_text"]').text(summary.ticket_progress_text || '0.00%');
$bar.find('[data-sum="speed_per_hour"]').text(summary.speed_per_hour != null ? summary.speed_per_hour : '0.00');
},
/**
* 后台同步:标记「同步中」、投递 CLI,并轮询直至锁释放
*
* @param {object} table bootstrapTable 实例
* @param {number[]} ids 工单 ID
* @param {boolean} disableSyncBtn 是否禁用工具栏同步按钮
*/
startBackgroundSync: function (table, ids, disableSyncBtn) {
ids = (ids || []).map(function (id) {
return parseInt(id, 10);
}).filter(function (id) {
return !isNaN(id) && id > 0;
});
if (!ids.length) {
return;
}
Controller.api.markTicketsSyncing(table, ids);
if (disableSyncBtn) {
$('.btn-sync').addClass('btn-disabled disabled');
}
Fast.api.ajax({
url: 'split.ticket/sync',
data: {ids: ids.join(',')},
loading: false
}, function (data, ret) {
var queued = (ret.data && ret.data.queued) ? ret.data.queued : ids;
Controller.api.syncingTicketIds = (queued || []).map(function (id) {
return parseInt(id, 10);
}).filter(function (id) {
return !isNaN(id) && id > 0;
});
if (Controller.api.syncingTicketIds.length) {
Controller.api.startSyncPolling(table);
} else {
Controller.api.finishTicketsSync(table);
}
}, function () {
Controller.api.finishTicketsSync(table);
});
},
/**
* 轮询同步锁状态,全部完成后刷新列表
*/
startSyncPolling: function (table) {
Controller.api.stopSyncPolling();
if (!Controller.api.syncingTicketIds.length) {
return;
}
Controller.api.syncPollStartedAt = Date.now();
Controller.api.syncPollTimer = setInterval(function () {
if (!Controller.api.syncingTicketIds.length) {
Controller.api.stopSyncPolling();
return;
}
Fast.api.ajax({
url: 'split.ticket/syncpolling',
data: {ids: Controller.api.syncingTicketIds.join(',')},
loading: false
}, function (data, ret) {
var still = (ret.data && ret.data.syncing) ? ret.data.syncing : [];
still = (still || []).map(function (id) {
return parseInt(id, 10);
}).filter(function (id) {
return !isNaN(id) && id > 0;
});
var elapsed = Date.now() - (Controller.api.syncPollStartedAt || 0);
var graceMs = Controller.api.syncPollGraceMs || 8000;
if (still.length === 0 && elapsed < graceMs) {
table.bootstrapTable('refresh', {silent: true});
return false;
}
if (still.length === 0) {
Controller.api.stopSyncPolling();
var doneMsg = (typeof Config.syncDoneMsg !== 'undefined' && Config.syncDoneMsg)
? Config.syncDoneMsg
: '同步完成';
Toastr.success(doneMsg);
Controller.api.finishTicketsSync(table);
return false;
}
if (still.join(',') !== Controller.api.syncingTicketIds.join(',')) {
Controller.api.syncingTicketIds = still;
var rowData = table.bootstrapTable('getData');
table.bootstrapTable('load', rowData);
}
table.bootstrapTable('refresh', {silent: true});
return false;
});
}, 3000);
},
stopSyncPolling: function () {
if (Controller.api.syncPollTimer) {
clearInterval(Controller.api.syncPollTimer);
Controller.api.syncPollTimer = null;
}
},
/**
* 将选中工单标记为「同步中」并刷新列表展示(不请求后端)
*/
markTicketsSyncing: function (table, ids) {
Controller.api.syncingTicketIds = (ids || []).map(function (id) {
return parseInt(id, 10);
}).filter(function (id) {
return !isNaN(id) && id > 0;
});
var data = table.bootstrapTable('getData');
table.bootstrapTable('load', data);
},
/**
* 同步结束:清除标记并刷新列表数据
*/
finishTicketsSync: function (table) {
Controller.api.stopSyncPolling();
Controller.api.syncingTicketIds = [];
table.bootstrapTable('refresh', {
silent: true
});
Controller.api.initSyncTooltips(table);
var ids = Table.api.selectedids(table);
$('.btn-sync').toggleClass('btn-disabled disabled', ids.length === 0);
},
/**
* 初始化同步状态列 tooltip(完整异常原因)
*/
initSyncTooltips: function (table) {
var $container = table.closest('.bootstrap-table');
$container.find('[data-sync-tooltip="1"]').tooltip({
container: 'body',
placement: 'auto top',
html: false
});
},
formatter: {
/**
* 操作列:编辑 → 拷贝 → 删除
*/
operate: function (value, row, index) {
var column = this;
var table = this.table;
var options = table.bootstrapTable('getOptions');
var buttons = [];
if (options.extend.edit_url) {
buttons.push($.extend({}, Table.button.edit, {url: options.extend.edit_url}));
}
buttons.push({
name: 'copy',
icon: 'fa fa-copy',
title: __('Copy'),
classname: 'btn btn-xs btn-info btn-copyone',
url: 'javascript:;',
extend: 'data-toggle="tooltip" data-container="body"',
});
if (options.extend.del_url) {
buttons.push($.extend({}, Table.button.del));
}
return Table.api.buttonlink(column, buttons, value, row, index, 'operate');
},
/**
* 工单类型:纯文本展示,无链接/标签样式
*/
ticketTypePlain: function (value, row) {
var text = row.ticket_type_text != null && row.ticket_type_text !== ''
? String(row.ticket_type_text)
: (value != null ? String(value) : '');
if (text === '') {
return '<span class="text-muted">-</span>';
}
return '<span class="split-ticket-type-plain">' + Fast.api.escape(text) + '</span>';
},
/**
* 分流链接:纯文本 + 边框背景标记,不可点击
*/
splitLinkCode: function (value) {
value = value == null ? '' : String(value);
if ($.trim(value) === '') {
return '<span class="text-muted">-</span>';
}
var safe = Fast.api.escape(value);
return '<span class="split-ticket-link-badge" style="display:inline-block;max-width:100%;padding:2px 8px;font-size:12px;line-height:1.5;color:#555;background:#f5f5f5;border:1px solid #ddd;border-radius:3px;word-break:break-all;">'
+ safe + '</span>';
},
/**
* 工单链接:只读 input + 外链图标按钮
*/
ticketUrlLink: function (value) {
value = value == null ? '' : String(value).trim();
if (value === '') {
return '<span class="text-muted">-</span>';
}
var href = value;
if (!/^https?:\/\//i.test(href)) {
href = 'https://' + href;
}
var safeValue = Fast.api.escape(value);
var safeHref = Fast.api.escape(href);
return '<div class="input-group input-group-sm split-ticket-url-group">'
+ '<input type="text" class="form-control input-sm" readonly value="' + safeValue + '" title="' + safeValue + '">'
+ '<span class="input-group-btn">'
+ '<a href="' + safeHref + '" target="_blank" rel="noopener noreferrer" class="btn btn-default btn-sm" title="' + safeValue + '">'
+ '<i class="fa fa-link"></i></a>'
+ '</span></div>';
},
speedPerHour: function (value) {
var num = parseFloat(value);
if (isNaN(num)) {
return '0.00';
}
return num.toFixed(2);
},
numberCount: function (value, row) {
var total = parseInt(value, 10) || 0;
var offline = parseInt(row.number_offline_count, 10) || 0;
var banned = parseInt(row.number_banned_count, 10) || 0;
var tip = __('Number_count_detail').replace('%s', offline).replace('%s', banned);
if (offline > 0 || banned > 0) {
return '<span data-toggle="tooltip" title="' + Fast.api.escape(tip) + '">' + total + '</span>';
}
return String(total);
},
syncDisplay: function (value, row) {
var rowId = parseInt(row.id, 10);
if (Controller.api.syncingTicketIds.indexOf(rowId) !== -1) {
return Controller.api.formatter.syncingDisplayHtml();
}
var text = value || '';
var tooltip = row.sync_tooltip_text ? String(row.sync_tooltip_text).trim() : '';
if (tooltip !== '') {
tooltip = tooltip.replace(/\r?\n/g, '');
}
var hasTooltip = tooltip !== '';
var color = 'danger';
var extraClass = '';
if (row.sync_auto_paused) {
color = 'warning';
extraClass = ' split-ticket-sync-status-paused';
} else if (row.sync_status === 'success') {
color = 'success';
} else if (row.sync_status === 'pending') {
color = 'muted';
} else if (row.sync_status === 'error') {
extraClass = ' split-ticket-sync-status-error';
}
var attrs = '';
if (hasTooltip) {
attrs = ' data-sync-tooltip="1" data-toggle="tooltip" title="' + Fast.api.escape(tooltip) + '"';
}
return '<span class="text-' + color + extraClass + '"' + attrs + '>' + Fast.api.escape(text) + '</span>';
},
syncSuccessDisplay: function (value, row) {
var text = value == null ? '' : String(value).trim();
if (text === '') {
return '<span class="text-muted">-</span>';
}
var colorClass = row.status === 'normal'
? 'split-sync-success-normal'
: 'split-sync-success-stopped';
return '<span class="' + colorClass + '">' + Fast.api.escape(text) + '</span>';
},
syncingDisplayHtml: function () {
var label = (typeof Config.syncInProgressMsg !== 'undefined' && Config.syncInProgressMsg)
? Config.syncInProgressMsg
: '同步中';
return '<span class="split-ticket-sync-pending">'
+ '<i class="fa fa-refresh fa-spin"></i>'
+ '<span class="split-ticket-sync-label">' + Fast.api.escape(label) + '</span>'
+ '</span>';
}
},
events: {
operate: $.extend({}, Table.api.events.operate, {
'click .btn-copyone': function (e, value, row, index) {
e.stopPropagation();
e.preventDefault();
var table = $(this).closest('table');
var options = table.bootstrapTable('getOptions');
var id = row[options.pk];
Fast.api.open(
Fast.api.fixurl('split.ticket/add?copy_id=' + id),
__('Copy'),
$(this).data() || {}
);
}
})
},
bindevent: function () {
Form.api.bindevent($('form[role=form]'));
Controller.api.fixSelectPlaceholder();
Controller.api.bindNumberTypeToggle();
Controller.api.bindEndTimeCheck();
},
/**
* selectpicker 空选项文案改为中文「请选择」
*/
fixSelectPlaceholder: function () {
var text = __('Please select');
if (!text || text === 'Please select' || text === 'Please Select') {
text = '请选择';
}
$('#c-ticket_type, #c-split_link_id').each(function () {
var $el = $(this);
$el.attr({'data-none-selected-text': text, 'title': text});
$el.find('option[value=""]').first().text(text);
if ($el.data('selectpicker')) {
$el.selectpicker('render');
}
});
},
/**
* 号码类型为 custom 时显示自定义输入框
*/
bindNumberTypeToggle: function () {
var $type = $('#c-number_type');
var $wrap = $('.split-number-type-custom');
var $custom = $('#c-number_type_custom');
if (!$type.length) {
return;
}
var toggle = function () {
var val = $type.val();
if (val === 'custom') {
$wrap.removeClass('hide');
$custom.attr('data-rule', 'required');
} else {
$wrap.addClass('hide');
$custom.removeAttr('data-rule');
$custom.val('');
}
if ($custom.data('validator')) {
$custom.trigger('validate');
}
};
$type.on('changed.bs.select change', toggle);
toggle();
},
/**
* 前端预校验:到期时间须晚于开始时间(后端为准)
*/
bindEndTimeCheck: function () {
var $form = $('form[role=form]');
var $start = $('#c-start_time');
var $end = $('#c-end_time');
if (!$start.length || !$end.length) {
return;
}
var parseTs = function (str) {
str = $.trim(str || '');
if (!str) {
return 0;
}
var d = new Date(str.replace(/-/g, '/'));
return isNaN(d.getTime()) ? 0 : Math.floor(d.getTime() / 1000);
};
$form.on('submit', function (e) {
var s = parseTs($start.val());
var en = parseTs($end.val());
if (s > 0 && en > 0 && en <= s) {
e.preventDefault();
e.stopImmediatePropagation();
Layer.msg(__('End time must after start'));
return false;
}
});
}
}
};
return Controller;
});