407 lines
14 KiB
HTML
407 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}工单管理 - 软件授权管理系统{% endblock %}
|
|
|
|
{% block page_title %}工单管理{% endblock %}
|
|
|
|
{% block page_actions %}
|
|
<button type="button" class="btn btn-primary" 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">
|
|
<input type="text" class="form-control" id="search-keyword" placeholder="搜索工单标题或内容...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<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">
|
|
<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">
|
|
<input type="text" class="form-control" id="search-product" placeholder="产品ID或名称...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-outline-primary">
|
|
<i class="fas fa-search me-2"></i>
|
|
搜索
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 工单列表 -->
|
|
<div class="card shadow">
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<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="8" class="text-center text-muted">
|
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
加载中...
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 分页 -->
|
|
<nav aria-label="工单列表分页">
|
|
<ul class="pagination justify-content-center" id="pagination">
|
|
<!-- 分页将通过JavaScript动态生成 -->
|
|
</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" name="title" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="ticket_product" class="form-label">关联产品</label>
|
|
<select class="form-select" id="ticket_product" name="product_id">
|
|
<option value="">无关联产品</option>
|
|
{% for product in products %}
|
|
<option value="{{ product.product_id }}">{{ product.product_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="ticket_priority" class="form-label">优先级 *</label>
|
|
<select class="form-select" id="ticket_priority" name="priority" required>
|
|
<option value="0">低</option>
|
|
<option value="1" selected>中</option>
|
|
<option value="2">高</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="ticket_content" class="form-label">详细描述 *</label>
|
|
<textarea class="form-control" id="ticket_content" name="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="create-ticket-btn">创建</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
let currentPage = 1;
|
|
|
|
// 页面加载完成后初始化
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadTickets();
|
|
initEventListeners();
|
|
});
|
|
|
|
// 初始化事件监听器
|
|
function initEventListeners() {
|
|
// 搜索表单
|
|
document.getElementById('search-form').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
currentPage = 1;
|
|
loadTickets();
|
|
});
|
|
|
|
// 创建工单按钮
|
|
document.getElementById('create-ticket-btn').addEventListener('click', function() {
|
|
createTicket();
|
|
});
|
|
}
|
|
|
|
// 加载工单列表
|
|
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);
|
|
if (product) params.append('product', product);
|
|
|
|
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) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = tickets.map(ticket => `
|
|
<tr>
|
|
<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);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 获取优先级徽章
|
|
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');
|
|
}
|
|
</script>
|
|
{% endblock %} |