Files
palladium-monitor/ticket-system/templates/ticket_detail.html
T
AI Agent 26940c4574 feat: initial commit - lightweight ticket system with Flask+SQLite
Features:
- User authentication (login/register)
- Ticket CRUD with status/priority/category
- Rich text description with paste-to-image
- File attachment upload (images/documents)
- Multi-select CC recipients
- Role-based permission control (admin/user)
- In-app notifications
- Statistics dashboard
- Gunicorn production deployment
2026-07-02 15:31:27 +08:00

299 lines
11 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 %}工单 #{{ ticket.id }}{% endblock %}
{% block content %}
<div class="detail-header">
<div>
<h1>#{{ ticket.id }} {{ ticket.title }}</h1>
<div class="detail-meta">
创建 {{ ticket.created_at.strftime('%Y-%m-%d %H:%M') }} &middot;
更新 {{ ticket.updated_at.strftime('%Y-%m-%d %H:%M') }}
</div>
</div>
<div style="display:flex;gap:8px;">
<a href="/ticket/{{ ticket.id }}/edit" class="btn btn-secondary">编辑</a>
{% if ticket.status != 'closed' and ticket.status != 'resolved' %}
<form method="post" action="/ticket/{{ ticket.id }}/close" style="display:inline;">
<button type="submit" class="btn btn-primary" onclick="return confirm('确认关闭?')">关闭</button>
</form>
{% endif %}
<form method="post" action="/ticket/{{ ticket.id }}/delete" style="display:inline;">
<button type="submit" class="btn btn-danger" onclick="return confirm('确认删除?')">删除</button>
</form>
</div>
</div>
<div class="detail-grid">
<!-- 左侧:内容 + 评论 + 附件 -->
<div>
<!-- 描述 -->
<div class="card" style="margin-bottom:16px;">
<h2 style="font-size:15px;margin-bottom:8px;color:var(--text-muted);">描述</h2>
<div style="white-space:pre-wrap;line-height:1.7;">{{ ticket.description | render_desc }}</div>
</div>
<!-- 附件 -->
<div>
<h2 style="font-size:15px;margin-bottom:12px;color:var(--text-muted);">
附件({{ attachments|length }}
</h2>
<!-- 上传区域 -->
<div class="attachment-zone" id="uploadZone"
ondrop="handleDrop(event)"
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)"
onclick="document.getElementById('fileInput').click()">
<input type="file" id="fileInput" multiple accept="*/*"
onchange="handleFileSelect(this.files)">
<div class="az-label">📎 点击选择、拖拽或粘贴(Ctrl+V)上传图片/附件</div>
<div class="az-hint">支持图片、文档、压缩包等,最大 20 MB</div>
</div>
<!-- 附件列表 -->
<div class="attachment-list" id="attachmentList">
{% for att in attachments %}
<div class="attachment-item" data-aid="{{ att.id }}">
{% if att.is_image %}
<img class="att-thumb"
src="/uploads/{{ ticket.id }}/{{ att.filename }}"
alt="{{ att.original_name }}"
onclick="showImage(this.src)" loading="lazy">
{% else %}
<div class="att-icon">
{% if att.content_type == 'application/pdf' %}📄
{% elif att.content_type.startswith('text/') or att.content_type == 'application/json' %}📝
{% elif att.content_type.startswith('application/') %}📦
{% else %}📎{% endif %}
</div>
<a class="att-link" href="/uploads/{{ ticket.id }}/{{ att.filename }}" download>
{{ att.original_name }}
</a>
{% endif %}
<div class="att-info">
{{ att.uploaded_by }} · {{ att.uploaded_at.strftime('%m-%d %H:%M') }}
· {{ att.size / 1024 | round(1) }} KB
</div>
<button class="att-delete" title="删除"
onclick="deleteAttachment({{ att.id }}, this)">×</button>
</div>
{% endfor %}
</div>
<div class="upload-progress" id="uploadProgress">
<div class="upload-progress-bar" id="uploadProgressBar"></div>
</div>
</div>
<!-- 评论 -->
<div style="margin-top:20px;">
<h2 style="font-size:15px;margin-bottom:12px;color:var(--text-muted);">
评论({{ ticket.comments|length }}
</h2>
<div class="comment-list">
{% for c in ticket.comments %}
<div class="comment">
<div>
<span class="author">{{ c.author }}</span>
<span class="time">{{ c.created_at.strftime('%m-%d %H:%M') }}</span>
</div>
<div class="content">{{ c.content }}</div>
</div>
{% endfor %}
</div>
<!-- 添加评论 -->
<form method="post" action="/ticket/{{ ticket.id }}/comment" class="card" style="margin-top:12px;">
<div class="form-group" style="margin-bottom:10px;">
<label>以 {{ current_user.username }} 身份评论</label>
<textarea name="content" placeholder="输入评论…(提示:可在评论框内粘贴截图)" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">发表评论</button>
</form>
</div>
</div>
<!-- 右侧:侧边栏 -->
<div class="sidebar">
<div class="card">
<div class="form-group" style="margin-bottom:10px;">
<label>状态</label>
<span class="badge badge-{{ ticket.status }}">{{ status_label(ticket.status) }}</span>
</div>
<div class="form-group" style="margin-bottom:10px;">
<label>优先级</label>
<span class="badge badge-{{ ticket.priority }}">{{ priority_label(ticket.priority) }}</span>
</div>
<div class="form-group" style="margin-bottom:10px;">
<label>分类</label>
<span class="badge badge-{{ ticket.category }}">{{ category_label(ticket.category) }}</span>
</div>
<div class="form-group" style="margin-bottom:10px;">
<label>处理人</label>
<p>{{ ticket.assignee or '—' }}</p>
</div>
<div class="form-group">
<label>提交人</label>
<p>{{ ticket.requester or '—' }}</p>
</div>
{% if ticket.cc_users %}
<div class="form-group">
<label>抄送人</label>
<p>
{% for uname in parse_cc(ticket.cc_users) %}
<span class="tag" style="margin:1px;">{{ uname }}</span>
{% endfor %}
</p>
</div>
{% endif %}
</div>
{% if ticket.tags %}
<div class="card">
<label style="font-size:13px;font-weight:600;color:var(--text-muted);">标签</label>
<div style="margin-top:6px;">
{% for tag in parse_tags(ticket.tags) %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- 图片预览模态框 -->
<div class="image-modal" id="imageModal" onclick="closeImage()">
<img id="modalImage" src="">
</div>
<script>
const TICKET_ID = {{ ticket.id }};
const uploadList = document.getElementById('attachmentList');
const uploadZone = document.getElementById('uploadZone');
const progressBar = document.getElementById('uploadProgressBar');
const progressEl = document.getElementById('uploadProgress');
function showImage(src) {
document.getElementById('modalImage').src = src;
document.getElementById('imageModal').classList.add('show');
}
function closeImage() {
document.getElementById('imageModal').classList.remove('show');
}
// 显示上传进度
function showProgress(pct) {
progressEl.classList.add('active');
progressBar.style.width = pct + '%';
}
function hideProgress() {
progressEl.classList.remove('active');
progressBar.style.width = '0%';
}
// 上传文件到服务器
async function uploadFiles(files) {
for (const file of files) {
showProgress(0);
const formData = new FormData();
formData.append('file', file);
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', e => {
if (e.lengthComputable) {
showProgress(Math.round(e.loaded / e.total * 100));
}
});
const resp = await fetch(`/api/upload?t=${TICKET_ID}`, {
method: 'POST',
body: formData,
});
if (resp.ok) {
const data = await resp.json();
addAttachment(data);
} else {
const data = await resp.json();
alert('上传失败:' + (data.error || '未知错误'));
}
} catch (e) {
alert('上传失败:' + e.message);
}
hideProgress();
}
}
// 处理粘贴图片
document.addEventListener('paste', function (e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const files = [];
for (const item of items) {
if (item.type.indexOf('image') !== -1) {
const blob = item.getAsFile();
if (blob) files.push(blob);
}
}
if (files.length > 0) {
e.preventDefault();
uploadFiles(files);
}
});
// 拖拽上传
function handleDragOver(e) {
e.preventDefault();
uploadZone.classList.add('drag-over');
}
function handleDragLeave(e) {
uploadZone.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
uploadZone.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) uploadFiles(files);
}
// 文件选择
function handleFileSelect(files) {
if (files && files.length > 0) {
uploadFiles(files);
}
document.getElementById('fileInput').value = '';
}
// 新增附件到列表(DOM
function addAttachment(data) {
const item = document.createElement('div');
item.className = 'attachment-item';
item.dataset.aid = '';
if (data.is_image) {
item.innerHTML = `
<img class="att-thumb" src="${data.url}" alt="${data.filename}" onclick="showImage(this.src)">
<div class="att-info">${data.filename} · ${formatSize(data.size)}</div>`;
} else {
item.innerHTML = `
<div class="att-icon">📎</div>
<a class="att-link" href="${data.url}" download>${data.filename}</a>
<div class="att-info">${formatSize(data.size)}</div>`;
}
uploadList.appendChild(item);
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// 删除附件
async function deleteAttachment(aid, btn) {
if (!confirm('确认删除此附件?')) return;
try {
const resp = await fetch(`/api/upload/delete/${aid}`, { method: 'POST' });
if (resp.ok) {
btn.closest('.attachment-item').remove();
}
} catch (e) {
alert('删除失败:' + e.message);
}
}
</script>
{% endblock %}