<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<title>智慧水务 - 安全巡检子系统</title>
<style>
/* ============================================================
1. 全局CSS变量与基础重置 —— 子系统可在此统一调整品牌色
============================================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--header-h: 56px; /* 顶栏高度 */
--sidebar-w: 240px; /* 侧边栏宽度 */
--tab-h: 56px; /* 底部标签栏高度 */
/* ---- 品牌色系(水利蓝)—— 其他子系统可替换此处颜色 ---- */
--primary: #447dd7; /* 主色 */
--primary-light: #7dacfa;
--primary-soft: #bbd6eb;
--accent: #00a9ea; /* 强调色(随手拍按钮渐变用) */
--accent-dark: #023793;
--success: #52aeef;
--danger: #ff4d4f;
--warning: #faad14;
--bg-body: #f0f4f8; /* 页面背景 */
--bg-card: #ffffff;
--border: #d9e2ec;
--text1: #1d2129; /* 主要文字 */
--text2: #4e5969; /* 次要文字 */
--radius: 8px;
--transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-body);
color: var(--text1);
-webkit-tap-highlight-color: transparent;
overflow: hidden; /* 彻底禁止页面级滚动条 */
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
/* 全局滚动条隐藏(但容器内部仍可滚动) */
::-webkit-scrollbar {
display: none;
}
* {
scrollbar-width: none;
-ms-overflow-style: none;
}
/* ============================================================
2. 页面骨架(Flex大框架)
============================================================ */
.layout {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
/* ---- 顶栏 ---- */
.header {
height: var(--header-h);
background: var(--primary);
color: #fff;
display: flex;
align-items: center;
padding: 0 16px;
flex-shrink: 0;
z-index: 100;
gap: 12px;
}
.menu-btn {
display: none;
background: none;
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
padding: 8px;
border-radius: 4px;
}
.logo {
font-weight: 700;
font-size: 18px;
white-space: nowrap;
display: flex;
align-items: center;
gap: 8px;
}
.logo-icon {
width: 28px;
height: 28px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: var(--primary);
}
/* ---- 桌面端一级导航(量水测水、配水调度等)—— 移动端隐藏 ---- */
.top-nav {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
}
.top-nav-item {
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
}
.top-nav-item:hover,
.top-nav-item.active {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.top-nav-item.active {
font-weight: 700;
}
/* 用户下拉菜单 */
.user-dropdown {
position: relative;
cursor: pointer;
}
.user-dropdown-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
}
.user-dropdown-trigger:hover {
background: rgba(255, 255, 255, 0.15);
}
.user-dropdown-menu {
display: none;
position: absolute;
right: 0;
top: 100%;
margin-top: 4px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 150px;
padding: 8px 0;
z-index: 200;
}
.user-dropdown-menu a {
display: block;
padding: 10px 16px;
color: var(--text1);
text-decoration: none;
font-size: 14px;
}
.user-dropdown-menu a:hover {
background: #f5f7fa;
}
.show {
display: block;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #fff;
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
/* ---- 主体区域(侧边栏 + 内容) ---- */
.body {
display: flex;
flex: 1;
min-height: 0;
position: relative;
}
/* 侧边栏:子系统内部菜单 */
.sidebar {
width: var(--sidebar-w);
background: #fff;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 16px 0;
overflow-y: auto;
flex-shrink: 0;
transition: transform var(--transition);
z-index: 90;
}
.sidebar-title {
padding: 0 20px 12px;
font-size: 14px;
color: var(--text2);
font-weight: 600;
border-bottom: 1px solid var(--border);
margin-bottom: 8px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
color: var(--text2);
font-size: 15px;
cursor: pointer;
min-height: 48px;
border-left: 3px solid transparent;
}
.nav-item.active,
.nav-item:hover {
background: #f0f5ff;
color: var(--primary);
border-left-color: var(--primary);
font-weight: 500;
}
/* 遮罩层(移动端抽屉用) */
.overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 80;
}
/* 主内容区 */
.main {
flex: 1;
overflow-y: auto;
padding: 20px;
background: var(--bg-body);
}
/* ============================================================
3. 移动端底部标签栏 + 中间凸起按钮
============================================================ */
.tab-bar {
display: none; /* 桌面端隐藏,移动端显示 */
position: relative;
height: var(--tab-h);
background: #fff;
border-top: 1px solid var(--border);
flex-shrink: 0;
z-index: 100;
padding-bottom: env(safe-area-inset-bottom);
}
.tab-bar-inner {
display: flex;
height: 100%;
align-items: center;
justify-content: space-around;
position: relative;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
flex: 1;
height: 100%;
color: var(--text2);
font-size: 12px;
cursor: pointer;
min-height: 44px;
}
.tab-item.active {
color: var(--primary);
font-weight: 500;
}
.tab-icon {
font-size: 22px;
line-height: 1;
}
/* 中间凸起按钮(随手拍) */
.center-fab-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 100%;
}
.center-fab {
position: absolute;
top: -28px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, var(--success), var(--accent-dark));
color: #fff;
border: 4px solid #fff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
cursor: pointer;
z-index: 5;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.center-fab:active {
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.center-fab-label {
position: absolute;
bottom: -18px;
font-size: 11px;
color: var(--text2);
white-space: nowrap;
}
/* ============================================================
4. 通用组件样式(按钮、标签、卡片、表单、模态框等)
============================================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
border-radius: 6px;
font-size: 15px;
border: none;
cursor: pointer;
font-weight: 500;
min-height: 44px;
min-width: 44px;
transition: opacity 0.2s;
}
.btn-primary {
background: var(--primary);
color: #fff;
}
.btn-danger {
background: var(--danger);
color: #fff;
}
.btn-outline {
background: #fff;
border: 1px solid var(--border);
color: var(--text1);
}
.btn-accent {
background: var(--accent);
color: #fff;
}
.btn:active {
opacity: 0.8;
}
.btn-sm {
padding: 6px 12px;
font-size: 13px;
min-height: 36px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
background: #e6f7ff;
color: var(--primary);
}
.badge.success {
background: #e6fffa;
color: #00a86b;
}
.badge.warning {
background: #fff7e6;
color: var(--warning);
}
.badge.danger {
background: #fff2f0;
color: var(--danger);
}
.card-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: #fff;
border-radius: var(--radius);
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.stat-title {
font-size: 14px;
color: var(--text2);
}
.stat-value {
font-size: 28px;
font-weight: 700;
margin: 8px 0;
}
.search-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
align-items: center;
}
.search-input,
.search-select {
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 14px;
font-size: 15px;
min-height: 44px;
outline: none;
flex: 1;
min-width: 150px;
}
/* 表格(禁止横向滚动条,采用fixed布局适配列宽) */
.table-wrapper {
background: #fff;
border-radius: var(--radius);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.data-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.data-table th,
.data-table td {
padding: 12px 8px;
text-align: left;
border-bottom: 1px solid var(--border);
font-size: 14px;
word-break: break-word;
}
.data-table th {
background: #f7f9fc;
font-weight: 600;
color: var(--text2);
}
/* 列宽比例可根据子系统字段调整 */
.data-table th:nth-child(1),
.data-table td:nth-child(1) {
width: 15%;
}
.data-table th:nth-child(2),
.data-table td:nth-child(2) {
width: 25%;
}
.data-table th:nth-child(3),
.data-table td:nth-child(3) {
width: 15%;
}
.data-table th:nth-child(4),
.data-table td:nth-child(4) {
width: 15%;
}
.data-table th:nth-child(5),
.data-table td:nth-child(5) {
width: 15%;
}
.data-table th:nth-child(6),
.data-table td:nth-child(6) {
width: 15%;
}
/* 移动端卡片列表 */
.mobile-cards {
display: none;
}
.task-card {
background: #fff;
border-radius: var(--radius);
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
position: relative;
}
.task-card-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.task-card-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
font-size: 14px;
color: var(--text2);
}
.more-btn {
position: absolute;
right: 12px;
bottom: 12px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
padding: 8px;
color: var(--text2);
}
/* 模态框(新建/编辑、检查表、随手拍、维修、确认) */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 150;
align-items: center;
justify-content: center;
}
.modal {
background: #fff;
border-radius: 12px;
padding: 24px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
}
.form-input,
.form-textarea {
width: 100%;
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px;
font-size: 15px;
min-height: 44px;
}
.form-textarea {
resize: vertical;
}
/* 检查表内样式 */
.check-item {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.check-item .status-toggle {
display: flex;
gap: 16px;
}
.status-toggle label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.abnormal-detail {
display: none;
width: 100%;
margin-top: 8px;
}
/* Toast消息提示 */
.toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
z-index: 300;
font-size: 15px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show {
opacity: 1;
}
/* ============================================================
5. 响应式断点设计
============================================================ */
@media (max-width: 1024px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.top-nav {
display: none;
} /* 移动端隐藏一级导航 */
.menu-btn {
display: block;
} /* 显示汉堡菜单 */
.sidebar {
/* 侧边栏变为抽屉 */
position: fixed;
top: var(--header-h);
left: 0;
bottom: 0;
transform: translateX(-100%);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
z-index: 100;
}
.sidebar.open {
transform: translateX(0);
}
.overlay.show {
display: block;
}
.tab-bar {
display: block;
} /* 显示底部标签栏 */
.card-grid {
grid-template-columns: 1fr;
}
.table-wrapper {
display: none;
} /* 隐藏表格,改为卡片列表 */
.mobile-cards {
display: block;
}
.main {
padding: 12px;
padding-bottom: calc(
12px + var(--tab-h) + env(safe-area-inset-bottom)
);
}
.search-bar {
flex-direction: column;
}
.search-input,
.search-select {
width: 100%;
}
}
</style>
</head>
<body>
<!-- ============================================================
6. HTML结构
============================================================ -->
<div class="layout">
<!-- 顶栏:logo + 桌面端一级导航 + 用户信息 -->
<header class="header">
<button class="menu-btn" id="menuBtn">☰</button>
<div class="logo">
<span class="logo-icon">💧</span><span>智慧水务</span>
</div>
<nav class="top-nav" id="topNav">
<!-- 以下5个为一级导航,可根据子系统增减或修改高亮 -->
<a class="top-nav-item" data-nav="measure">量水测水</a>
<a class="top-nav-item" data-nav="dispatch">配水调度</a>
<a class="top-nav-item active" data-nav="inspection">安全巡检</a>
<a class="top-nav-item" data-nav="quality">水质水情</a>
<a class="top-nav-item" data-nav="system">系统管理</a>
<div class="user-dropdown" id="userDropdown">
<div class="user-dropdown-trigger">
<div class="user-avatar">张</div>
<span>张工</span><span>▼</span>
</div>
<div class="user-dropdown-menu" id="dropdownMenu">
<a href="#">个人中心</a><a href="#">修改密码</a
><a href="#" id="logoutBtn">退出登录</a>
</div>
</div>
</nav>
</header>
<div class="overlay" id="overlay"></div>
<div class="body">
<!-- 侧边栏:子系统内部菜单 -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-title">安全巡检子系统</div>
<a class="nav-item active" data-page="home">📊 首页</a>
<a class="nav-item" data-page="monitor">📋 监测任务</a>
<a class="nav-item" data-page="warn">⚠️ 预警管理</a>
<a class="nav-item" data-page="report">📈 统计分析</a>
<a class="nav-item" data-page="settings">⚙️ 系统设置</a>
</aside>
<!-- 主内容区:多个面板(使用display切换) -->
<main class="main" id="mainContent">
<!-- 面板1:首页仪表盘 -->
<section id="panel-home" class="panel" style="display: block">
<h2 style="margin-bottom: 16px">巡检仪表盘</h2>
<div class="card-grid">
<div class="stat-card">
<div class="stat-title">总任务</div>
<div class="stat-value" id="statTotal">0</div>
</div>
<div class="stat-card">
<div class="stat-title">进行中</div>
<div class="stat-value" id="statOngoing">0</div>
</div>
<div class="stat-card">
<div class="stat-title">已完成</div>
<div class="stat-value" id="statDone">0</div>
</div>
<div class="stat-card">
<div class="stat-title">未读预警</div>
<div class="stat-value" id="statAlerts">0</div>
</div>
</div>
<h3>最近任务</h3>
<div id="recentTasksTable" class="table-wrapper"></div>
<div id="recentTasksCards" class="mobile-cards"></div>
</section>
<!-- 面板2:监测任务 -->
<section id="panel-monitor" class="panel" style="display: none">
<h2>巡检监测任务</h2>
<div class="search-bar">
<input
type="search"
class="search-input"
placeholder="搜索任务编号/巡检点"
id="monitorSearch"
/>
<select class="search-select" id="monitorStatus">
<option value="">全部状态</option>
<option value="pending">待巡检</option>
<option value="ongoing">巡检中</option>
<option value="defect">待修复</option>
<option value="done">已完成</option>
</select>
<button class="btn btn-primary" id="monitorSearchBtn">
搜索
</button>
<button class="btn btn-outline" id="monitorResetBtn">重置</button>
<button
class="btn btn-accent"
style="margin-left: auto"
id="addTaskBtn"
>
+ 新建任务
</button>
</div>
<div id="monitorTableWrapper" class="table-wrapper"></div>
<div id="monitorMobileCards" class="mobile-cards"></div>
</section>
<!-- 面板3:预警管理 -->
<section id="panel-warn" class="panel" style="display: none">
<h2>缺陷预警管理</h2>
<div id="defectTableWrapper" class="table-wrapper"></div>
<div id="defectMobileCards" class="mobile-cards"></div>
</section>
<!-- 其他占位面板 -->
<section id="panel-report" class="panel" style="display: none">
<h2>统计分析</h2>
<p>图表与报表功能开发中...</p>
</section>
<section id="panel-settings" class="panel" style="display: none">
<h2>系统设置</h2>
<p>参数配置页面...</p>
</section>
</main>
</div>
<!-- 移动端底部标签栏(含中间凸起按钮) -->
<nav class="tab-bar" id="tabBar">
<div class="tab-bar-inner">
<div class="tab-item active" data-page="home">
<span class="tab-icon">💧</span><span>首页</span>
</div>
<div class="tab-item" data-page="monitor">
<span class="tab-icon">🔍</span><span>巡检</span>
</div>
<div class="center-fab-wrapper">
<button class="center-fab" id="centerFab">📸</button>
<span class="center-fab-label">随手拍</span>
</div>
<div class="tab-item" data-page="warn">
<span class="tab-icon">⚠️</span><span>预警</span>
</div>
<div class="tab-item" data-page="profile">
<span class="tab-icon">👤</span><span>我的</span>
</div>
</div>
</nav>
<!-- ============================================================
7. 模态框集合(根据需要复用)
============================================================ -->
<!-- 任务表单(新建/编辑) -->
<div class="modal-overlay" id="taskModal">
<div class="modal">
<h3 id="taskModalTitle">新建巡检任务</h3>
<div class="form-group">
<label>巡检点</label><input class="form-input" id="taskSite" />
</div>
<div class="form-group">
<label>计划日期</label
><input class="form-input" type="date" id="taskDate" />
</div>
<div class="form-group">
<label>巡检人</label><input class="form-input" id="taskPerson" />
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end">
<button class="btn btn-outline" id="cancelTaskBtn">取消</button>
<button class="btn btn-primary" id="saveTaskBtn">保存</button>
</div>
</div>
</div>
<!-- 检查表模态框 -->
<div class="modal-overlay" id="checkModal">
<div class="modal" style="max-width: 600px">
<h3>水闸启闭机检查表</h3>
<p style="color: var(--text2)">
任务编号:<span id="checkTaskNo"></span> | 巡检点:<span
id="checkTaskSite"
></span>
</p>
<div id="checkItemsContainer"></div>
<div class="form-group">
<label>备注</label
><textarea
class="form-textarea"
id="checkRemark"
rows="2"
></textarea>
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end">
<button class="btn btn-outline" id="cancelCheckBtn">返回</button>
<button class="btn btn-primary" id="submitCheckBtn">
提交检查结果
</button>
</div>
</div>
</div>
<!-- 随手拍模态框 -->
<div class="modal-overlay" id="snapModal">
<div class="modal">
<h3>📸 随手拍 - 快速上报异常</h3>
<div class="form-group">
<label>部位/设施</label><input class="form-input" id="snapSite" />
</div>
<div class="form-group">
<label>异常描述</label
><textarea class="form-textarea" id="snapDesc" rows="2"></textarea>
</div>
<div class="form-group">
<button class="btn btn-outline btn-sm" id="snapPhotoBtn">
📷 拍照 (自动水印)
</button>
<span style="font-size: 12px; color: var(--text2)">
GPS自动获取</span
>
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end">
<button class="btn btn-outline" id="cancelSnapBtn">取消</button>
<button class="btn btn-primary" id="submitSnapBtn">提交</button>
</div>
</div>
</div>
<!-- 维修反馈模态框 -->
<div class="modal-overlay" id="repairModal">
<div class="modal">
<h3>维修反馈</h3>
<div class="form-group">
<label>维修方法</label
><textarea
class="form-textarea"
id="repairMethod"
rows="2"
></textarea>
</div>
<div class="form-group">
<label>更换备件</label><input class="form-input" id="repairParts" />
</div>
<button class="btn btn-outline btn-sm" id="fakeRepairPhoto">
📷 上传修复照片
</button>
<div
style="
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
"
>
<button class="btn btn-outline" id="cancelRepairBtn">取消</button>
<button class="btn btn-primary" id="confirmRepairBtn">
提交维修
</button>
</div>
</div>
</div>
<!-- 确认弹窗 -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width: 300px; text-align: center">
<h3 id="confirmTitle"></h3>
<p id="confirmMsg"></p>
<div
style="
display: flex;
gap: 12px;
justify-content: center;
margin-top: 20px;
"
>
<button class="btn btn-outline" id="confirmCancel">取消</button>
<button class="btn btn-danger" id="confirmOk">确定</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
</div>
<!-- ============================================================
8. JavaScript 业务逻辑
============================================================ -->
<script>
;(function () {
// ---------- 模拟数据(可替换为API请求)----------
let tasks = [
{
id: 1,
no: 'XJ-20260701',
site: '水库大坝A段',
person: '李四',
date: '2026-07-16',
status: 'pending',
},
{
id: 2,
no: 'XJ-20260702',
site: '泄洪闸控制室',
person: '王五',
date: '2026-07-15',
status: 'done',
checkResult: {
items: [
{ name: '电动机', normal: true },
{ name: '减速器', normal: true },
{ name: '钢丝绳', normal: true },
{ name: '制动器', normal: true },
{ name: '限位开关', normal: true },
],
},
},
{
id: 3,
no: 'XJ-20260703',
site: '河道护岸B区',
person: '赵六',
date: '2026-07-14',
status: 'defect',
checkResult: {
items: [
{ name: '电动机', normal: true },
{ name: '减速器', normal: false, desc: '输入轴渗油' },
{ name: '钢丝绳', normal: true },
{ name: '制动器', normal: true },
{ name: '限位开关', normal: true },
],
},
},
{
id: 4,
no: 'XJ-20260704',
site: '泵站设备间',
person: '李四',
date: '2026-07-16',
status: 'ongoing',
},
]
let defects = [
{
id: 1,
taskId: 3,
taskNo: 'XJ-20260703',
site: '河道护岸B区',
item: '减速器',
desc: '输入轴渗油,油位低',
status: 'pending_repair',
repairMethod: '',
repairPhoto: null,
},
]
// 状态映射(可扩展)
const statusMap = {
pending: '待巡检',
ongoing: '巡检中',
defect: '待修复',
done: '已完成',
}
const statusBadge = {
pending: 'warning',
ongoing: '',
defect: 'danger',
done: 'success',
}
const defectStatusMap = {
pending_repair: '待维修',
repaired: '已维修待复核',
closed: '已销号',
}
const defectBadge = {
pending_repair: 'warning',
repaired: '',
closed: 'success',
}
let currentTaskId = null
let currentDefectId = null
let editingTaskId = null
let activePanel = 'home'
// 快捷选择器
const $ = (s) => document.querySelector(s)
const $$ = (s) => document.querySelectorAll(s)
// ---------- 工具函数 ----------
function showToast(msg) {
const t = $('#toast')
t.textContent = msg
t.classList.add('show')
setTimeout(() => t.classList.remove('show'), 2000)
}
// ---------- 统计与渲染 ----------
function updateStats() {
$('#statTotal').textContent = tasks.length
$('#statOngoing').textContent = tasks.filter(
(t) => t.status === 'ongoing' || t.status === 'pending',
).length
$('#statDone').textContent = tasks.filter(
(t) => t.status === 'done',
).length
$('#statAlerts').textContent = defects.filter(
(d) => d.status !== 'closed',
).length
}
// 通用表格渲染(避免横向滚动条)
function renderTable(containerId, data, columns, actions) {
let html = `<table class="data-table"><thead><tr>${columns.map((c) => `<th>${c}</th>`).join('')}</tr></thead><tbody>`
data.forEach((row) => {
html += '<tr>'
columns.forEach((col) => {
let val = row[col] ?? ''
if (col === 'status')
val = `<span class="badge ${statusBadge[row.status] || ''}">${statusMap[row.status] || val}</span>`
html += `<td>${val}</td>`
})
if (actions) html += `<td>${actions(row)}</td>`
html += '</tr>'
})
html += '</tbody></table>'
document.getElementById(containerId).innerHTML = html
}
function renderRecentTasks() {
const recent = tasks.slice(0, 4)
renderTable('recentTasksTable', recent, [
'no',
'site',
'person',
'date',
'status',
])
let cards = ''
recent.forEach((t) => {
cards += `<div class="task-card"><div class="task-card-header"><strong>${t.no}</strong><span class="badge ${statusBadge[t.status]}">${statusMap[t.status]}</span></div><div class="task-card-body"><div>📍 ${t.site}</div><div>👤 ${t.person}</div><div>📅 ${t.date}</div></div></div>`
})
$('#recentTasksCards').innerHTML = cards
}
function renderMonitorTasks() {
const kw = $('#monitorSearch')?.value.trim().toLowerCase() || ''
const st = $('#monitorStatus')?.value || ''
let filtered = tasks.filter(
(t) =>
(!kw ||
t.no.includes(kw) ||
t.site.includes(kw) ||
t.person.includes(kw)) &&
(!st || t.status === st),
)
const columns = ['no', 'site', 'person', 'date', 'status']
const actions = (t) =>
`${t.status !== 'done' ? `<button class="btn btn-sm btn-outline start-task" data-id="${t.id}">巡检</button>` : ''} <button class="btn btn-sm btn-outline edit-task-btn" data-id="${t.id}">✏️</button> <button class="btn btn-sm btn-outline delete-task-btn" data-id="${t.id}">🗑</button>`
renderTable('monitorTableWrapper', filtered, columns, actions)
let cards = ''
filtered.forEach((t) => {
cards += `<div class="task-card"><div class="task-card-header"><strong>${t.no}</strong><span class="badge ${statusBadge[t.status]}">${statusMap[t.status]}</span></div><div class="task-card-body"><div>📍 ${t.site}</div><div>👤 ${t.person}</div><div>📅 ${t.date}</div></div><button class="more-btn monitor-more" data-id="${t.id}">•••</button></div>`
})
$('#monitorMobileCards').innerHTML = cards
attachMonitorEvents()
}
function renderDefects() {
const columns = ['taskNo', 'site', 'item', 'desc', 'status']
const actions = (d) =>
`${d.status === 'pending_repair' ? `<button class="btn btn-sm btn-primary repair-btn" data-id="${d.id}">维修</button>` : ''} ${d.status === 'repaired' ? `<button class="btn btn-sm btn-outline review-btn" data-id="${d.id}">复核</button>` : ''}`
renderTable('defectTableWrapper', defects, columns, actions)
let cards = ''
defects.forEach((d) => {
cards += `<div class="task-card"><div class="task-card-header"><strong>${d.taskNo}</strong><span class="badge ${defectBadge[d.status]}">${defectStatusMap[d.status]}</span></div><div class="task-card-body"><div>📍 ${d.site}</div><div>⚠️ ${d.item}</div><div>📝 ${d.desc}</div></div><div style="margin-top:8px;">${d.status === 'pending_repair' ? `<button class="btn btn-sm btn-primary repair-btn" data-id="${d.id}">维修接单</button>` : ''} ${d.status === 'repaired' ? `<button class="btn btn-sm btn-outline review-btn" data-id="${d.id}">复核</button>` : ''}</div></div>`
})
$('#defectMobileCards').innerHTML = cards
attachDefectEvents()
}
// 事件绑定(监测任务)
function attachMonitorEvents() {
$$('.start-task').forEach(
(b) => (b.onclick = () => openCheckModal(parseInt(b.dataset.id))),
)
$$('.edit-task-btn').forEach(
(b) => (b.onclick = () => openTaskModal(parseInt(b.dataset.id))),
)
$$('.delete-task-btn').forEach(
(b) =>
(b.onclick = () => {
currentTaskId = parseInt(b.dataset.id)
showConfirm('删除任务', '确认删除?', () => {
tasks = tasks.filter((t) => t.id !== currentTaskId)
refreshAll()
showToast('已删除')
})
}),
)
$$('.monitor-more').forEach(
(b) =>
(b.onclick = () => {
currentTaskId = parseInt(b.dataset.id)
alert('操作:开始巡检/编辑/删除') // 移动端简化处理
}),
)
}
function attachDefectEvents() {
$$('.repair-btn').forEach(
(b) =>
(b.onclick = () => {
currentDefectId = parseInt(b.dataset.id)
openRepairModal()
}),
)
$$('.review-btn').forEach(
(b) =>
(b.onclick = () => {
currentDefectId = parseInt(b.dataset.id)
showConfirm('复核确认', '确认现场复核合格?', () => {
const d = defects.find((d) => d.id === currentDefectId)
if (d) {
d.status = 'closed'
const t = tasks.find((t) => t.id === d.taskId)
if (
t &&
defects.filter(
(dd) => dd.taskId === t.id && dd.status !== 'closed',
).length === 0
)
t.status = 'done'
}
refreshAll()
showToast('缺陷已销号')
})
}),
)
}
function refreshAll() {
updateStats()
renderRecentTasks()
if (activePanel === 'monitor') renderMonitorTasks()
if (activePanel === 'warn') renderDefects()
if (activePanel === 'home') {
updateStats()
renderRecentTasks()
}
}
// ---------- 面板切换 ----------
function switchPanel(page) {
$$('.panel').forEach((p) => (p.style.display = 'none'))
const panel = document.getElementById(`panel-${page}`)
if (panel) panel.style.display = 'block'
activePanel = page
// 侧边栏高亮
$$('.nav-item').forEach((i) => i.classList.remove('active'))
document
.querySelector(`.nav-item[data-page="${page}"]`)
?.classList.add('active')
// 底部标签栏高亮
$$('.tab-item').forEach((i) => i.classList.remove('active'))
document
.querySelector(`.tab-item[data-page="${page}"]`)
?.classList.add('active')
if (page === 'monitor') renderMonitorTasks()
if (page === 'warn') renderDefects()
if (page === 'home') {
updateStats()
renderRecentTasks()
}
if (window.innerWidth <= 768) closeSidebar()
}
// ---------- 任务表单模态框 ----------
function openTaskModal(id = null) {
editingTaskId = id
if (id) {
const t = tasks.find((t) => t.id === id)
if (!t) return
$('#taskSite').value = t.site
$('#taskDate').value = t.date
$('#taskPerson').value = t.person
$('#taskModalTitle').textContent = '编辑巡检任务'
} else {
$('#taskSite').value = ''
$('#taskDate').value = new Date().toISOString().slice(0, 10)
$('#taskPerson').value = '张工'
$('#taskModalTitle').textContent = '新建巡检任务'
}
$('#taskModal').style.display = 'flex'
}
$('#addTaskBtn').onclick = () => openTaskModal()
$('#cancelTaskBtn').onclick = () =>
($('#taskModal').style.display = 'none')
$('#saveTaskBtn').onclick = () => {
const site = $('#taskSite').value.trim(),
date = $('#taskDate').value,
person = $('#taskPerson').value.trim()
if (!site || !date) return showToast('请填写完整')
if (editingTaskId) {
const t = tasks.find((t) => t.id === editingTaskId)
if (t) {
t.site = site
t.date = date
t.person = person
}
} else {
tasks.unshift({
id: Date.now(),
no:
'XJ-' +
date.replace(/-/g, '') +
'-' +
Math.floor(Math.random() * 100),
site,
date,
person,
status: 'pending',
})
}
$('#taskModal').style.display = 'none'
refreshAll()
showToast(editingTaskId ? '已更新' : '已创建')
editingTaskId = null
}
// ---------- 检查表逻辑 ----------
const checkItems = ['电动机', '减速器', '钢丝绳', '制动器', '限位开关']
function openCheckModal(taskId) {
const task = tasks.find((t) => t.id === taskId)
if (!task) return
currentTaskId = taskId
$('#checkTaskNo').textContent = task.no
$('#checkTaskSite').textContent = task.site
let html = ''
checkItems.forEach((item) => {
html += `<div class="check-item"><strong>${item}</strong><div class="status-toggle"><label><input type="radio" name="check_${item}" value="normal" checked>正常</label><label><input type="radio" name="check_${item}" value="abnormal">异常</label></div><div class="abnormal-detail" id="detail_${item}"><input class="form-input" placeholder="异常描述" id="desc_${item}"><button class="btn btn-outline btn-sm" onclick="alert('模拟拍照')">📷</button></div></div>`
})
$('#checkItemsContainer').innerHTML = html
checkItems.forEach((item) => {
document.getElementsByName(`check_${item}`).forEach((r) =>
r.addEventListener('change', (e) => {
document.getElementById(`detail_${item}`).style.display =
e.target.value === 'abnormal' ? 'block' : 'none'
}),
)
})
$('#checkModal').style.display = 'flex'
}
$('#cancelCheckBtn').onclick = () =>
($('#checkModal').style.display = 'none')
$('#submitCheckBtn').onclick = () => {
const task = tasks.find((t) => t.id === currentTaskId)
if (!task) return
const resultItems = checkItems.map((item) => {
const normal = document.querySelector(
`input[name="check_${item}"][value="normal"]`,
).checked
const desc = normal
? ''
: document.getElementById(`desc_${item}`)?.value || ''
return {
name: item,
normal,
desc,
photo: normal ? null : 'sim.jpg',
}
})
task.checkResult = { items: resultItems }
if (resultItems.some((r) => !r.normal)) {
task.status = 'defect'
resultItems
.filter((r) => !r.normal)
.forEach((ai) => {
defects.push({
id: Date.now() + Math.random(),
taskId: task.id,
taskNo: task.no,
site: task.site,
item: ai.name,
desc: ai.desc || '异常',
status: 'pending_repair',
repairMethod: '',
repairPhoto: null,
})
})
showToast('已生成缺陷通知单')
} else {
task.status = 'done'
showToast('检查完成')
}
$('#checkModal').style.display = 'none'
refreshAll()
}
// 随手拍
$('#centerFab').onclick = () => {
$('#snapSite').value = ''
$('#snapDesc').value = ''
$('#snapModal').style.display = 'flex'
}
$('#cancelSnapBtn').onclick = () =>
($('#snapModal').style.display = 'none')
$('#snapPhotoBtn').onclick = () => alert('模拟拍照,自动叠加水印')
$('#submitSnapBtn').onclick = () => {
const site = $('#snapSite').value.trim(),
desc = $('#snapDesc').value.trim()
if (!site || !desc) return showToast('请填写部位和描述')
const taskId = Date.now()
const taskNo =
'SP-' +
new Date().toISOString().slice(0, 10).replace(/-/g, '') +
'-' +
Math.floor(Math.random() * 100)
tasks.unshift({
id: taskId,
no: taskNo,
site,
person: '张工',
date: new Date().toISOString().slice(0, 10),
status: 'defect',
})
defects.unshift({
id: Date.now() + 1,
taskId,
taskNo,
site,
item: '随手拍上报',
desc,
status: 'pending_repair',
repairMethod: '',
repairPhoto: null,
})
$('#snapModal').style.display = 'none'
refreshAll()
showToast('随手拍异常已上报')
}
// 维修
function openRepairModal() {
$('#repairMethod').value = ''
$('#repairParts').value = ''
$('#repairModal').style.display = 'flex'
}
$('#cancelRepairBtn').onclick = () =>
($('#repairModal').style.display = 'none')
$('#fakeRepairPhoto').onclick = () => alert('模拟上传修复照片')
$('#confirmRepairBtn').onclick = () => {
const d = defects.find((d) => d.id === currentDefectId)
if (!d) return
d.repairMethod = $('#repairMethod').value || '已完成维修'
d.repairPhoto = 'repair.jpg'
d.status = 'repaired'
$('#repairModal').style.display = 'none'
refreshAll()
showToast('维修完成,待复核')
}
// 通用确认弹窗
function showConfirm(title, msg, cb) {
$('#confirmTitle').textContent = title
$('#confirmMsg').textContent = msg
$('#confirmModal').style.display = 'flex'
const ok = () => {
cb()
$('#confirmModal').style.display = 'none'
$('#confirmOk').removeEventListener('click', ok)
}
$('#confirmOk').addEventListener('click', ok)
$('#confirmCancel').onclick = () => {
$('#confirmModal').style.display = 'none'
$('#confirmOk').removeEventListener('click', ok)
}
}
// ---------- 导航交互 ----------
function openSidebar() {
$('#sidebar').classList.add('open')
$('#overlay').classList.add('show')
document.body.style.overflow = 'hidden'
}
function closeSidebar() {
$('#sidebar').classList.remove('open')
$('#overlay').classList.remove('show')
document.body.style.overflow = ''
}
$('#menuBtn').onclick = () =>
$('#sidebar').classList.contains('open')
? closeSidebar()
: openSidebar()
$('#overlay').onclick = closeSidebar
window.addEventListener('resize', () => {
if (window.innerWidth > 768) closeSidebar()
})
// 侧边栏菜单点击
$$('.nav-item').forEach((item) =>
item.addEventListener('click', () => switchPanel(item.dataset.page)),
)
// 底部标签栏点击(排除“我的”占位)
$$('.tab-item').forEach((item) => {
item.addEventListener('click', () => {
const page = item.dataset.page
if (page === 'profile') {
showToast('个人中心开发中')
return
}
switchPanel(page)
})
})
// 桌面端一级导航切换(用于演示不同子系统,实际开发中可替换为页面跳转)
$$('.top-nav-item').forEach((item) => {
item.addEventListener('click', (e) => {
e.preventDefault()
$$('.top-nav-item').forEach((i) => i.classList.remove('active'))
item.classList.add('active')
showToast(
`已切换到 ${item.textContent} 模块(此为演示,实际需路由)`,
)
})
})
// 用户下拉菜单
document
.getElementById('userDropdown')
.addEventListener('click', (e) => {
e.stopPropagation()
document.getElementById('dropdownMenu').classList.toggle('show')
})
document.addEventListener('click', () =>
document.getElementById('dropdownMenu').classList.remove('show'),
)
$('#logoutBtn')?.addEventListener('click', (e) => {
e.preventDefault()
showToast('已退出登录')
})
// 监测搜索与重置
$('#monitorSearchBtn')?.addEventListener('click', () =>
renderMonitorTasks(),
)
$('#monitorResetBtn')?.addEventListener('click', () => {
$('#monitorSearch').value = ''
$('#monitorStatus').value = ''
renderMonitorTasks()
})
// 启动
switchPanel('home')
updateStats()
renderRecentTasks()
})()
</script>
</body>
</html>