Kamixitong/app/web/templates/ticket/list.html

626 lines
22 KiB
HTML
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.

{% extends "base.html" %}
{% block title %}工单管理 - 软件授权管理系统{% endblock %}
{% block page_title %}工单管理{% endblock %}
{% block page_actions %}
<button type="button" class="btn btn-primary" id="create-ticket-btn" data-bs-toggle="modal" data-bs-target="#createTicketModal">
<i class="fas fa-plus me-2"></i>
创建工单
</button>
{% endblock %}
{% block content %}
<!-- 搜索表单 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-3">
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="工单标题">
</div>
<div class="col-md-2">
<label for="search-status" class="form-label">状态</label>
<select class="form-select" id="search-status">
<option value="">全部状态</option>
<option value="0">待处理</option>
<option value="1">处理中</option>
<option value="2">已解决</option>
<option value="3">已关闭</option>
</select>
</div>
<div class="col-md-2">
<label for="search-priority" class="form-label">优先级</label>
<select class="form-select" id="search-priority">
<option value="">全部优先级</option>
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
</select>
</div>
<div class="col-md-3">
<label for="search-product" class="form-label">产品</label>
<input type="text" class="form-control" id="search-product" placeholder="产品ID">
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-undo me-2"></i>重置
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 批量操作栏 -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="btn-group btn-group-sm" id="batch-status-btn" style="display: none;">
<button type="button" class="btn btn-outline-secondary" id="batch-pending-btn">
<i class="fas fa-clock me-1"></i>批量待处理
</button>
<button type="button" class="btn btn-outline-info" id="batch-processing-btn">
<i class="fas fa-cog me-1"></i>批量处理中
</button>
<button type="button" class="btn btn-outline-success" id="batch-resolved-btn">
<i class="fas fa-check-circle me-1"></i>批量已解决
</button>
<button type="button" class="btn btn-outline-dark" id="batch-closed-btn">
<i class="fas fa-times-circle me-1"></i>批量已关闭
</button>
</div>
</div>
<div class="text-muted small">
<span id="selected-count">已选择 0 项</span>
</div>
</div>
</div>
</div>
<!-- 工单列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
<th>工单ID</th>
<th>标题</th>
<th>产品</th>
<th>优先级</th>
<th>状态</th>
<th>创建时间</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="ticket-list">
<tr>
<td colspan="9" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
</ul>
</nav>
</div>
</div>
<!-- 创建工单模态框 -->
<div class="modal fade" id="createTicketModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">创建工单</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="create-ticket-form">
<div class="mb-3">
<label for="ticket_title" class="form-label">标题 *</label>
<input type="text" class="form-control" id="ticket_title" required>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="ticket_product" class="form-label">产品</label>
<input type="text" class="form-control" id="ticket_product" placeholder="产品ID">
</div>
<div class="col-md-6">
<label for="ticket_priority" class="form-label">优先级</label>
<select class="form-select" id="ticket_priority">
<option value="0"></option>
<option value="1" selected></option>
<option value="2"></option>
</select>
</div>
</div>
<div class="mb-3">
<label for="ticket_content" class="form-label">详细描述 *</label>
<textarea class="form-control" id="ticket_content" rows="5" required></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="submit-create-ticket">创建工单</button>
</div>
</div>
</div>
</div>
<!-- 批量更新状态模态框 -->
<div class="modal fade" id="batchUpdateStatusModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">批量更新工单状态</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>确定要将选中的 <strong id="batch-update-count"></strong> 个工单状态更新为 <strong id="batch-update-status-text"></strong> 吗?</p>
<div class="mb-3">
<label for="batch-update-remark" class="form-label">备注</label>
<textarea class="form-control" id="batch-update-remark" rows="3" placeholder="请输入备注信息"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="confirm-batch-update">确定更新</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
let batchUpdateStatus = null; // 用于存储批量更新的状态
// 页面加载完成后初始化
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('工单列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
if (typeof apiRequest === 'function') {
loadTickets();
initEventListeners();
} else {
console.error('apiRequest函数未定义等待脚本加载...');
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadTickets();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// 初始化事件监听器
function initEventListeners() {
// 搜索表单
document.getElementById('search-form').addEventListener('submit', function(e) {
e.preventDefault();
currentPage = 1;
loadTickets();
});
// 重置搜索
document.getElementById('reset-search').addEventListener('click', function() {
document.getElementById('search-keyword').value = '';
document.getElementById('search-status').value = '';
document.getElementById('search-priority').value = '';
document.getElementById('search-product').value = '';
currentPage = 1;
loadTickets();
});
// 创建工单按钮
document.getElementById('submit-create-ticket').addEventListener('click', function() {
createTicket();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.ticket-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量设为待处理
document.getElementById('batch-pending-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(0, '待处理');
});
// 批量设为处理中
document.getElementById('batch-processing-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(1, '处理中');
});
// 批量设为已解决
document.getElementById('batch-resolved-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(2, '已解决');
});
// 批量设为已关闭
document.getElementById('batch-closed-btn').addEventListener('click', function(e) {
e.preventDefault();
showBatchUpdateStatusModal(3, '已关闭');
});
// 确认批量更新
document.getElementById('confirm-batch-update').addEventListener('click', function() {
batchUpdateTicketStatus();
});
}
// 加载工单列表
function loadTickets(page = 1) {
console.log('loadTickets函数被调用页码:', page);
const params = new URLSearchParams({
page: page,
per_page: 10
});
// 添加搜索参数
const keyword = document.getElementById('search-keyword').value.trim();
const status = document.getElementById('search-status').value;
const priority = document.getElementById('search-priority').value;
const product = document.getElementById('search-product').value.trim();
if (keyword) params.append('keyword', keyword);
if (status) params.append('status', status);
if (priority) params.append('priority', priority);
if (product) params.append('product_id', product);
const apiUrl = `/api/v1/tickets?${params}`;
console.log('准备请求API:', apiUrl);
apiRequest(apiUrl)
.then(data => {
if (data.success) {
renderTicketList(data.data.tickets);
renderPagination(data.data.pagination);
} else {
showNotification(data.message || '加载工单列表失败', 'error');
}
})
.catch(error => {
console.error('Failed to load tickets:', error);
showNotification('加载工单列表失败', 'error');
});
}
// 渲染工单列表
function renderTicketList(tickets) {
const tbody = document.getElementById('ticket-list');
if (tickets.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = tickets.map(ticket => `
<tr>
<td>
<input type="checkbox" class="ticket-checkbox" data-ticket-id="${ticket.ticket_id}">
</td>
<td>
<code>${ticket.ticket_id}</code>
</td>
<td>
<a href="/tickets/${ticket.ticket_id}" class="text-decoration-none">
${ticket.title}
</a>
${ticket.reply_count > 0 ? `<span class="badge bg-primary ms-1">${ticket.reply_count}</span>` : ''}
</td>
<td>
${ticket.product_name || '-'}
</td>
<td>
${getPriorityBadge(ticket.priority)}
</td>
<td>
<span class="badge ${getTicketStatusClass(ticket.status)}">
${getTicketStatusText(ticket.status)}
</span>
</td>
<td>
<small>${formatDate(ticket.create_time)}</small>
</td>
<td>
<small>${ticket.update_time ? formatDate(ticket.update_time) : '-'}</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/tickets/${ticket.ticket_id}" class="btn btn-outline-primary" title="查看">
<i class="fas fa-eye"></i>
</a>
${ticket.status !== 3 ? `
<button type="button" class="btn btn-outline-warning btn-update"
data-ticket-id="${ticket.ticket_id}"
title="更新状态">
<i class="fas fa-edit"></i>
</button>
` : ''}
</div>
</td>
</tr>
`).join('');
// 绑定事件
document.querySelectorAll('.btn-update').forEach(btn => {
btn.addEventListener('click', function() {
const ticketId = this.dataset.ticketId;
updateTicketStatus(ticketId);
});
});
// 绑定复选框事件
document.querySelectorAll('.ticket-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 获取优先级徽章
function getPriorityBadge(priority) {
const priorityMap = {
0: '<span class="badge bg-secondary">低</span>',
1: '<span class="badge bg-warning">中</span>',
2: '<span class="badge bg-danger">高</span>'
};
return priorityMap[priority] || '<span class="badge bg-secondary">未知</span>';
}
// 获取工单状态文本
function getTicketStatusText(status) {
const statusMap = {
0: '待处理',
1: '处理中',
2: '已解决',
3: '已关闭'
};
return statusMap[status] || '未知';
}
// 获取工单状态样式类
function getTicketStatusClass(status) {
const classMap = {
0: 'bg-secondary',
1: 'bg-info',
2: 'bg-success',
3: 'bg-dark'
};
return classMap[status] || 'bg-secondary';
}
// 渲染分页
function renderPagination(pagination) {
const paginationEl = document.getElementById('pagination');
if (pagination.pages <= 1) {
paginationEl.innerHTML = '';
return;
}
let html = '';
// 上一页
if (pagination.has_prev) {
html += `<li class="page-item">
<a class="page-link" href="#" data-page="${pagination.page - 1}">上一页</a>
</li>`;
}
// 页码
const startPage = Math.max(1, pagination.page - 2);
const endPage = Math.min(pagination.pages, pagination.page + 2);
if (startPage > 1) {
html += `<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>`;
if (startPage > 2) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
}
for (let i = startPage; i <= endPage; i++) {
html += `<li class="page-item ${i === pagination.page ? 'active' : ''}">
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>`;
}
if (endPage < pagination.pages) {
if (endPage < pagination.pages - 1) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
html += `<li class="page-item"><a class="page-link" href="#" data-page="${pagination.pages}">${pagination.pages}</a></li>`;
}
// 下一页
if (pagination.has_next) {
html += `<li class="page-item">
<a class="page-link" href="#" data-page="${pagination.page + 1}">下一页</a>
</li>`;
}
paginationEl.innerHTML = html;
// 绑定分页点击事件
paginationEl.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const page = parseInt(this.dataset.page);
if (page && page !== currentPage) {
currentPage = page;
loadTickets(page);
}
});
});
}
// 创建工单
function createTicket() {
// 获取表单数据
const formData = {
title: document.getElementById('ticket_title').value.trim(),
product_id: document.getElementById('ticket_product').value,
priority: parseInt(document.getElementById('ticket_priority').value),
content: document.getElementById('ticket_content').value.trim()
};
// 基础验证
if (!formData.title) {
showNotification('请输入工单标题', 'warning');
return;
}
if (!formData.content) {
showNotification('请输入工单详细描述', 'warning');
return;
}
// 发送请求
apiRequest('/api/v1/tickets', {
method: 'POST',
body: JSON.stringify(formData)
})
.then(data => {
if (data.success) {
showNotification('工单创建成功', 'success');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('createTicketModal'));
modal.hide();
// 重置表单
document.getElementById('create-ticket-form').reset();
// 重新加载工单列表
loadTickets(currentPage);
} else {
showNotification(data.message || '创建失败', 'error');
}
})
.catch(error => {
console.error('Failed to create ticket:', error);
showNotification('网络错误,请稍后重试', 'error');
});
}
// 更新工单状态
function updateTicketStatus(ticketId) {
// 这里可以实现状态更新功能
showNotification('功能开发中...', 'info');
}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.ticket-checkbox:checked').length;
const batchStatusBtn = document.getElementById('batch-status-btn');
if (selectedCount > 0) {
batchStatusBtn.style.display = 'inline-block';
} else {
batchStatusBtn.style.display = 'none';
}
// 更新选中数量显示
document.getElementById('selected-count').textContent = `已选择 ${selectedCount}`;
}
// 显示批量更新状态模态框
function showBatchUpdateStatusModal(status, statusText) {
const selectedCount = document.querySelectorAll('.ticket-checkbox:checked').length;
if (selectedCount === 0) {
showNotification('请至少选择一个工单', 'warning');
return;
}
batchUpdateStatus = status;
document.getElementById('batch-update-count').textContent = selectedCount;
document.getElementById('batch-update-status-text').textContent = statusText;
document.getElementById('batch-update-remark').value = '';
const modal = new bootstrap.Modal(document.getElementById('batchUpdateStatusModal'));
modal.show();
}
// 批量更新工单状态
function batchUpdateTicketStatus() {
const selectedCheckboxes = document.querySelectorAll('.ticket-checkbox:checked');
const ticketIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.dataset.ticketId));
const remark = document.getElementById('batch-update-remark').value.trim();
if (ticketIds.length === 0) {
showNotification('请至少选择一个工单', 'warning');
return;
}
if (batchUpdateStatus === null) {
showNotification('请选择要更新的状态', 'warning');
return;
}
apiRequest('/api/v1/tickets/batch/status', {
method: 'PUT',
body: JSON.stringify({
ticket_ids: ticketIds,
status: batchUpdateStatus,
remark: remark
})
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadTickets(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchUpdateStatusModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
console.error('Failed to batch update ticket status:', error);
showNotification('批量更新状态失败', 'error');
});
}
</script>
{% endblock %}