302 lines
9.2 KiB
JavaScript
302 lines
9.2 KiB
JavaScript
|
|
/* ============================================
|
|||
|
|
HoloLake · M06 工单管理界面 · 逻辑层
|
|||
|
|
环节3:响应式 + 数据导出(JSON/CSV)
|
|||
|
|
DEV-010 桔子
|
|||
|
|
============================================ */
|
|||
|
|
// === 状态流转规则 ===
|
|||
|
|
const STATUS_FLOW = {
|
|||
|
|
'pending': { next: 'active', label: '待处理' },
|
|||
|
|
'active': { next: 'done', label: '进行中' },
|
|||
|
|
'done': { next: 'pending', label: '已完成' }
|
|||
|
|
};
|
|||
|
|
// === 初始化 · 从 localStorage 读取工单 ===
|
|||
|
|
let tickets = JSON.parse(localStorage.getItem('holotake_tic
|
|||
|
|
kets') || '[]');
|
|||
|
|
// === 保存到 localStorage ===
|
|||
|
|
function saveTickets() {
|
|||
|
|
localStorage.setItem('holotake_tickets', JSON.stringify(t
|
|||
|
|
ickets));
|
|||
|
|
}
|
|||
|
|
// === 生成工单编号 ===
|
|||
|
|
function generateId() {
|
|||
|
|
const now = new Date();
|
|||
|
|
const dateStr = [
|
|||
|
|
now.getFullYear(),
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 18
|
|||
|
|
String(now.getMonth() + 1).padStart(2, '0'),
|
|||
|
|
String(now.getDate()).padStart(2, '0')
|
|||
|
|
].join('');
|
|||
|
|
const rand = String(Math.floor(Math.random() * 1000)).pad
|
|||
|
|
Start(3, '0');
|
|||
|
|
return 'TK-' + dateStr + '-' + rand;
|
|||
|
|
}
|
|||
|
|
// === 渲染统计卡片 ===
|
|||
|
|
function renderStats() {
|
|||
|
|
const total = tickets.length;
|
|||
|
|
const pending = tickets.filter(t => t.status === 'pendin
|
|||
|
|
g').length;
|
|||
|
|
const active = tickets.filter(t => t.status === 'activ
|
|||
|
|
e').length;
|
|||
|
|
const done = tickets.filter(t => t.status === 'done').len
|
|||
|
|
gth;
|
|||
|
|
document.getElementById('statTotal').textContent = total;
|
|||
|
|
document.getElementById('statPending').textContent = pend
|
|||
|
|
ing;
|
|||
|
|
document.getElementById('statActive').textContent = activ
|
|||
|
|
e;
|
|||
|
|
document.getElementById('statDone').textContent = done;
|
|||
|
|
}
|
|||
|
|
// === 获取筛选和排序后的工单列表 ===
|
|||
|
|
function getFilteredTickets() {
|
|||
|
|
const filterStatus = document.getElementById('filterStatu
|
|||
|
|
s').value;
|
|||
|
|
const sortBy = document.getElementById('sortBy').value;
|
|||
|
|
let list = [...tickets];
|
|||
|
|
// 筛选
|
|||
|
|
if (filterStatus !== 'all') {
|
|||
|
|
list = list.filter(t => t.status === filterStatus);
|
|||
|
|
}
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 19
|
|||
|
|
// 排序
|
|||
|
|
if (sortBy === 'newest') {
|
|||
|
|
list.sort((a, b) => new Date(b.createdAt) - new Date(a.
|
|||
|
|
createdAt));
|
|||
|
|
} else if (sortBy === 'oldest') {
|
|||
|
|
list.sort((a, b) => new Date(a.createdAt) - new Date(b.
|
|||
|
|
createdAt));
|
|||
|
|
} else if (sortBy === 'status') {
|
|||
|
|
const order = { 'pending': 0, 'active': 1, 'done': 2 };
|
|||
|
|
list.sort((a, b) => order[a.status] - order[b.status]);
|
|||
|
|
}
|
|||
|
|
return list;
|
|||
|
|
}
|
|||
|
|
// === 渲染工单列表 ===
|
|||
|
|
function renderTickets() {
|
|||
|
|
const list = getFilteredTickets();
|
|||
|
|
const container = document.getElementById('ticketList');
|
|||
|
|
if (list.length === 0) {
|
|||
|
|
container.innerHTML = '<div class="empty-state"><div cl
|
|||
|
|
ass="empty-icon">📋</div><div class="empty-text">暂无工单,点
|
|||
|
|
击上方按钮新建</div></div>';
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
container.innerHTML = list.map(ticket => {
|
|||
|
|
const statusInfo = STATUS_FLOW[ticket.status];
|
|||
|
|
return '<div class="ticket-item">' +
|
|||
|
|
'<span class="ticket-id">' + ticket.id + '</span>' +
|
|||
|
|
'<div class="ticket-info">' +
|
|||
|
|
'<div class="ticket-title">' + escapeHtml(ticket.ti
|
|||
|
|
tle) + '</div>' +
|
|||
|
|
'<div class="ticket-meta">负责人:' + escapeHtml(tic
|
|||
|
|
ket.assignee) + ' · ' + formatDate(ticket.createdAt) + '</d
|
|||
|
|
iv>' +
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 20
|
|||
|
|
'</div>' +
|
|||
|
|
'<div class="ticket-actions">' +
|
|||
|
|
'<span class="status-badge ' + ticket.status + '" o
|
|||
|
|
nclick="toggleStatus(\'' + ticket.id + '\')">' + statusInf
|
|||
|
|
o.label + '</span>' +
|
|||
|
|
'<button class="btn-delete" onclick="deleteTicket
|
|||
|
|
(\'' + ticket.id + '\')" title="删除">🗑</button>' +
|
|||
|
|
'</div>' +
|
|||
|
|
'</div>';
|
|||
|
|
}).join('');
|
|||
|
|
}
|
|||
|
|
// === HTML转义(防XSS) ===
|
|||
|
|
function escapeHtml(str) {
|
|||
|
|
const div = document.createElement('div');
|
|||
|
|
div.textContent = str;
|
|||
|
|
return div.innerHTML;
|
|||
|
|
}
|
|||
|
|
// === 格式化日期 ===
|
|||
|
|
function formatDate(dateStr) {
|
|||
|
|
const d = new Date(dateStr);
|
|||
|
|
return d.getFullYear() + '-' +
|
|||
|
|
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
|||
|
|
String(d.getDate()).padStart(2, '0') + ' ' +
|
|||
|
|
String(d.getHours()).padStart(2, '0') + ':' +
|
|||
|
|
String(d.getMinutes()).padStart(2, '0');
|
|||
|
|
}
|
|||
|
|
// === 切换工单状态 ===
|
|||
|
|
function toggleStatus(id) {
|
|||
|
|
const ticket = tickets.find(t => t.id === id);
|
|||
|
|
if (!ticket) return;
|
|||
|
|
ticket.status = STATUS_FLOW[ticket.status].next;
|
|||
|
|
saveTickets();
|
|||
|
|
renderAll();
|
|||
|
|
}
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 21
|
|||
|
|
// === 删除工单 ===
|
|||
|
|
function deleteTicket(id) {
|
|||
|
|
if (!confirm('确认删除这条工单?')) return;
|
|||
|
|
tickets = tickets.filter(t => t.id !== id);
|
|||
|
|
saveTickets();
|
|||
|
|
renderAll();
|
|||
|
|
}
|
|||
|
|
// === 新建工单弹窗 ===
|
|||
|
|
function openModal() {
|
|||
|
|
document.getElementById('modalOverlay').classList.add('sh
|
|||
|
|
ow');
|
|||
|
|
document.getElementById('inputTitle').value = '';
|
|||
|
|
document.getElementById('inputAssignee').value = '';
|
|||
|
|
document.getElementById('inputDetail').value = '';
|
|||
|
|
document.getElementById('inputTitle').focus();
|
|||
|
|
}
|
|||
|
|
function closeModal() {
|
|||
|
|
document.getElementById('modalOverlay').classList.remove
|
|||
|
|
('show');
|
|||
|
|
}
|
|||
|
|
// === 提交新建工单 ===
|
|||
|
|
function submitTicket() {
|
|||
|
|
const title = document.getElementById('inputTitle').valu
|
|||
|
|
e.trim();
|
|||
|
|
const assignee = document.getElementById('inputAssigne
|
|||
|
|
e').value.trim();
|
|||
|
|
const detail = document.getElementById('inputDetail').val
|
|||
|
|
ue.trim();
|
|||
|
|
if (!title) {
|
|||
|
|
alert('请填写工单标题');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (!assignee) {
|
|||
|
|
alert('请填写负责人');
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 22
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const newTicket = {
|
|||
|
|
id: generateId(),
|
|||
|
|
title: title,
|
|||
|
|
assignee: assignee,
|
|||
|
|
detail: detail,
|
|||
|
|
status: 'pending',
|
|||
|
|
createdAt: new Date().toISOString()
|
|||
|
|
};
|
|||
|
|
tickets.unshift(newTicket);
|
|||
|
|
saveTickets();
|
|||
|
|
closeModal();
|
|||
|
|
renderAll();
|
|||
|
|
}
|
|||
|
|
// === 重置全部工单 ===
|
|||
|
|
function resetTickets() {
|
|||
|
|
if (!confirm('确认清空所有工单数据?此操作不可恢复!')) return;
|
|||
|
|
tickets = [];
|
|||
|
|
saveTickets();
|
|||
|
|
renderAll();
|
|||
|
|
}
|
|||
|
|
// ============================================
|
|||
|
|
// 环节3新增:导出功能
|
|||
|
|
// ============================================
|
|||
|
|
// === 导出下拉菜单控制 ===
|
|||
|
|
function toggleExportMenu() {
|
|||
|
|
const dropdown = document.getElementById('exportDropdow
|
|||
|
|
n');
|
|||
|
|
const clickAway = document.getElementById('clickAway');
|
|||
|
|
const isOpen = dropdown.classList.contains('show');
|
|||
|
|
if (isOpen) {
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 23
|
|||
|
|
closeExportMenu();
|
|||
|
|
} else {
|
|||
|
|
dropdown.classList.add('show');
|
|||
|
|
clickAway.classList.add('show');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function closeExportMenu() {
|
|||
|
|
document.getElementById('exportDropdown').classList.remov
|
|||
|
|
e('show');
|
|||
|
|
document.getElementById('clickAway').classList.remove('sh
|
|||
|
|
ow');
|
|||
|
|
}
|
|||
|
|
// === 导出为 JSON 文件 ===
|
|||
|
|
function exportJSON() {
|
|||
|
|
closeExportMenu();
|
|||
|
|
if (tickets.length === 0) {
|
|||
|
|
alert('当前没有工单数据可导出');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const data = JSON.stringify(tickets, null, 2);
|
|||
|
|
const blob = new Blob([data], { type: 'application/json;c
|
|||
|
|
harset=utf-8' });
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
const a = document.createElement('a');
|
|||
|
|
a.href = url;
|
|||
|
|
a.download = 'HoloLake-Tickets-' + getDateStamp() + '.jso
|
|||
|
|
n';
|
|||
|
|
document.body.appendChild(a);
|
|||
|
|
a.click();
|
|||
|
|
document.body.removeChild(a);
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
}
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 24
|
|||
|
|
// === 导出为 CSV 文件 ===
|
|||
|
|
function exportCSV() {
|
|||
|
|
closeExportMenu();
|
|||
|
|
if (tickets.length === 0) {
|
|||
|
|
alert('当前没有工单数据可导出');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// CSV表头
|
|||
|
|
const header = '编号,标题,负责人,状态,详情,创建时间';
|
|||
|
|
// CSV内容行
|
|||
|
|
const rows = tickets.map(t => {
|
|||
|
|
const statusLabel = STATUS_FLOW[t.status] ? STATUS_FLOW
|
|||
|
|
[t.status].label : t.status;
|
|||
|
|
return [
|
|||
|
|
t.id,
|
|||
|
|
csvEscape(t.title),
|
|||
|
|
csvEscape(t.assignee),
|
|||
|
|
statusLabel,
|
|||
|
|
csvEscape(t.detail || ''),
|
|||
|
|
formatDate(t.createdAt)
|
|||
|
|
].join(',');
|
|||
|
|
});
|
|||
|
|
const csv = '\uFEFF' + header + '\n' + rows.join('\n');
|
|||
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=ut
|
|||
|
|
f-8' });
|
|||
|
|
const url = URL.createObjectURL(blob);
|
|||
|
|
const a = document.createElement('a');
|
|||
|
|
a.href = url;
|
|||
|
|
a.download = 'HoloLake-Tickets-' + getDateStamp() + '.cs
|
|||
|
|
v';
|
|||
|
|
document.body.appendChild(a);
|
|||
|
|
a.click();
|
|||
|
|
document.body.removeChild(a);
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 25
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
}
|
|||
|
|
// === CSV字段转义(处理逗号和引号) ===
|
|||
|
|
function csvEscape(str) {
|
|||
|
|
if (!str) return '';
|
|||
|
|
if (str.includes(',') || str.includes('"') || str.include
|
|||
|
|
s('\n')) {
|
|||
|
|
return '"' + str.replace(/"/g, '""') + '"';
|
|||
|
|
}
|
|||
|
|
return str;
|
|||
|
|
}
|
|||
|
|
// === 生成日期戳(用于文件名) ===
|
|||
|
|
function getDateStamp() {
|
|||
|
|
const now = new Date();
|
|||
|
|
return now.getFullYear() +
|
|||
|
|
String(now.getMonth() + 1).padStart(2, '0') +
|
|||
|
|
String(now.getDate()).padStart(2, '0') + '-' +
|
|||
|
|
String(now.getHours()).padStart(2, '0') +
|
|||
|
|
String(now.getMinutes()).padStart(2, '0');
|
|||
|
|
}
|
|||
|
|
// === 统一渲染 ===
|
|||
|
|
function renderAll() {
|
|||
|
|
renderStats();
|
|||
|
|
renderTickets();
|
|||
|
|
}
|
|||
|
|
// === 筛选/排序变化时重新渲染 ===
|
|||
|
|
document.getElementById('filterStatus').addEventListener('c
|
|||
|
|
hange', renderTickets);
|
|||
|
|
document.getElementById('sortBy').addEventListener('chang
|
|||
|
|
e', renderTickets);
|
|||
|
|
// === 弹窗点击遮罩关闭 ===
|
|||
|
|
document.getElementById('modalOverlay').addEventListener('c
|
|||
|
|
lick', function(e) {
|
|||
|
|
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 26
|
|||
|
|
if (e.target === this) closeModal();
|
|||
|
|
});
|
|||
|
|
// === 页面加载完成 · 首次渲染 ===
|
|||
|
|
renderAll();
|