Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

809 lines
27 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.

<!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>