图:

代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>维修登记管理系统 - 增强版</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
:root {
--primary: #3498db;
--secondary: #2c3e50;
--success: #2ecc71;
--warning: #f39c12;
--danger: #e74c3c;
--light: #ecf0f1;
--dark: #34495e;
--gray: #95a5a6;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
font-size: 28px;
color: var(--primary);
}
.logo h1 {
color: var(--secondary);
font-size: 24px;
font-weight: 700;
}
.stats {
display: flex;
gap: 15px;
}
.stat-card {
background: var(--light);
padding: 10px 15px;
border-radius: 8px;
text-align: center;
min-width: 100px;
}
.stat-card h3 {
font-size: 24px;
color: var(--secondary);
}
.stat-card p {
font-size: 12px;
color: var(--gray);
margin-top: 5px;
}
.main-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
}
.card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: var(--secondary);
}
.form-group {
margin-bottom: 15px;
position: relative;
}
label {
display: block;
margin-bottom: 5px;
color: var(--dark);
font-weight: 500;
font-size: 14px;
}
input, select, textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
textarea {
min-height: 80px;
resize: vertical;
}
.smart-input input {
padding-right: 35px;
}
.clear-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--gray);
cursor: pointer;
padding: 5px;
display: none;
}
.smart-input input:not(:placeholder-shown) ~ .clear-btn {
display: block;
}
.history-hint {
font-size: 11px;
color: var(--gray);
margin-top: 3px;
font-style: italic;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(52, 152, 219, 0.3);
}
.btn-success {
background: var(--success);
color: white;
}
.btn-warning {
background: var(--warning);
color: white;
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-sm {
padding: 5px 10px;
font-size: 12px;
}
.btn-secondary {
background: var(--light);
color: var(--dark);
border: 1px solid #ddd;
}
.btn-secondary:hover {
background: #d5dbdb;
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover {
background: #138496;
}
.search-box {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-box input {
flex: 1;
}
.status-filter {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.filter-btn {
padding: 6px 12px;
border-radius: 20px;
background: var(--light);
border: 1px solid #ddd;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.filter-btn.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.tickets-list {
max-height: 500px;
overflow-y: auto;
padding-right: 5px;
}
.ticket-item {
background: #f9f9f9;
border-radius: 8px;
padding: 15px;
margin-bottom: 12px;
border-left: 4px solid var(--gray);
transition: all 0.3s;
}
.ticket-item:hover {
transform: translateX(5px);
box-shadow: 0 3px 8px rgba(0,0,0,0.1);
}
.ticket-item.pending { border-left-color: var(--warning); }
.ticket-item.progress { border-left-color: var(--primary); }
.ticket-item.completed { border-left-color: var(--success); }
.ticket-item.canceled { border-left-color: var(--danger); }
.ticket-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.ticket-title {
font-weight: 600;
color: var(--secondary);
font-size: 16px;
}
.ticket-number {
background: var(--secondary);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-weight: bold;
font-size: 14px;
letter-spacing: 1px;
}
.ticket-status {
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.status-pending { background: #ffeaa7; color: #d35400; }
.status-progress { background: #74b9ff; color: #0984e3; }
.status-completed { background: #55efc4; color: #00b894; }
.status-canceled { background: #ff7675; color: #d63031; }
.ticket-body {
font-size: 14px;
color: #555;
margin-bottom: 10px;
line-height: 1.5;
}
.ticket-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--gray);
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #eee;
flex-wrap: wrap;
gap: 10px;
}
.ticket-actions {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--gray);
}
.empty-state i {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
background: white;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
transform: translateX(150%);
transition: transform 0.3s ease-out;
display: flex;
align-items: center;
gap: 10px;
}
.notification.show {
transform: translateX(0);
}
.notification.success {
border-left: 4px solid var(--success);
}
.notification.error {
border-left: 4px solid var(--danger);
}
.history-management {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
font-size: 12px;
}
.history-management button {
margin-right: 8px;
margin-top: 5px;
}
.history-list {
margin-top: 8px;
display: none;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
max-height: 150px;
overflow-y: auto;
padding: 5px;
}
.history-list.show {
display: block;
}
.history-item {
padding: 5px 8px;
cursor: pointer;
border-radius: 3px;
transition: background 0.2s;
}
.history-item:hover {
background: var(--light);
}
.history-item .count {
float: right;
color: var(--gray);
font-size: 11px;
}
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
animation: fadeIn 0.3s;
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 20px;
border-radius: 10px;
width: 90%;
max-width: 500px;
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
animation: slideIn 0.3s;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
color: var(--secondary);
}
.close-modal {
font-size: 28px;
font-weight: bold;
color: var(--gray);
cursor: pointer;
border: none;
background: none;
}
.close-modal:hover {
color: var(--danger);
}
.copy-info-box {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin: 15px 0;
border: 1px solid #ddd;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
line-height: 1.6;
max-height: 300px;
overflow-y: auto;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 15px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
header {
flex-direction: column;
gap: 15px;
}
.stats {
width: 100%;
justify-content: center;
}
.ticket-header {
flex-direction: column;
align-items: flex-start;
}
.modal-content {
width: 95%;
margin: 10% auto;
}
}
.tickets-list::-webkit-scrollbar,
.history-list::-webkit-scrollbar,
.copy-info-box::-webkit-scrollbar {
width: 6px;
}
.tickets-list::-webkit-scrollbar-track,
.history-list::-webkit-scrollbar-track,
.copy-info-box::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.tickets-list::-webkit-scrollbar-thumb,
.history-list::-webkit-scrollbar-thumb,
.copy-info-box::-webkit-scrollbar-thumb {
background: #bdc3c7;
border-radius: 10px;
}
.tickets-list::-webkit-scrollbar-thumb:hover,
.history-list::-webkit-scrollbar-thumb:hover,
.copy-info-box::-webkit-scrollbar-thumb:hover {
background: #95a5a6;
}
.search-hint {
font-size: 12px;
color: var(--gray);
margin-top: 5px;
font-style: italic;
}
.print-actions {
display: flex;
gap: 5px;
margin-top: 8px;
}
.copy-success {
color: var(--success);
font-size: 12px;
margin-left: 10px;
display: none;
}
.copy-success.show {
display: inline;
animation: fadeIn 0.3s;
}
.print-preview {
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">
<i class="fas fa-tools"></i>
<h1>维修登记管理系统</h1>
</div>
<div class="stats">
<div class="stat-card">
<h3 id="total-count">0</h3>
<p>总工单</p>
</div>
<div class="stat-card">
<h3 id="pending-count">0</h3>
<p>待处理</p>
</div>
<div class="stat-card">
<h3 id="progress-count">0</h3>
<p>维修中</p>
</div>
</div>
</header>
<div class="main-content">
<div class="left-panel">
<div class="card">
<div class="card-header">
<div class="card-title">提交维修单</div>
<i class="fas fa-clipboard-list" style="color: var(--primary);"></i>
</div>
<form id="repair-form">
<div class="form-group">
<label for="device">设备名称 <span style="color: var(--gray); font-weight: normal;">(可选,支持历史记录)</span></label>
<div class="smart-input">
<input type="text" id="device" placeholder="输入或选择设备名称" list="device-list" autocomplete="off">
<button type="button" class="clear-btn" onclick="clearInput('device')" title="清空">
<i class="fas fa-times"></i>
</button>
</div>
<datalist id="device-list"></datalist>
<div class="history-hint">💡 提示:输入时会自动匹配历史记录,也可以输入新设备名称</div>
<div class="history-management">
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleHistory('device')">
<i class="fas fa-history"></i> 查看历史
</button>
<button type="button" class="btn btn-sm btn-danger" onclick="clearHistory('device')">
<i class="fas fa-trash"></i> 清空历史
</button>
<div id="device-history-list" class="history-list"></div>
</div>
</div>
<div class="form-group">
<label for="problem">故障描述 <span style="color: var(--gray); font-weight: normal;">(可选,支持历史记录)</span></label>
<div class="smart-input">
<input type="text" id="problem" placeholder="输入或选择故障描述" list="problem-list" autocomplete="off">
<button type="button" class="clear-btn" onclick="clearInput('problem')" title="清空">
<i class="fas fa-times"></i>
</button>
</div>
<datalist id="problem-list"></datalist>
<div class="history-hint">💡 提示:输入时会自动匹配历史故障,也可以输入新的故障描述</div>
<div class="history-management">
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleHistory('problem')">
<i class="fas fa-history"></i> 查看历史
</button>
<button type="button" class="btn btn-sm btn-danger" onclick="clearHistory('problem')">
<i class="fas fa-trash"></i> 清空历史
</button>
<div id="problem-history-list" class="history-list"></div>
</div>
</div>
<div class="form-group">
<label for="contact">联系人 <span style="color: var(--danger);">*</span></label>
<input type="text" id="contact" placeholder="请输入您的姓名" required>
</div>
<div class="form-group">
<label for="phone">联系电话 <span style="color: var(--danger);">*</span></label>
<input type="tel" id="phone" placeholder="请输入联系电话" required>
</div>
<div class="form-group">
<label for="location">设备位置 <span style="color: var(--danger);">*</span></label>
<input type="text" id="location" placeholder="如:办公室A区3楼" required>
</div>
<div class="form-group">
<label for="urgency">紧急程度 <span style="color: var(--danger);">*</span></label>
<select id="urgency" required>
<option value="">请选择紧急程度</option>
<option value="low">低</option>
<option value="medium">中</option>
<option value="high">高</option>
<option value="critical">紧急</option>
</select>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> 提交维修单
</button>
</form>
</div>
<div class="card" style="margin-top: 20px;">
<div class="card-header">
<div class="card-title">系统说明</div>
</div>
<div style="font-size: 14px; color: #555; line-height: 1.6;">
<p>本系统用于管理设备维修登记,您可以:</p>
<ul style="margin: 10px 0 10px 20px;">
<li><strong>智能输入:</strong>设备和故障支持历史记录自动匹配</li>
<li><strong>灵活填写:</strong>可选择历史记录,也可输入新内容</li>
<li><strong>自动生成单号:</strong>格式:前2位字母 + 后2位数字(如:AB12)</li>
<li><strong>打印功能:</strong>一键打印维修单,方便上门服务</li>
<li><strong>复制信息:</strong>一键复制信息,快速发微信</li>
</ul>
<p><strong>必填项:</strong>联系人、电话、位置、紧急程度</p>
<p><strong>可选项:</strong>设备名称、故障描述(支持历史记录)</p>
<p>所有数据保存在浏览器本地,刷新页面不会丢失。</p>
</div>
</div>
</div>
<div class="right-panel">
<div class="card">
<div class="card-header">
<div class="card-title">维修工单管理</div>
<i class="fas fa-tasks" style="color: var(--primary);"></i>
</div>
<div class="search-box">
<input type="text" id="search-input" placeholder="搜索单号、设备、联系人或问题...">
<button class="btn btn-primary" id="search-btn">
<i class="fas fa-search"></i> 搜索
</button>
</div>
<div class="search-hint">提示:可搜索单号(如 AB12)、设备名、联系人或问题关键词</div>
<div class="status-filter">
<div class="filter-btn active" data-status="all">全部</div>
<div class="filter-btn" data-status="pending">待处理</div>
<div class="filter-btn" data-status="progress">维修中</div>
<div class="filter-btn" data-status="completed">已完成</div>
<div class="filter-btn" data-status="canceled">已取消</div>
</div>
<div class="tickets-list" id="tickets-list">
<div class="empty-state">
<i class="fas fa-clipboard-list"></i>
<p>暂无维修工单,请提交第一个维修单</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="copyModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-clipboard"></i> 复制信息发微信</h3>
<button class="close-modal" onclick="closeCopyModal()">×</button>
</div>
<div class="copy-info-box" id="copyInfoContent"></div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeCopyModal()">关闭</button>
<button class="btn btn-info" onclick="copyToClipboard()">
<i class="fas fa-copy"></i> 复制信息
</button>
<span id="copySuccess" class="copy-success">✓ 已复制!</span>
</div>
</div>
</div>
<div class="notification" id="notification">
<i class="fas fa-check-circle"></i>
<span id="notification-text">操作成功!</span>
</div>
<script>
let repairTickets = JSON.parse(localStorage.getItem('repairTickets')) || [];
let deviceHistory = JSON.parse(localStorage.getItem('deviceHistory')) || [];
let problemHistory = JSON.parse(localStorage.getItem('problemHistory')) || [];
const form = document.getElementById('repair-form');
const ticketsList = document.getElementById('tickets-list');
const searchInput = document.getElementById('search-input');
const searchBtn = document.getElementById('search-btn');
const filterBtns = document.querySelectorAll('.filter-btn');
const notification = document.getElementById('notification');
const notificationText = document.getElementById('notification-text');
const copyModal = document.getElementById('copyModal');
const copyInfoContent = document.getElementById('copyInfoContent');
const copySuccess = document.getElementById('copySuccess');
const statusMap = {
'pending': { label: '待处理', class: 'pending', statusClass: 'status-pending' },
'progress': { label: '维修中', class: 'progress', statusClass: 'status-progress' },
'completed': { label: '已完成', class: 'completed', statusClass: 'status-completed' },
'canceled': { label: '已取消', class: 'canceled', statusClass: 'status-canceled' }
};
const urgencyMap = {
'low': '低',
'medium': '中',
'high': '高',
'critical': '紧急'
};
function generateTicketNumber() {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const numbers = '0123456789';
let ticketNumber = '';
ticketNumber += letters.charAt(Math.floor(Math.random() * letters.length));
ticketNumber += letters.charAt(Math.floor(Math.random() * letters.length));
ticketNumber += numbers.charAt(Math.floor(Math.random() * numbers.length));
ticketNumber += numbers.charAt(Math.floor(Math.random() * numbers.length));
if (repairTickets.some(t => t.ticketNumber === ticketNumber)) {
return generateTicketNumber();
}
return ticketNumber;
}
document.addEventListener('DOMContentLoaded', () => {
renderTickets();
updateStats();
updateDatalists();
form.addEventListener('submit', (e) => {
e.preventDefault();
addRepairTicket();
});
searchBtn.addEventListener('click', renderTickets);
searchInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') renderTickets();
});
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderTickets();
});
});
});
function addRepairTicket() {
const ticketNumber = generateTicketNumber();
const device = document.getElementById('device').value.trim();
const problem = document.getElementById('problem').value.trim();
if (device && !deviceHistory.includes(device)) {
deviceHistory.unshift(device);
if (deviceHistory.length > 20) deviceHistory.pop();
localStorage.setItem('deviceHistory', JSON.stringify(deviceHistory));
}
if (problem && !problemHistory.includes(problem)) {
problemHistory.unshift(problem);
if (problemHistory.length > 20) problemHistory.pop();
localStorage.setItem('problemHistory', JSON.stringify(problemHistory));
}
const ticket = {
id: Date.now(),
ticketNumber: ticketNumber,
device: device || '未指定设备',
problem: problem || '未描述故障',
contact: document.getElementById('contact').value,
phone: document.getElementById('phone').value,
location: document.getElementById('location').value,
urgency: document.getElementById('urgency').value,
status: 'pending',
createdAt: new Date().toLocaleString('zh-CN'),
updatedAt: new Date().toLocaleString('zh-CN')
};
repairTickets.unshift(ticket);
saveToLocalStorage();
renderTickets();
updateStats();
updateDatalists();
showNotification(`维修单 ${ticketNumber} 提交成功!`, 'success');
form.reset();
}
function renderTickets() {
const searchTerm = searchInput.value.toLowerCase();
const activeFilter = document.querySelector('.filter-btn.active').dataset.status;
let filteredTickets = repairTickets;
if (activeFilter !== 'all') {
filteredTickets = filteredTickets.filter(ticket => ticket.status === activeFilter);
}
if (searchTerm) {
filteredTickets = filteredTickets.filter(ticket => {
const ticketNumber = (ticket.ticketNumber || '').toString().toLowerCase();
const device = (ticket.device || '').toString().toLowerCase();
const problem = (ticket.problem || '').toString().toLowerCase();
const contact = (ticket.contact || '').toString().toLowerCase();
const phone = (ticket.phone || '').toString().toLowerCase();
return ticketNumber.includes(searchTerm) ||
device.includes(searchTerm) ||
problem.includes(searchTerm) ||
contact.includes(searchTerm) ||
phone.includes(searchTerm);
});
}
if (filteredTickets.length === 0) {
ticketsList.innerHTML = `
<div class="empty-state">
<i class="fas fa-search"></i>
<p>${searchTerm || activeFilter !== 'all' ? '没有找到匹配的工单' : '暂无维修工单,请提交第一个维修单'}</p>
</div>
`;
return;
}
ticketsList.innerHTML = filteredTickets.map(ticket => {
const statusInfo = statusMap[ticket.status];
const urgencyLabel = urgencyMap[ticket.urgency];
const urgencyColor = ticket.urgency === 'critical' ? 'color: var(--danger); font-weight: bold;' : '';
return `
<div class="ticket-item ${statusInfo.class}">
<div class="ticket-header">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<span class="ticket-number">${ticket.ticketNumber}</span>
<span class="ticket-title">${ticket.device}</span>
</div>
<div class="ticket-status ${statusInfo.statusClass}">${statusInfo.label}</div>
</div>
<div class="ticket-body">
<div><strong>问题:</strong>${ticket.problem}</div>
<div><strong>位置:</strong>${ticket.location}</div>
<div><strong>联系人:</strong>${ticket.contact} (${ticket.phone})</div>
<div><strong>紧急程度:</strong><span style="${urgencyColor}">${urgencyLabel}</span></div>
</div>
<div class="ticket-meta">
<span>单号:${ticket.ticketNumber}</span>
<span>提交:${ticket.createdAt}</span>
<span>更新:${ticket.updatedAt}</span>
</div>
<div class="ticket-actions">
<button class="btn btn-sm btn-warning" onclick="updateStatus(${ticket.id})">
<i class="fas fa-sync-alt"></i> 更新状态
</button>
<button class="btn btn-sm btn-success" onclick="completeTicket(${ticket.id})">
<i class="fas fa-check"></i> 完成
</button>
<button class="btn btn-sm btn-danger" onclick="deleteTicket(${ticket.id})">
<i class="fas fa-trash"></i> 删除
</button>
<div class="print-actions">
<button class="btn btn-sm btn-primary" onclick="printTicket(${ticket.id})">
<i class="fas fa-print"></i> 打印
</button>
<button class="btn btn-sm btn-info" onclick="copyTicketInfo(${ticket.id})">
<i class="fas fa-copy"></i> 复制发微信
</button>
</div>
</div>
</div>
`;
}).join('');
}
function printTicket(id) {
const ticket = repairTickets.find(t => t.id === id);
if (!ticket) return;
const urgencyLabel = urgencyMap[ticket.urgency];
const statusLabel = statusMap[ticket.status].label;
const printHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>维修单打印 - ${ticket.ticketNumber}</title>
<style>
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
margin: 20px;
color: #333;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 2px solid #333;
border-radius: 8px;
}
.header {
text-align: center;
margin-bottom: 20px;
border-bottom: 2px solid #333;
padding-bottom: 10px;
}
.header h1 {
margin: 0;
color: #2c3e50;
font-size: 28px;
}
.header p {
margin: 5px 0;
color: #666;
font-size: 14px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin: 20px 0;
}
.info-item {
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #3498db;
}
.info-item.full-width {
grid-column: 1 / -1;
}
.label {
font-weight: bold;
color: #2c3e50;
display: block;
margin-bottom: 5px;
font-size: 14px;
}
.value {
color: #333;
font-size: 16px;
}
.status-bar {
background: #ecf0f1;
padding: 10px;
border-radius: 4px;
text-align: center;
margin: 15px 0;
font-weight: bold;
}
.urgency-high {
color: #e74c3c;
font-weight: bold;
}
.footer {
margin-top: 30px;
padding-top: 15px;
border-top: 2px solid #333;
text-align: center;
font-size: 12px;
color: #666;
}
.footer p {
margin: 3px 0;
}
.print-time {
text-align: right;
font-size: 12px;
color: #999;
margin-bottom: 15px;
}
/* 打印优化 */
@media print {
body {
margin: 0;
background: white;
}
.container {
border: 1px solid #000;
box-shadow: none;
}
.no-print {
display: none;
}
}
/* 打印按钮(仅在打印预览时显示) */
.print-actions {
text-align: center;
margin-top: 20px;
padding: 15px;
background: #f0f0f0;
border-radius: 6px;
}
.print-btn {
padding: 10px 20px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 0 10px;
}
.print-btn:hover {
background: #2980b9;
}
.close-btn {
background: #95a5a6;
}
.close-btn:hover {
background: #7f8c8d;
}
</style>
</head>
<body>
<div class="container">
<div class="print-time">打印时间:${new Date().toLocaleString('zh-CN')}</div>
<div class="header">
<h1>维修服务单</h1>
<p>单号:${ticket.ticketNumber}</p>
<p>状态:${statusLabel}</p>
</div>
<div class="status-bar">
紧急程度:${urgencyLabel} ${ticket.urgency === 'critical' ? ' ⚠️' : ''}
</div>
<div class="info-grid">
<div class="info-item">
<span class="label">设备名称</span>
<span class="value">${ticket.device}</span>
</div>
<div class="info-item">
<span class="label">设备位置</span>
<span class="value">${ticket.location}</span>
</div>
<div class="info-item full-width">
<span class="label">故障描述</span>
<span class="value">${ticket.problem}</span>
</div>
<div class="info-item">
<span class="label">联系人</span>
<span class="value">${ticket.contact}</span>
</div>
<div class="info-item">
<span class="label">联系电话</span>
<span class="value">${ticket.phone}</span>
</div>
<div class="info-item">
<span class="label">提交时间</span>
<span class="value">${ticket.createdAt}</span>
</div>
<div class="info-item">
<span class="label">最后更新</span>
<span class="value">${ticket.updatedAt}</span>
</div>
</div>
<div class="footer">
<p>服务热线:138-0000-0000</p>
<p>服务时间:工作日 9:00-18:00</p>
<p>请保留此单据作为服务凭证</p>
</div>
<div class="print-actions no-print">
<button class="print-btn" onclick="window.print()">🖨️ 打印</button>
<button class="print-btn close-btn" onclick="window.close()">❌ 关闭</button>
</div>
</div>
<script>
// 自动打印(可选)
// setTimeout(() => window.print(), 500);
<\/script>
</body>
</html>
`;
const printWindow = window.open('', '_blank', 'width=800,height=600');
if (!printWindow) {
showNotification('请允许浏览器弹出窗口', 'error');
return;
}
printWindow.document.write(printHTML);
printWindow.document.close();
printWindow.onload = function() {
showNotification('打印窗口已打开,请点击打印按钮', 'success');
};
setTimeout(() => {
if (!printWindow.document.body || printWindow.document.body.innerHTML === '') {
showNotification('浏览器阻止了打印窗口,请手动允许弹出窗口', 'error');
}
}, 1000);
}
function copyTicketInfo(id) {
const ticket = repairTickets.find(t => t.id === id);
if (!ticket) return;
const urgencyLabel = urgencyMap[ticket.urgency];
const statusLabel = statusMap[ticket.status].label;
const info = `【维修工单】${ticket.ticketNumber}
设备:${ticket.device}
问题:${ticket.problem}
位置:${ticket.location}
联系人:${ticket.contact}
电话:${ticket.phone}
紧急程度:${urgencyLabel}
状态:${statusLabel}
提交:${ticket.createdAt}
请尽快安排处理!`;
copyInfoContent.textContent = info;
copyModal.style.display = 'block';
copySuccess.classList.remove('show');
}
function copyToClipboard() {
const info = copyInfoContent.textContent;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(info).then(() => {
showCopySuccess();
}).catch(() => {
fallbackCopy(info);
});
} else {
fallbackCopy(info);
}
}
function fallbackCopy(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showCopySuccess();
} catch (err) {
showNotification('复制失败,请手动复制', 'error');
}
document.body.removeChild(textArea);
}
function showCopySuccess() {
copySuccess.classList.add('show');
showNotification('信息已复制到剪贴板!', 'success');
setTimeout(() => {
copySuccess.classList.remove('show');
}, 2000);
}
function closeCopyModal() {
copyModal.style.display = 'none';
}
window.onclick = function(event) {
if (event.target === copyModal) {
closeCopyModal();
}
}
function updateStatus(id) {
const ticket = repairTickets.find(t => t.id === id);
if (!ticket) return;
const statusOrder = ['pending', 'progress', 'completed'];
const currentIndex = statusOrder.indexOf(ticket.status);
const nextIndex = (currentIndex + 1) % statusOrder.length;
const newStatus = statusOrder[nextIndex];
ticket.status = newStatus;
ticket.updatedAt = new Date().toLocaleString('zh-CN');
saveToLocalStorage();
renderTickets();
updateStats();
showNotification(`工单 ${ticket.ticketNumber} 状态更新为:${statusMap[newStatus].label}`, 'success');
}
function completeTicket(id) {
const ticket = repairTickets.find(t => t.id === id);
if (!ticket) return;
ticket.status = 'completed';
ticket.updatedAt = new Date().toLocaleString('zh-CN');
saveToLocalStorage();
renderTickets();
updateStats();
showNotification(`工单 ${ticket.ticketNumber} 已完成!`, 'success');
}
function deleteTicket(id) {
const ticket = repairTickets.find(t => t.id === id);
if (!ticket) return;
if (confirm(`确定要删除维修单 ${ticket.ticketNumber} 吗?此操作不可恢复。`)) {
repairTickets = repairTickets.filter(t => t.id !== id);
saveToLocalStorage();
renderTickets();
updateStats();
showNotification(`工单 ${ticket.ticketNumber} 已删除`, 'error');
}
}
function updateStats() {
document.getElementById('total-count').textContent = repairTickets.length;
document.getElementById('pending-count').textContent =
repairTickets.filter(t => t.status === 'pending').length;
document.getElementById('progress-count').textContent =
repairTickets.filter(t => t.status === 'progress').length;
}
function updateDatalists() {
const deviceDatalist = document.getElementById('device-list');
const problemDatalist = document.getElementById('problem-list');
deviceDatalist.innerHTML = '';
problemDatalist.innerHTML = '';
deviceHistory.forEach(device => {
const option = document.createElement('option');
option.value = device;
option.textContent = device;
deviceDatalist.appendChild(option);
});
problemHistory.forEach(problem => {
const option = document.createElement('option');
option.value = problem;
option.textContent = problem;
problemDatalist.appendChild(option);
});
updateHistoryDisplay('device');
updateHistoryDisplay('problem');
}
function updateHistoryDisplay(type) {
const container = document.getElementById(`${type}-history-list`);
const history = type === 'device' ? deviceHistory : problemHistory;
if (!container) return;
if (history.length === 0) {
container.innerHTML = '<div style="padding: 8px; color: var(--gray);">暂无历史记录</div>';
return;
}
const usageCount = {};
repairTickets.forEach(ticket => {
const value = ticket[type];
if (value && value !== '未指定设备' && value !== '未描述故障') {
usageCount[value] = (usageCount[value] || 0) + 1;
}
});
container.innerHTML = history.map(item => `
<div class="history-item" onclick="selectHistory('${type}', '${item}')">
${item}
<span class="count">${usageCount[item] || 0}次</span>
</div>
`).join('');
}
function toggleHistory(type) {
const container = document.getElementById(`${type}-history-list`);
container.classList.toggle('show');
}
function selectHistory(type, value) {
document.getElementById(type).value = value;
document.getElementById(`${type}-history-list`).classList.remove('show');
}
function clearInput(id) {
document.getElementById(id).value = '';
document.getElementById(id).focus();
}
function clearHistory(type) {
if (confirm(`确定要清空${type === 'device' ? '设备' : '故障'}的历史记录吗?`)) {
if (type === 'device') {
deviceHistory = [];
localStorage.setItem('deviceHistory', JSON.stringify(deviceHistory));
} else {
problemHistory = [];
localStorage.setItem('problemHistory', JSON.stringify(problemHistory));
}
updateDatalists();
showNotification('历史记录已清空', 'error');
}
}
function saveToLocalStorage() {
localStorage.setItem('repairTickets', JSON.stringify(repairTickets));
}
function showNotification(message, type = 'success') {
notificationText.textContent = message;
notification.className = `notification ${type}`;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
if (repairTickets.length === 0) {
setTimeout(() => {
repairTickets = [
{
id: Date.now() - 10000,
ticketNumber: 'AB12',
device: '服务器主机',
problem: '无法启动,电源指示灯不亮',
contact: '张经理',
phone: '13800138000',
location: '数据中心A区',
urgency: 'critical',
status: 'progress',
createdAt: new Date(Date.now() - 86400000).toLocaleString('zh-CN'),
updatedAt: new Date().toLocaleString('zh-CN')
},
{
id: Date.now() - 20000,
ticketNumber: 'CD34',
device: '打印机',
problem: '打印输出有条纹,色彩不正常',
contact: '李助理',
phone: '13900139000',
location: '办公室B区',
urgency: 'medium',
status: 'pending',
createdAt: new Date(Date.now() - 172800000).toLocaleString('zh-CN'),
updatedAt: new Date(Date.now() - 172800000).toLocaleString('zh-CN')
}
];
deviceHistory = ['服务器主机', '打印机', '显示器', '键盘', '鼠标'];
problemHistory = ['无法启动', '打印异常', '显示问题', '连接失败', '噪音过大'];
saveToLocalStorage();
localStorage.setItem('deviceHistory', JSON.stringify(deviceHistory));
localStorage.setItem('problemHistory', JSON.stringify(problemHistory));
renderTickets();
updateStats();
updateDatalists();
showNotification('已加载示例数据和历史记录', 'success');
}, 1000);
}
</script>
</body>
</html>