/* ============================================
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 = '
' +
'
' + ticket.id + '' +
'
' +
'
' + escapeHtml(ticket.ti
tle) + '
' +
'
负责人:' + escapeHtml(tic
ket.assignee) + ' · ' + formatDate(ticket.createdAt) + '' +
📡 BC-M06-001 · DEV-010桔子 · 工单管理界面·环节3·响应式布局+数据导出 20
'
' +
'
' +
'' + statusInf
o.label + '' +
'' +
'
' +
'
';
}).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();