809 lines
27 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工单管理 · HoloLake · 视觉优化版</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
/* ========== 基础样式(深蓝渐变保持不变) ========== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #0a1628 0%, #1a2a4a 50%, #0d1f3c 100%);
color: #e0e6ed;
min-height: 100vh;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 导航栏(增加图标) */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 24px;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 24px;
font-weight: bold;
color: #fff;
}
.logo i {
color: #4fc3f7;
font-size: 28px;
}
.nav-links a {
margin-left: 24px;
text-decoration: none;
color: #8899aa;
font-size: 14px;
transition: color 0.2s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.nav-links a i {
font-size: 16px;
}
.nav-links a.active {
color: #4fc3f7;
}
.nav-links a:hover {
color: #fff;
}
/* 统计卡片区(微调圆角与阴影) */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 32px;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 20px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.08);
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.stat-card:hover {
background: rgba(255, 255, 255, 0.12);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.stat-card.active {
border: 2px solid #4fc3f7;
background: rgba(79, 195, 247, 0.15);
}
.stat-number {
font-size: 32px;
font-weight: bold;
color: #fff;
margin-bottom: 4px;
}
.stat-label {
color: #8899aa;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.stat-label i {
color: #4fc3f7;
font-size: 16px;
}
/* 工单列表头部 */
.ticket-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 10px;
}
.ticket-header h2 {
font-size: 18px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
}
.ticket-header h2 i {
color: #4fc3f7;
}
.btn-primary {
background: #4fc3f7;
color: #0a1628;
border: none;
padding: 8px 20px;
border-radius: 30px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
box-shadow: 0 2px 8px rgba(79, 195, 247, 0.3);
}
.btn-primary i {
font-size: 16px;
}
.btn-primary:hover {
background: #81d4fa;
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.5);
}
/* 工具栏 */
.toolbar {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.sort-btn {
color: #4fc3f7;
cursor: pointer;
font-weight: 500;
user-select: none;
padding: 6px 12px;
border-radius: 30px;
transition: all 0.2s;
background: rgba(79, 195, 247, 0.1);
display: inline-flex;
align-items: center;
gap: 6px;
}
.sort-btn:hover {
background: rgba(79, 195, 247, 0.2);
}
.tool-btn {
background: rgba(255, 255, 255, 0.08);
color: #e0e6ed;
border: 1px solid rgba(255, 255, 255, 0.15);
padding: 6px 16px;
border-radius: 30px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(2px);
}
.tool-btn i {
font-size: 14px;
color: #4fc3f7;
}
.tool-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: #4fc3f7;
}
/* 工单列表容器 */
.ticket-list {
background: rgba(0, 0, 0, 0.25);
border-radius: 24px;
overflow: hidden;
margin-bottom: 32px;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(2px);
}
/* 单个工单项(优化悬停与图标) */
.ticket-item {
display: grid;
grid-template-columns: 80px 1fr 120px 100px 60px;
align-items: center;
padding: 18px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
transition: background 0.2s, transform 0.1s;
line-height: 1.6;
letter-spacing: 0.3px;
}
.ticket-item:hover {
background: rgba(255, 255, 255, 0.08);
}
.ticket-item:last-child {
border-bottom: none;
}
.ticket-id {
color: #8899aa;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
}
.ticket-id i {
color: #4fc3f7;
font-size: 14px;
}
.ticket-title {
font-weight: 400;
color: #fff;
text-shadow: 0 0 8px rgba(79,195,247,0.2);
}
.ticket-assignee {
color: #8899aa;
font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
opacity: 0.9;
}
.ticket-assignee i {
color: #4fc3f7;
font-size: 12px;
}
.ticket-status {
padding: 6px 14px;
border-radius: 30px;
font-size: 12px;
font-weight: 600;
text-align: center;
display: inline-flex;
align-items: center;
gap: 6px;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
letter-spacing: 0.5px;
}
.ticket-status:hover {
filter: brightness(1.1);
transform: scale(1.02);
}
.status-进行中 {
background: rgba(79, 195, 247, 0.2);
color: #4fc3f7;
}
.status-进行中 i {
color: #4fc3f7;
}
.status-已完成 {
background: rgba(76, 175, 80, 0.2);
color: #81c784;
}
.status-已完成 i {
color: #81c784;
}
.status-待开始 {
background: rgba(255, 255, 255, 0.1);
color: #b0bec5;
}
.status-待开始 i {
color: #b0bec5;
}
/* 删除按钮 */
.delete-btn {
color: #f44336;
cursor: pointer;
font-size: 18px;
text-align: center;
transition: all 0.2s;
opacity: 0.7;
}
.delete-btn:hover {
opacity: 1;
transform: scale(1.2);
}
/* 底部版权 */
.footer {
text-align: center;
color: #62748c;
font-size: 14px;
padding: 32px 0;
border-top: 1px solid rgba(255, 255, 255, 0.05);
margin-top: 32px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.footer i {
color: #4fc3f7;
}
/* ========== 模态框(弹窗)优化 ========== */
.modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8);
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal.show {
display: flex;
}
.modal-content {
background: #1e2a3a;
border-radius: 32px;
padding: 32px;
width: 90%;
max-width: 500px;
color: #e0e6ed;
border: 1px solid rgba(255,255,255,0.2);
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
animation: modalFadeIn 0.3s ease;
}
@keyframes modalFadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.modal-content h3 {
margin-top: 0;
margin-bottom: 24px;
color: #4fc3f7;
font-size: 22px;
display: flex;
align-items: center;
gap: 10px;
}
.modal-content h3 i {
font-size: 24px;
}
.modal-content input,
.modal-content select,
.modal-content textarea {
width: 100%;
padding: 14px 16px;
margin: 8px 0 20px;
background: #0f1a2b;
border: 1px solid #2a3a4a;
color: #e0e6ed;
border-radius: 16px;
font-size: 14px;
transition: border 0.2s, box-shadow 0.2s;
}
.modal-content input:focus,
.modal-content select:focus,
.modal-content textarea:focus {
outline: none;
border-color: #4fc3f7;
box-shadow: 0 0 0 3px rgba(79, 195, 247, 0.2);
}
.modal-content button {
background: #4fc3f7;
color: #0a1628;
border: none;
padding: 12px 24px;
border-radius: 40px;
cursor: pointer;
margin-right: 12px;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.3);
}
.modal-content button.cancel {
background: #3a4a5a;
color: #e0e6ed;
box-shadow: none;
}
.modal-content button.cancel:hover {
background: #4a5a6a;
}
.modal-content button:hover {
background: #81d4fa;
transform: translateY(-2px);
}
.modal-content button i {
font-size: 16px;
}
/* 响应式微调 */
@media (max-width: 768px) {
.stats {
grid-template-columns: repeat(2, 1fr);
}
.ticket-item {
grid-template-columns: 60px 1fr 100px 80px 40px;
padding: 12px 16px;
font-size: 13px;
}
.ticket-status {
padding: 4px 8px;
font-size: 11px;
}
}
@media (max-width: 480px) {
.stats {
grid-template-columns: 1fr;
}
.navbar {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.nav-links a {
margin-left: 0;
margin-right: 16px;
}
.ticket-item {
grid-template-columns: 50px 1fr 80px 70px 35px;
padding: 10px 12px;
font-size: 12px;
}
.toolbar {
justify-content: flex-start;
}
}
.page-title {
color: #003366; /* 深蓝色示例 */
}</style>
</head>
<body>
<div class="app">
<nav class="navbar">
<div class="logo">
<i class="fas fa-water"></i> HoloLake
</div>
<div class="nav-links">
<a href="#" class="active"><i class="fas fa-tasks"></i> 工单管理</a>
<a href="#"><i class="fas fa-chart-pie"></i> 统计</a>
<a href="#"><i class="fas fa-cog"></i> 设置</a>
</div>
</nav>
<main class="main-content">
<div class="stats">
<div class="stat-card" data-status="全部">
<div class="stat-number" id="stat-all">5</div>
<div class="stat-label"><i class="fas fa-clipboard-list"></i> 总工单</div>
</div>
<div class="stat-card" data-status="进行中">
<div class="stat-number" id="stat-progress">3</div>
<div class="stat-label"><i class="fas fa-spinner"></i> 进行中</div>
</div>
<div class="stat-card" data-status="已完成">
<div class="stat-number" id="stat-done">1</div>
<div class="stat-label"><i class="fas fa-check-circle"></i> 已完成</div>
</div>
<div class="stat-card" data-status="待开始">
<div class="stat-number" id="stat-todo">1</div>
<div class="stat-label"><i class="fas fa-clock"></i> 待分配</div>
</div>
</div>
<div class="ticket-header">
<h2><i class="fas fa-list-ul"></i> 工单列表</h2>
<button class="btn-primary" id="new-ticket-btn"><i class="fas fa-plus-circle"></i> 新建工单</button>
</div>
<div class="toolbar">
<span id="sort-btn" class="sort-btn"><i class="fas fa-sort-amount-down-alt"></i> 编号↑</span>
<button class="tool-btn" id="export-btn"><i class="fas fa-download"></i> 导出CSV</button>
<button class="tool-btn" id="reset-btn"><i class="fas fa-undo-alt"></i> 重置</button>
</div>
<div id="ticket-list" class="ticket-list"></div>
</main>
<footer class="footer">
<i class="fas fa-water"></i> HoloLake Era · AGE OS v1.0 · <i class="fas fa-heart" style="color: #f44336;"></i> 光湖团队
</footer>
</div>
<!-- 新建工单模态框 -->
<div id="ticket-modal" class="modal">
<div class="modal-content">
<h3><i class="fas fa-pen-alt"></i> 新建工单</h3>
<input type="text" id="ticket-title" placeholder="工单标题" required>
<input type="text" id="ticket-assignee" placeholder="负责人 (如: 肥猫·M01)" required>
<select id="ticket-status">
<option value="进行中">进行中</option>
<option value="已完成">已完成</option>
<option value="待开始">待开始</option>
</select>
<textarea id="ticket-desc" placeholder="详情描述(选填)" rows="3"></textarea>
<div style="text-align: right;">
<button class="cancel" id="cancel-modal"><i class="fas fa-times"></i> 取消</button>
<button id="save-ticket"><i class="fas fa-check"></i> 确认创建</button>
</div>
</div>
</div>
<script>
// ---------- 数据持久化 ----------
let tickets = [];
const DEFAULT_TICKETS = [
{ id: '#001', title: '用户登录界面开发', assignee: '肥猫·M01', status: '进行中', desc: '' },
{ id: '#002', title: '后端服务器部署上线', assignee: '页页·后端', status: '已完成', desc: '' },
{ id: '#003', title: '系统状态看板搭建', assignee: '小草莓·M-DASHBOARD', status: '进行中', desc: '' },
{ id: '#004', title: '网站云盘系统', assignee: '燕樊·M15', status: '进行中', desc: '' },
{ id: '#005', title: '用户中心界面', assignee: '花尔·M05', status: '待开始', desc: '' }
];
function loadTickets() {
const stored = localStorage.getItem('hololake_tickets');
if (stored) {
tickets = JSON.parse(stored);
} else {
tickets = [...DEFAULT_TICKETS];
saveTickets();
}
}
function saveTickets() {
localStorage.setItem('hololake_tickets', JSON.stringify(tickets));
}
function generateNewId() {
if (tickets.length === 0) return '#001';
const maxNum = Math.max(...tickets.map(t => parseInt(t.id.slice(1))));
return '#' + String(maxNum + 1).padStart(3, '0');
}
// ---------- 渲染 ----------
let currentFilter = '全部';
let currentSort = 'asc';
function renderTickets() {
let filtered = tickets;
if (currentFilter !== '全部') {
filtered = tickets.filter(t => t.status === currentFilter);
}
filtered.sort((a, b) => {
const numA = parseInt(a.id.slice(1));
const numB = parseInt(b.id.slice(1));
return currentSort === 'asc' ? numA - numB : numB - numA;
});
const listHtml = filtered.map(t => {
let statusIcon = '';
if (t.status === '进行中') statusIcon = '<i class="fas fa-sync-alt fa-spin"></i>';
else if (t.status === '已完成') statusIcon = '<i class="fas fa-check-circle"></i>';
else if (t.status === '待开始') statusIcon = '<i class="fas fa-hourglass-start"></i>';
return `
<div class="ticket-item" data-id="${t.id}">
<span class="ticket-id"><i class="fas fa-hashtag"></i> ${t.id.slice(1)}</span>
<span class="ticket-title">${t.title}</span>
<span class="ticket-assignee"><i class="fas fa-user"></i> ${t.assignee}</span>
<span class="ticket-status status-${t.status}" data-id="${t.id}">${statusIcon} ${t.status}</span>
<span class="delete-btn" data-id="${t.id}"><i class="fas fa-trash-alt"></i></span>
</div>
`;
}).join('');
document.getElementById('ticket-list').innerHTML = listHtml;
document.getElementById('stat-all').innerText = tickets.length;
document.getElementById('stat-progress').innerText = tickets.filter(t => t.status === '进行中').length;
document.getElementById('stat-done').innerText = tickets.filter(t => t.status === '已完成').length;
document.getElementById('stat-todo').innerText = tickets.filter(t => t.status === '待开始').length;
}
function exportToCSV() {
let filtered = tickets;
if (currentFilter !== '全部') {
filtered = tickets.filter(t => t.status === currentFilter);
}
filtered.sort((a, b) => {
const numA = parseInt(a.id.slice(1));
const numB = parseInt(b.id.slice(1));
return currentSort === 'asc' ? numA - numB : numB - numA;
});
let csv = "编号,标题,负责人,状态,描述\n";
filtered.forEach(t => {
const desc = (t.desc || '').replace(/,/g, '').replace(/\n/g, ' ');
csv += `${t.id},${t.title},${t.assignee},${t.status},${desc}\n`;
});
const blob = new Blob(["\uFEFF" + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.setAttribute('download', '工单数据.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function resetToDefault() {
if (confirm('重置将恢复默认工单,当前所有自定义工单会被清除。确定吗?')) {
tickets = DEFAULT_TICKETS.map(t => ({ ...t }));
saveTickets();
currentFilter = '全部';
currentSort = 'asc';
document.getElementById('sort-btn').innerHTML = '<i class="fas fa-sort-amount-down-alt"></i> 编号↑';
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
renderTickets();
}
}
document.addEventListener('DOMContentLoaded', () => {
loadTickets();
renderTickets();
document.querySelectorAll('.stat-card').forEach(card => {
card.addEventListener('click', () => {
const status = card.getAttribute('data-status');
currentFilter = status === '全部' ? '全部' : status;
renderTickets();
document.querySelectorAll('.stat-card').forEach(c => c.classList.remove('active'));
card.classList.add('active');
});
});
const sortBtn = document.getElementById('sort-btn');
if (sortBtn) {
sortBtn.addEventListener('click', () => {
currentSort = currentSort === 'asc' ? 'desc' : 'asc';
sortBtn.innerHTML = currentSort === 'asc'
? '<i class="fas fa-sort-amount-down-alt"></i> 编号↑'
: '<i class="fas fa-sort-amount-up-alt"></i> 编号↓';
renderTickets();
});
}
document.getElementById('new-ticket-btn').addEventListener('click', () => {
document.getElementById('ticket-modal').classList.add('show');
document.getElementById('ticket-title').focus();
});
document.getElementById('cancel-modal').addEventListener('click', () => {
document.getElementById('ticket-modal').classList.remove('show');
});
document.getElementById('ticket-modal').addEventListener('click', (e) => {
if (e.target === document.getElementById('ticket-modal')) {
document.getElementById('ticket-modal').classList.remove('show');
}
});
document.getElementById('save-ticket').addEventListener('click', () => {
const title = document.getElementById('ticket-title').value.trim();
const assignee = document.getElementById('ticket-assignee').value.trim();
const status = document.getElementById('ticket-status').value;
const desc = document.getElementById('ticket-desc').value.trim();
if (!title || !assignee) {
alert('请填写标题和负责人');
return;
}
const newTicket = {
id: generateNewId(),
title: title,
assignee: assignee,
status: status,
desc: desc
};
tickets.push(newTicket);
saveTickets();
renderTickets();
document.getElementById('ticket-modal').classList.remove('show');
document.getElementById('ticket-title').value = '';
document.getElementById('ticket-assignee').value = '';
document.getElementById('ticket-status').value = '进行中';
document.getElementById('ticket-desc').value = '';
});
document.getElementById('ticket-list').addEventListener('click', (e) => {
const deleteBtn = e.target.closest('.delete-btn');
if (deleteBtn) {
const id = deleteBtn.getAttribute('data-id');
tickets = tickets.filter(t => t.id !== id);
saveTickets();
renderTickets();
return;
}
const statusSpan = e.target.closest('.ticket-status');
if (statusSpan) {
const id = statusSpan.getAttribute('data-id');
const ticket = tickets.find(t => t.id === id);
if (ticket) {
if (ticket.status === '进行中') ticket.status = '已完成';
else if (ticket.status === '已完成') ticket.status = '进行中';
else if (ticket.status === '待开始') ticket.status = '进行中';
saveTickets();
renderTickets();
}
return;
}
const item = e.target.closest('.ticket-item');
if (item && !e.target.closest('.delete-btn') && !e.target.closest('.ticket-status')) {
const id = item.getAttribute('data-id');
const ticket = tickets.find(t => t.id === id);
alert(`工单详情\n编号${ticket.id}\n标题${ticket.title}\n负责人${ticket.assignee}\n状态${ticket.status}\n描述${ticket.desc || '无'}`);
}
});
document.getElementById('export-btn').addEventListener('click', exportToCSV);
document.getElementById('reset-btn').addEventListener('click', resetToDefault);
});
</script>
background: linear-gradient(135deg, #0a1628 0%, #1a2a4a 50%, #0d1f3c 100%);</body>
</html>