第一次提交
This commit is contained in:
333
app/web/templates/ticket/detail.html
Normal file
333
app/web/templates/ticket/detail.html
Normal file
@@ -0,0 +1,333 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}工单详情 - 软件授权管理系统{% endblock %}
|
||||
|
||||
{% block page_title %}工单详情{% endblock %}
|
||||
|
||||
{% block page_actions %}
|
||||
<a href="{{ url_for('web.tickets') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
返回列表
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- 工单基本信息 -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">工单信息</h6>
|
||||
<div>
|
||||
<span class="badge {{ 'bg-secondary' if ticket.status == 0 else 'bg-info' if ticket.status == 1 else 'bg-success' if ticket.status == 2 else 'bg-dark' }}">
|
||||
{{ '待处理' if ticket.status == 0 else '处理中' if ticket.status == 1 else '已解决' if ticket.status == 2 else '已关闭' }}
|
||||
</span>
|
||||
{{ get_priority_badge(ticket.priority)|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-2">工单ID:</dt>
|
||||
<dd class="col-sm-10"><code>{{ ticket.ticket_id }}</code></dd>
|
||||
|
||||
<dt class="col-sm-2">标题:</dt>
|
||||
<dd class="col-sm-10">{{ ticket.title }}</dd>
|
||||
|
||||
<dt class="col-sm-2">关联产品:</dt>
|
||||
<dd class="col-sm-10">
|
||||
{% if ticket.product %}
|
||||
{{ ticket.product.product_name }} ({{ ticket.product.product_id }})
|
||||
{% else %}
|
||||
无关联产品
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-2">创建时间:</dt>
|
||||
<dd class="col-sm-10">{{ ticket.create_time | format_date }}</dd>
|
||||
|
||||
<dt class="col-sm-2">最后更新:</dt>
|
||||
<dd class="col-sm-10">{{ ticket.update_time | format_date }}</dd>
|
||||
|
||||
<dt class="col-sm-2">创建人:</dt>
|
||||
<dd class="col-sm-10">{{ ticket.creator or '系统用户' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单内容 -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">工单内容</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="bg-light p-3 rounded">
|
||||
{{ ticket.content|e|nl2br }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复列表 -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">回复记录 ({{ ticket.replies.count() }})</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% set replies = ticket.replies.order_by('create_time').all() %}
|
||||
{% if replies|length > 0 %}
|
||||
{% for reply in replies %}
|
||||
<div class="border-bottom pb-3 mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<strong>{{ reply.creator or '支持团队' }}</strong>
|
||||
<small class="text-muted">{{ reply.create_time | format_date }}</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{{ reply.content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted text-center">暂无回复</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复表单 -->
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">添加回复</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="reply-form">
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control" id="reply_content" rows="4" placeholder="请输入回复内容..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="reply-btn">
|
||||
<i class="fas fa-paper-plane me-2"></i>
|
||||
<span id="reply-text">提交回复</span>
|
||||
</button>
|
||||
{% if ticket.status != 3 %}
|
||||
<button type="button" class="btn btn-success ms-2" id="resolve-btn">
|
||||
<i class="fas fa-check me-2"></i>
|
||||
标记为已解决
|
||||
</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- 操作面板 -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">操作</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if ticket.status == 0 %}
|
||||
<!-- 待处理状态 -->
|
||||
<button type="button" class="btn btn-info" id="start-process-btn">
|
||||
<i class="fas fa-play me-2"></i>
|
||||
开始处理
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" id="close-ticket-btn">
|
||||
<i class="fas fa-times me-2"></i>
|
||||
关闭工单
|
||||
</button>
|
||||
{% elif ticket.status == 1 %}
|
||||
<!-- 处理中状态 -->
|
||||
<button type="button" class="btn btn-success" id="resolve-btn">
|
||||
<i class="fas fa-check me-2"></i>
|
||||
标记为已解决
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" id="close-ticket-btn">
|
||||
<i class="fas fa-times me-2"></i>
|
||||
关闭工单
|
||||
</button>
|
||||
{% elif ticket.status == 2 %}
|
||||
<!-- 已解决状态 -->
|
||||
<button type="button" class="btn btn-success" id="reopen-ticket-btn">
|
||||
<i class="fas fa-redo me-2"></i>
|
||||
重新打开
|
||||
</button>
|
||||
{% elif ticket.status == 3 %}
|
||||
<!-- 已关闭状态 -->
|
||||
<button type="button" class="btn btn-success" id="reopen-ticket-btn">
|
||||
<i class="fas fa-redo me-2"></i>
|
||||
重新打开
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单信息 -->
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">工单信息</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<small>工单状态会影响用户的使用体验</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<small>请及时回复用户的问题</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<small>工单关闭后仍可重新打开</small>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 获取优先级徽章
|
||||
function getPriorityBadge(priority) {
|
||||
const priorityMap = {
|
||||
0: '<span class="badge bg-secondary ms-1">低</span>',
|
||||
1: '<span class="badge bg-warning ms-1">中</span>',
|
||||
2: '<span class="badge bg-danger ms-1">高</span>'
|
||||
};
|
||||
return priorityMap[priority] || '<span class="badge bg-secondary ms-1">未知</span>';
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
// 初始化事件监听器
|
||||
function initEventListeners() {
|
||||
// 回复表单
|
||||
document.getElementById('reply-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
addReply();
|
||||
});
|
||||
|
||||
// 开始处理按钮
|
||||
const startProcessBtn = document.getElementById('start-process-btn');
|
||||
if (startProcessBtn) {
|
||||
startProcessBtn.addEventListener('click', function() {
|
||||
updateTicketStatus(1); // 处理中
|
||||
});
|
||||
}
|
||||
|
||||
// 标记为已解决按钮
|
||||
const resolveBtn = document.getElementById('resolve-btn');
|
||||
if (resolveBtn) {
|
||||
resolveBtn.addEventListener('click', function() {
|
||||
updateTicketStatus(2); // 已解决
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭工单按钮
|
||||
const closeTicketBtn = document.getElementById('close-ticket-btn');
|
||||
if (closeTicketBtn) {
|
||||
closeTicketBtn.addEventListener('click', function() {
|
||||
if (confirm('确定要关闭该工单吗?')) {
|
||||
updateTicketStatus(3); // 已关闭
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 重新打开工单按钮
|
||||
const reopenTicketBtn = document.getElementById('reopen-ticket-btn');
|
||||
if (reopenTicketBtn) {
|
||||
reopenTicketBtn.addEventListener('click', function() {
|
||||
updateTicketStatus(1); // 处理中
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加回复
|
||||
function addReply() {
|
||||
const replyBtn = document.getElementById('reply-btn');
|
||||
const replyText = document.getElementById('reply-text');
|
||||
const content = document.getElementById('reply_content').value.trim();
|
||||
|
||||
if (!content) {
|
||||
showNotification('请输入回复内容', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
replyBtn.disabled = true;
|
||||
replyText.textContent = '提交中...';
|
||||
|
||||
// 发送请求
|
||||
apiRequest(`/api/v1/tickets/{{ ticket.ticket_id }}/replies`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content: content })
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('回复成功', 'success');
|
||||
// 清空输入框
|
||||
document.getElementById('reply_content').value = '';
|
||||
// 重新加载页面以显示新回复
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showNotification(data.message || '回复失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to add reply:', error);
|
||||
showNotification('网络错误,请稍后重试', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// 恢复按钮状态
|
||||
replyBtn.disabled = false;
|
||||
replyText.textContent = '提交回复';
|
||||
});
|
||||
}
|
||||
|
||||
// 更新工单状态
|
||||
function updateTicketStatus(status) {
|
||||
const statusText = {
|
||||
0: '待处理',
|
||||
1: '处理中',
|
||||
2: '已解决',
|
||||
3: '已关闭'
|
||||
};
|
||||
|
||||
apiRequest(`/api/v1/tickets/{{ ticket.ticket_id }}/status`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ status: status })
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(`工单状态已更新为${statusText[status]}`, 'success');
|
||||
// 重新加载页面以显示新状态
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showNotification(data.message || '状态更新失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to update ticket status:', error);
|
||||
showNotification('网络错误,请稍后重试', 'error');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
627
app/web/templates/ticket/list.html
Normal file
627
app/web/templates/ticket/list.html
Normal file
@@ -0,0 +1,627 @@
|
||||
{% 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 URL:', apiUrl);
|
||||
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="${escapeHtml(ticket.ticket_id)}">
|
||||
</td>
|
||||
<td>
|
||||
<code>${escapeHtml(ticket.ticket_id)}</code>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/tickets/${escapeHtml(ticket.ticket_id)}" class="text-decoration-none">
|
||||
${escapeHtml(ticket.title)}
|
||||
</a>
|
||||
${ticket.reply_count > 0 ? `<span class="badge bg-primary ms-1">${ticket.reply_count}</span>` : ''}
|
||||
</td>
|
||||
<td>
|
||||
${escapeHtml(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 %}
|
||||
Reference in New Issue
Block a user