311 lines
11 KiB
HTML
311 lines
11 KiB
HTML
|
|
{% 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>
|
||
|
|
{{ getPriorityBadge(ticket.priority) }}
|
||
|
|
</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">{{ formatDate(ticket.create_time) }}</dd>
|
||
|
|
|
||
|
|
<dt class="col-sm-2">最后更新:</dt>
|
||
|
|
<dd class="col-sm-10">{{ ticket.update_time or '-' }}</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|safe }}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 回复列表 -->
|
||
|
|
<div class="card shadow mb-4">
|
||
|
|
<div class="card-header">
|
||
|
|
<h6 class="mb-0">回复记录 ({{ ticket.replies|length }})</h6>
|
||
|
|
</div>
|
||
|
|
<div class="card-body">
|
||
|
|
{% if ticket.replies %}
|
||
|
|
{% for reply in ticket.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">{{ formatDate(reply.create_time) }}</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>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if ticket.status in [0, 1] %}
|
||
|
|
<button type="button" class="btn btn-warning" id="close-ticket-btn">
|
||
|
|
<i class="fas fa-times me-2"></i>
|
||
|
|
关闭工单
|
||
|
|
</button>
|
||
|
|
{% endif %}
|
||
|
|
|
||
|
|
{% if 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();
|
||
|
|
});
|
||
|
|
|
||
|
|
// 标记为已解决
|
||
|
|
document.getElementById('resolve-btn').addEventListener('click', function() {
|
||
|
|
updateTicketStatus(2); // 已解决
|
||
|
|
});
|
||
|
|
|
||
|
|
// 开始处理
|
||
|
|
if (document.getElementById('start-process-btn')) {
|
||
|
|
document.getElementById('start-process-btn').addEventListener('click', function() {
|
||
|
|
updateTicketStatus(1); // 处理中
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 关闭工单
|
||
|
|
document.getElementById('close-ticket-btn').addEventListener('click', function() {
|
||
|
|
if (confirm('确定要关闭该工单吗?')) {
|
||
|
|
updateTicketStatus(3); // 已关闭
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 重新打开工单
|
||
|
|
if (document.getElementById('reopen-ticket-btn')) {
|
||
|
|
document.getElementById('reopen-ticket-btn').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 %}
|