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

597 lines
22 KiB
HTML
Raw Normal View History

2025-11-11 21:39:12 +08:00
{% extends "base.html" %}
{% block title %}工单管理 - 软件授权管理系统{% endblock %}
{% block page_title %}工单管理{% endblock %}
{% block page_actions %}
2025-11-12 15:11:05 +08:00
<button type="button" class="btn btn-primary" id="create-ticket-btn" data-bs-toggle="modal" data-bs-target="#createTicketModal">
2025-11-11 21:39:12 +08:00
<i class="fas fa-plus me-2"></i>
创建工单
</button>
{% endblock %}
{% block content %}
2025-11-12 15:11:05 +08:00
<!-- 搜索表单 -->
2025-11-11 21:39:12 +08:00
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-3">
2025-11-12 15:11:05 +08:00
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="工单标题">
2025-11-11 21:39:12 +08:00
</div>
<div class="col-md-2">
2025-11-12 15:11:05 +08:00
<label for="search-status" class="form-label">状态</label>
2025-11-11 21:39:12 +08:00
<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">
2025-11-12 15:11:05 +08:00
<label for="search-priority" class="form-label">优先级</label>
2025-11-11 21:39:12 +08:00
<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">
2025-11-12 15:11:05 +08:00
<label for="search-product" class="form-label">产品</label>
<input type="text" class="form-control" id="search-product" placeholder="产品ID">
2025-11-11 21:39:12 +08:00
</div>
2025-11-12 15:11:05 +08:00
<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>
2025-11-11 21:39:12 +08:00
</div>
</form>
</div>
</div>
2025-11-12 15:11:05 +08:00
<!-- 批量操作栏 -->
<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>
2025-11-11 21:39:12 +08:00
<!-- 工单列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
2025-11-12 15:11:05 +08:00
<thead class="table-light">
2025-11-11 21:39:12 +08:00
<tr>
2025-11-12 15:11:05 +08:00
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
2025-11-11 21:39:12 +08:00
<th>工单ID</th>
<th>标题</th>
<th>产品</th>
<th>优先级</th>
<th>状态</th>
<th>创建时间</th>
2025-11-12 15:11:05 +08:00
<th>更新时间</th>
2025-11-11 21:39:12 +08:00
<th>操作</th>
</tr>
</thead>
<tbody id="ticket-list">
<tr>
2025-11-12 15:11:05 +08:00
<td colspan="9" class="text-center text-muted">加载中...</td>
2025-11-11 21:39:12 +08:00
</tr>
</tbody>
</table>
</div>
2025-11-12 15:11:05 +08:00
2025-11-11 21:39:12 +08:00
<!-- 分页 -->
2025-11-12 15:11:05 +08:00
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
2025-11-11 21:39:12 +08:00
</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>
2025-11-12 15:11:05 +08:00
<input type="text" class="form-control" id="ticket_title" required>
2025-11-11 21:39:12 +08:00
</div>
2025-11-12 15:11:05 +08:00
<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>
2025-11-11 21:39:12 +08:00
</div>
<div class="mb-3">
<label for="ticket_content" class="form-label">详细描述 *</label>
2025-11-12 15:11:05 +08:00
<textarea class="form-control" id="ticket_content" rows="5" required></textarea>
2025-11-11 21:39:12 +08:00
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
2025-11-12 15:11:05 +08:00
<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>
2025-11-11 21:39:12 +08:00
</div>
</div>
</div>
</div>
{% endblock %}
<script>
let currentPage = 1;
2025-11-12 15:11:05 +08:00
let batchUpdateStatus = null; // 用于存储批量更新的状态
2025-11-11 21:39:12 +08:00
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
loadTickets();
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
// 搜索表单
document.getElementById('search-form').addEventListener('submit', function(e) {
e.preventDefault();
currentPage = 1;
loadTickets();
});
2025-11-12 15:11:05 +08:00
// 重置搜索
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();
});
2025-11-11 21:39:12 +08:00
// 创建工单按钮
2025-11-12 15:11:05 +08:00
document.getElementById('submit-create-ticket').addEventListener('click', function() {
2025-11-11 21:39:12 +08:00
createTicket();
});
2025-11-12 15:11:05 +08:00
// 全选/取消全选
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();
});
2025-11-11 21:39:12 +08:00
}
// 加载工单列表
function loadTickets(page = 1) {
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);
2025-11-12 15:11:05 +08:00
if (product) params.append('product_id', product);
2025-11-11 21:39:12 +08:00
apiRequest(`/api/v1/tickets?${params}`)
.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) {
2025-11-12 15:11:05 +08:00
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>';
2025-11-11 21:39:12 +08:00
return;
}
tbody.innerHTML = tickets.map(ticket => `
<tr>
2025-11-12 15:11:05 +08:00
<td>
<input type="checkbox" class="ticket-checkbox" data-ticket-id="${ticket.ticket_id}">
</td>
2025-11-11 21:39:12 +08:00
<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);
});
});
2025-11-12 15:11:05 +08:00
// 绑定复选框事件
document.querySelectorAll('.ticket-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
2025-11-11 21:39:12 +08:00
}
// 获取优先级徽章
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');
}
2025-11-12 15:11:05 +08:00
// 更新批量操作按钮状态
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>