# 郑州智慧工地 H5 原型实施计划 > **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. **Goal:** 构建可运行的 H5 原型,包含 10 个页面、Mock 数据、智慧工地风 UI,运行在浏览器中直接打开。 **Architecture:** 纯前端 HTML/CSS/JS,无构建步骤。Mock 数据内嵌于 JS,所有 API 调用替换为本地 mock + setTimeout 模拟异步延迟。CSS 变量统一管理主题色。 **Tech Stack:** HTML5 / CSS3 / Vanilla JS / ECharts(CDN) / iconfont(CDN) --- ## 前置准备 ### Task 1: 创建项目目录结构 **Objective:** 创建 `h5/` 目录及子目录 **Files:** - Create: `h5/css/` - Create: `h5/js/` **Step 1: 创建目录** ```bash mkdir -p h5/css h5/js ``` **Step 2: 验证** ```bash ls -la h5/ ``` Expected output: ``` h5/ ├── css/ └── js/ ``` --- ### Task 2: 创建 CSS 变量定义文件 **Objective:** 建立主题色变量和全局重置样式 **Files:** - Create: `h5/css/variables.css` **Step 1: 创建 variables.css** ```css /* 智慧工地主题色 */ :root { /* 主色 */ --color-primary: #1e3a5f; --color-primary-light: #2d5a8a; --color-secondary: #3b82f6; /* 警示色 */ --color-danger: #f97316; --color-warning: #eab308; --color-success: #22c55e; /* 中性色 */ --color-bg: #f8fafc; --color-card: #ffffff; --color-text: #1f2937; --color-text-secondary: #6b7280; --color-border: #e5e7eb; /* 状态色 */ --color-online: #22c55e; --color-offline: #9ca3af; /* 字体 */ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; --font-mono: 'SF Mono', 'Fira Code', Consolas, monospace; } /* 全局重置 */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html, body { font-family: var(--font-family); background-color: var(--color-bg); color: var(--color-text); font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiwebkit; -webkit-tap-highlight-color: transparent; } a { color: var(--color-secondary); text-decoration: none; } button { font-family: inherit; cursor: pointer; border: none; outline: none; } input, textarea, select { font-family: inherit; font-size: inherit; } /* 工具类 */ .flex { display: flex; } .flex-col { flex-direction: column; } .items-center { align-items: center; } .justify-between { justify-content: space-between; } .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .p-3 { padding: 12px; } .p-4 { padding: 16px; } .mt-2 { margin-top: 8px; } .mt-3 { margin-top: 12px; } .mb-3 { margin-bottom: 12px; } .text-sm { font-size: 12px; } .text-secondary { color: var(--color-text-secondary); } .font-bold { font-weight: 700; } .text-center { text-align: center; } .w-full { width: 100%; } .hidden { display: none; } ``` **Step 3: 验证文件存在** ```bash cat h5/css/variables.css | head -20 ``` Expected: 包含 `--color-primary: #1e3a5f` 等变量定义 --- ### Task 3: 创建全局样式文件 **Objective:** 复用样式组件:按钮、卡片、Tab栏、顶栏 **Files:** - Create: `h5/css/style.css` - Modify: `h5/css/variables.css` (已创建) **Step 1: 创建 style.css** ```css @import 'variables.css'; /* ===== 页面容器 ===== */ .page { min-height: 100vh; padding-bottom: 60px; /* Tab 栏高度 */ } /* ===== 顶栏 ===== */ .header { position: sticky; top: 0; z-index: 100; background: var(--color-primary); color: #fff; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; font-size: 16px; font-weight: 600; } .header-back { color: #fff; font-size: 20px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.15); border-radius: 6px; } .header-title { flex: 1; text-align: center; } .header-right { width: 32px; } /* ===== 卡片 ===== */ .card { background: var(--color-card); border-radius: 12px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); } .card-title { font-size: 14px; font-weight: 600; color: var(--color-text); margin-bottom: 12px; } /* ===== 统计卡片 ===== */ .stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px; } .stat-card { background: var(--color-card); border-radius: 12px; padding: 16px; text-align: center; } .stat-label { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 4px; } .stat-value { font-size: 24px; font-weight: 700; font-family: var(--font-mono); } .stat-sub { font-size: 11px; color: var(--color-text-secondary); margin-top: 2px; } /* ===== 按钮 ===== */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 600; transition: opacity 0.2s; } .btn:active { opacity: 0.85; } .btn-primary { background: var(--color-primary); color: #fff; } .btn-secondary { background: var(--color-bg); color: var(--color-text); border: 1px solid var(--color-border); } .btn-danger { background: var(--color-danger); color: #fff; } .btn-block { width: 100%; } /* ===== 底部 Tab 栏 ===== */ .tab-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 60px; background: var(--color-card); border-top: 1px solid var(--color-border); display: flex; z-index: 200; } .tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; color: var(--color-text-secondary); font-size: 10px; transition: color 0.2s; } .tab-item.active { color: var(--color-primary); } .tab-item svg, .tab-item span { display: block; } /* ===== 子 Tab ===== */ .sub-tabs { display: flex; background: var(--color-card); border-bottom: 1px solid var(--color-border); padding: 0 12px; } .sub-tab { padding: 10px 16px; font-size: 13px; color: var(--color-text-secondary); border-bottom: 2px solid transparent; } .sub-tab.active { color: var(--color-primary); border-bottom-color: var(--color-primary); } /* ===== 列表项 ===== */ .list-item { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: var(--color-card); border-bottom: 1px solid var(--color-border); } .list-item:active { background: var(--color-bg); } .list-item-icon { width: 40px; height: 40px; border-radius: 8px; background: var(--color-bg); display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; } .list-item-content { flex: 1; min-width: 0; } .list-item-title { font-weight: 600; font-size: 14px; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .list-item-desc { font-size: 12px; color: var(--color-text-secondary); } .list-item-arrow { color: var(--color-text-secondary); font-size: 18px; } /* ===== 设备卡片网格 ===== */ .device-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 12px; } .device-card { background: var(--color-card); border-radius: 12px; padding: 14px; cursor: pointer; } .device-card:active { opacity: 0.85; } .device-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } .device-card-icon { font-size: 24px; } .device-card-status { width: 8px; height: 8px; border-radius: 50%; } .device-card-status.online { background: var(--color-online); } .device-card-status.offline { background: var(--color-offline); } .device-card-name { font-weight: 600; font-size: 14px; margin-bottom: 4px; } .device-card-model { font-size: 12px; color: var(--color-text-secondary); } .device-card-location { font-size: 11px; color: var(--color-text-secondary); margin-top: 4px; } /* ===== 预警列表项 ===== */ .alert-item { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; background: var(--color-card); border-bottom: 1px solid var(--color-border); cursor: pointer; } .alert-item:active { background: var(--color-bg); } .alert-icon { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; } .alert-icon.danger { background: #fff3e6; color: var(--color-danger); } .alert-icon.warning { background: #fef9c3; color: var(--color-warning); } .alert-icon.handled { background: #f3f4f6; color: var(--color-text-secondary); } .alert-content { flex: 1; min-width: 0; } .alert-title { font-weight: 600; font-size: 14px; margin-bottom: 2px; } .alert-meta { font-size: 12px; color: var(--color-text-secondary); } /* ===== 表单 ===== */ .form-group { margin-bottom: 16px; } .form-label { display: block; font-size: 13px; font-weight: 600; color: var(--color-text); margin-bottom: 6px; } .form-label .required { color: var(--color-danger); } .form-input { width: 100%; padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 8px; font-size: 14px; background: var(--color-card); transition: border-color 0.2s; } .form-input:focus { outline: none; border-color: var(--color-secondary); } textarea.form-input { resize: vertical; min-height: 80px; } .form-select { width: 100%; padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 8px; font-size: 14px; background: var(--color-card); appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 36px; } /* ===== 图片上传 ===== */ .photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } .photo-item { aspect-ratio: 1; border-radius: 8px; overflow: hidden; background: var(--color-bg); display: flex; align-items: center; justify-content: center; } .photo-item img { width: 100%; height: 100%; object-fit: cover; } .photo-add { border: 1px dashed var(--color-border); color: var(--color-text-secondary); font-size: 28px; cursor: pointer; } /* ===== Toast 提示 ===== */ .toast { position: fixed; top: 70px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: #fff; padding: 10px 20px; border-radius: 24px; font-size: 13px; z-index: 9999; animation: toast-in 0.3s ease; } @keyframes toast-in { from { opacity: 0; transform: translateX(-50%) translateY(-10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } } /* ===== 页面内容区 ===== */ .page-content { padding: 12px; } /* ===== 空状态 ===== */ .empty-state { padding: 40px 20px; text-align: center; color: var(--color-text-secondary); } .empty-state-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.5; } .empty-state-text { font-size: 14px; } ``` **Step 2: 验证** ```bash wc -l h5/css/style.css ``` Expected: `> 200` 行 --- ## Mock 数据层 ### Task 4: 创建 Mock 数据文件 **Objective:** 提供全量 Mock 数据供所有页面使用 **Files:** - Create: `h5/js/mock.js` **Step 1: 创建 mock.js** ```javascript // ============================ // Mock 数据 — 郑州智慧工地 H5 // ============================ // 设备 Mock 数据 const MOCK_DEVICES = [ { id: 'tc001', name: '1号塔吊', type: 'tower_crane', model: 'QTZ500', location: 'A区施工现场', status: 'online', lastSeen: '2026-04-14 10:30:00', }, { id: 'tc002', name: '2号塔吊', type: 'tower_crane', model: 'QTZ630', location: 'B区施工现场', status: 'online', lastSeen: '2026-04-14 10:29:00', }, { id: 'tc003', name: '3号塔吊', type: 'tower_crane', model: 'QTZ500', location: 'C区施工现场', status: 'offline', lastSeen: '2026-04-13 18:00:00', }, { id: 'el001', name: '1号升降机', type: 'elevator', model: 'SC200/200', location: 'A区主楼', status: 'online', lastSeen: '2026-04-14 10:30:00', }, { id: 'el002', name: '2号升降机', type: 'elevator', model: 'SC200/200', location: 'B区主楼', status: 'online', lastSeen: '2026-04-14 10:30:00', }, ]; // 设备实时数据 Mock const MOCK_REALTIME = { tc001: { weight: 45.2, windSpeed: 5.2, windLevel: 3, range: 30.5, height: 85.0, angle: 120.5, moment: 320.5, momentPercent: 85.3, alert: 'danger', alertMsg: '载重超限', }, tc002: { weight: 32.1, windSpeed: 4.8, windLevel: 2, range: 25.0, height: 92.3, angle: 45.2, moment: 210.5, momentPercent: 55.8, alert: null, alertMsg: null, }, tc003: { weight: 0, windSpeed: 0, windLevel: 0, range: 0, height: 0, angle: 0, moment: 0, momentPercent: 0, alert: null, alertMsg: null, }, el001: { realtimeWeight: 1.2, realtimeSpeed: 1.0, realtimeHeight: 45.5, realtimeDipX: 0.5, realtimeDipY: 0.3, outDoorStatus: '0', alert: 'warning', alertMsg: '风速过大', }, el002: { realtimeWeight: 0.8, realtimeSpeed: 0.8, realtimeHeight: 28.0, realtimeDipX: 0.3, realtimeDipY: 0.2, outDoorStatus: '0', alert: null, alertMsg: null, }, }; // 预警 Mock 数据 const MOCK_ALERTS = [ { id: 'al001', deviceId: 'tc001', deviceName: '1号塔吊', deviceType: 'tower_crane', level: 'danger', message: '载重超限:当前45.2kN,超过额定值40kN', metric: 'weight', value: 45.2, createdAt: '2026-04-14 10:25:00', status: 'unread', }, { id: 'al002', deviceId: 'el001', deviceName: '1号升降机', deviceType: 'elevator', level: 'warning', message: '风速过大:当前8.5m/s,超过阈值6m/s', metric: 'windSpeed', value: 8.5, createdAt: '2026-04-14 10:20:00', status: 'unread', }, { id: 'al003', deviceId: 'tc002', deviceName: '2号塔吊', deviceType: 'tower_crane', level: 'warning', message: '力矩超限:当前210.5kN·m,超过额定值180kN·m', metric: 'moment', value: 210.5, createdAt: '2026-04-14 09:15:00', status: 'handled', handleNote: '已现场确认,正常吊装作业', }, { id: 'al004', deviceId: 'tc001', deviceName: '1号塔吊', deviceType: 'tower_crane', level: 'danger', message: '回转角度异常:回转速度过快', metric: 'angle', value: 180.0, createdAt: '2026-04-14 08:30:00', status: 'handled', handleNote: '已通知操作员减速', }, { id: 'al005', deviceId: 'el001', deviceName: '1号升降机', deviceType: 'elevator', level: 'warning', message: '高度接近上限:当前高度45.5m,接近限高50m', metric: 'height', value: 45.5, createdAt: '2026-04-13 16:45:00', status: 'ignored', }, { id: 'al006', deviceId: 'tc003', deviceName: '3号塔吊', deviceType: 'tower_crane', level: 'warning', message: '设备离线:超过2小时无数据上报', metric: 'offline', value: 0, createdAt: '2026-04-13 20:00:00', status: 'unread', }, ]; // 隐患随手拍 Mock const MOCK_REPORTS = [ { id: 'rep001', desc: 'A区基坑临边防护栏缺失,存在高处坠落风险', category: '高空坠落', severity: '较大', gps: '34.7567,113.6234', address: '郑州市中原区A区基坑现场', photos: [], createdAt: '2026-04-14 09:30:00', }, { id: 'rep002', desc: 'B区钢筋加工区配电箱门未关闭', category: '触电', severity: '一般', gps: '34.7570,113.6240', address: '郑州市中原区B区钢筋棚', photos: [], createdAt: '2026-04-13 15:20:00', }, { id: 'rep003', desc: 'C区脚手架扣件松动', category: '物体打击', severity: '重大', gps: '34.7565,113.6228', address: '郑州市中原区C区脚手架', photos: [], createdAt: '2026-04-12 11:10:00', }, ]; // 施工日志 Mock const MOCK_LOGS = [ { id: 'log001', date: '2026-04-14', part: 'A区主楼', content: '完成3层混凝土浇筑,钢筋绑扎至4层', workers: 28, equipment: ['tower_crane', 'elevator'], safety: '无安全问题', note: '混凝土浇筑顺利', attachments: [], createdAt: '2026-04-14 18:00:00', }, { id: 'log002', date: '2026-04-13', part: 'B区地下室', content: '地下室西侧土方开挖,外运50车', workers: 18, equipment: ['elevator'], safety: '注意坑边防护', note: '夜间出土暂停', attachments: [], createdAt: '2026-04-13 18:30:00', }, { id: 'log003', date: '2026-04-12', part: 'C区基坑', content: '基坑支护施工,锚索张拉', workers: 12, equipment: [], safety: '锚索张拉时无关人员远离', note: '', attachments: [], createdAt: '2026-04-12 17:45:00', }, ]; // 用户 Mock const MOCK_USER = { id: 1, username: 'admin', realName: '张工地', role: '管理员', phone: '138****1234', }; // 辅助函数 function getDeviceById(id) { return MOCK_DEVICES.find(d => d.id === id); } function getAlertById(id) { return MOCK_ALERTS.find(a => a.id === id); } function getRealtimeById(id) { return MOCK_REALTIME[id] || null; } ``` **Step 2: 验证** ```bash grep -c "MOCK_DEVICES\|MOCK_ALERTS\|MOCK_REPORTS\|MOCK_LOGS" h5/js/mock.js ``` Expected: `4` --- ## JS 基础设施 ### Task 5: 创建 API 封装文件 **Objective:** 提供统一的 API 调用封装,所有请求走 Mock **Files:** - Create: `h5/js/api.js` **Step 1: 创建 api.js** ```javascript // ============================ // API 基础封装 — Mock 模式 // ============================ const API_BASE = '/v1'; // 登录 function apiLogin(username, password) { return new Promise((resolve) => { setTimeout(() => { resolve({ code: 0, data: { token: 'mock-token-' + Date.now(), expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(), user: MOCK_USER, } }); }, 500); }); } // 获取设备列表 function apiGetDevices(params = {}) { return new Promise((resolve) => { setTimeout(() => { let items = [...MOCK_DEVICES]; if (params.type === 'tower_crane') { items = items.filter(d => d.type === 'tower_crane'); } else if (params.type === 'elevator') { items = items.filter(d => d.type === 'elevator'); } resolve({ code: 0, data: { total: items.length, items } }); }, 300); }); } // 获取设备实时数据 function apiGetDeviceRealtime(deviceId) { return new Promise((resolve) => { setTimeout(() => { const data = getRealtimeById(deviceId); resolve({ code: 0, data }); }, 200); }); } // 获取预警列表 function apiGetAlerts(params = {}) { return new Promise((resolve) => { setTimeout(() => { let items = [...MOCK_ALERTS]; if (params.level === 'danger') { items = items.filter(a => a.level === 'danger'); } else if (params.level === 'warning') { items = items.filter(a => a.level === 'warning'); } else if (params.status === 'handled') { items = items.filter(a => a.status === 'handled' || a.status === 'ignored'); } else if (params.status === 'unread') { items = items.filter(a => a.status === 'unread'); } const unreadCount = MOCK_ALERTS.filter(a => a.status === 'unread').length; resolve({ code: 0, data: { total: items.length, unreadCount, items } }); }, 300); }); } // 获取预警详情 function apiGetAlertDetail(alertId) { return new Promise((resolve) => { setTimeout(() => { const alert = getAlertById(alertId); resolve({ code: 0, data: alert }); }, 200); }); } // 处理预警 function apiHandleAlert(alertId, action, note) { return new Promise((resolve) => { setTimeout(() => { const alert = MOCK_ALERTS.find(a => a.id === alertId); if (alert) { alert.status = action === 'handled' ? 'handled' : 'ignored'; if (note) alert.handleNote = note; } resolve({ code: 0, message: '操作成功' }); }, 500); }); } // 提交隐患随手拍 function apiSubmitReport(formData) { return new Promise((resolve) => { setTimeout(() => { const newReport = { id: 'rep' + Date.now(), ...formData, createdAt: new Date().toLocaleString('zh-CN'), }; MOCK_REPORTS.unshift(newReport); resolve({ code: 0, data: newReport }); }, 800); }); } // 获取随手拍记录 function apiGetReports() { return new Promise((resolve) => { setTimeout(() => { resolve({ code: 0, data: { total: MOCK_REPORTS.length, items: MOCK_REPORTS } }); }, 300); }); } // 获取施工日志列表 function apiGetLogs() { return new Promise((resolve) => { setTimeout(() => { resolve({ code: 0, data: { total: MOCK_LOGS.length, items: MOCK_LOGS } }); }, 300); }); } // 提交施工日志 function apiSubmitLog(formData) { return new Promise((resolve) => { setTimeout(() => { const newLog = { id: 'log' + Date.now(), ...formData, createdAt: new Date().toLocaleString('zh-CN'), }; MOCK_LOGS.unshift(newLog); resolve({ code: 0, data: newLog }); }, 800); }); } // OSS 预签名 URL(Mock) function apiGetUploadToken(filename, contentType) { return new Promise((resolve) => { setTimeout(() => { resolve({ code: 0, data: { uploadUrl: 'https://jesxion-ai-studio.oss-cn-beijing.aliyuncs.com/mock/' + filename, objectKey: 'mock/' + filename, expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), } }); }, 300); }); } ``` **Step 2: 验证** ```bash grep -c "^function api" h5/js/api.js ``` Expected: `10` --- ### Task 6: 创建全局 App 逻辑文件 **Objective:** 全局逻辑:登录状态检查、Tab 栏激活、页面跳转、Toast 工具 **Files:** - Create: `h5/js/app.js` **Step 1: 创建 app.js** ```javascript // ============================ // 全局 App 逻辑 // ============================ // ============ 登录状态 ============ function getToken() { return localStorage.getItem('token'); } function setToken(token) { localStorage.setItem('token', token); } function clearToken() { localStorage.removeItem('token'); } function isLoggedIn() { return !!getToken(); } // 检查登录,未登录跳转登录页 function requireAuth() { if (!isLoggedIn()) { location.href = 'login.html'; return false; } return true; } // ============ Toast ============ function showToast(message, duration = 2000) { const existing = document.querySelector('.toast'); if (existing) existing.remove(); const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), duration); } // ============ Tab 栏激活 ============ function initTabBar() { const path = location.pathname; const page = path.split('/').pop() || 'index.html'; document.querySelectorAll('.tab-item').forEach(tab => { const href = tab.getAttribute('href'); if (href === './' + page || href === page) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); } // ============ URL 参数解析 ============ function getQueryParam(key) { const params = new URLSearchParams(location.search); return params.get(key); } // ============ 格式化 ============ function formatTime(dateStr) { if (!dateStr) return ''; const d = new Date(dateStr); if (isNaN(d)) return dateStr; const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const h = String(d.getHours()).padStart(2, '0'); const m = String(d.getMinutes()).padStart(2, '0'); return `${month}-${day} ${h}:${m}`; } function formatDate(dateStr) { if (!dateStr) return ''; const d = new Date(dateStr); if (isNaN(d)) return dateStr; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; } // ============ 设备类型图标 ============ function getDeviceIcon(type) { return type === 'tower_crane' ? '🏗️' : '🛗'; } // ============ 预警图标 ============ function getAlertIcon(level) { if (level === 'danger') return '🔴'; if (level === 'warning') return '🟡'; return '⚫'; } // ============ 设备在线状态 ============ function getStatusClass(status) { return status === 'online' ? 'online' : 'offline'; } function getStatusText(status) { return status === 'online' ? '在线' : '离线'; } ``` **Step 2: 验证** ```bash grep -c "^function " h5/js/app.js ``` Expected: `> 8` --- ## 页面实现 ### Task 7: 创建登录页 **Objective:** 账号密码登录,Mock 任意账号密码可登录 **Files:** - Create: `h5/login.html` **Step 1: 创建 login.html** ```html 登录 - 郑州智慧工地
``` **Step 2: 验证** ```bash grep -c "apiLogin\|isLoggedIn\|setToken" h5/login.html ``` Expected: `3` --- ### Task 8: 创建首页仪表盘 **Objective:** 设备统计卡片 + 预警统计卡片 + 快捷入口 **Files:** - Create: `h5/index.html` **Step 1: 创建 index.html** ```html 首页 - 郑州智慧工地
郑州智慧工地
👤
设备概况
塔吊
-
- 在线
升降机
-
- 在线
今日预警
危险
-
警告
-
最新预警
查看全部 ›
加载中...
🏠 首页 🏗️ 设备 📷 随手拍 📋 日志
``` **Step 2: 验证** ```bash grep -c "apiGetDevices\|apiGetAlerts\|requireAuth" h5/index.html ``` Expected: `3` --- ### Task 9: 创建设备列表页 **Objective:** 卡片网格展示,Tab 子导航(全部/塔吊/升降机) **Files:** - Create: `h5/devices.html` **Step 1: 创建 devices.html** ```html 设备列表 - 郑州智慧工地
设备列表
全部
塔吊
升降机
加载中...
🏠 首页 🏗️ 设备 📷 随手拍 📋 日志
``` **Step 2: 验证** ```bash grep -c "device-grid\|device-card\|apiGetDevices" h5/devices.html ``` Expected: `3` --- ### Task 10: 创建设备详情页 **Objective:** 实时数据面板,每 30 秒自动刷新 **Files:** - Create: `h5/device.html` **Step 1: 创建 device.html** ```html 设备详情 - 郑州智慧工地
设备详情
🏗️
-
-
-
-
实时数据
加载中...
- 自动刷新
``` **Step 2: 验证** ```bash grep -c "deviceId\|apiGetDeviceRealtime\|setInterval" h5/device.html ``` Expected: `3` --- ### Task 11: 创建预警列表页 **Objective:** 最新优先,Tab 筛选(全部/危险/警告/已处理) **Files:** - Create: `h5/alerts.html` **Step 1: 创建 alerts.html** ```html 预警中心 - 郑州智慧工地
预警中心
全部
危险
警告
已处理
加载中...
🏠 首页 🏗️ 设备 📷 随手拍 📋 日志
``` **Step 2: 验证** ```bash grep -c "alert-item\|apiGetAlerts\|getAlertIcon" h5/alerts.html ``` Expected: `3` --- ### Task 12: 创建预警详情页 **Objective:** 预警详情 + 确认处理/忽略操作 **Files:** - Create: `h5/alert.html` **Step 1: 创建 alert.html** ```html 预警详情 - 郑州智慧工地
预警详情
加载中...
``` **Step 2: 验证** ```bash grep -c "apiGetAlertDetail\|apiHandleAlert\|handleAlert" h5/alert.html ``` Expected: `3` --- ### Task 13: 创建隐患随手拍页面 **Objective:** 拍照 + 完整字段表单 + 直传 OSS **Files:** - Create: `h5/report.html` **Step 1: 创建 report.html** ```html 隐患随手拍 - 郑州智慧工地
隐患随手拍
👤
📍 正在获取位置...
现场照片 *
+
🏠 首页 🏗️ 设备 📷 随手拍 📋 日志
``` **Step 2: 验证** ```bash grep -c "navigator.geolocation\|apiSubmitReport\|photoInput" h5/report.html ``` Expected: `3` --- ### Task 14: 创建施工日志列表页 **Objective:** 日志列表 + 写日志入口 + 右上角个人中心入口 **Files:** - Create: `h5/logs.html` **Step 1: 创建 logs.html** ```html 施工日志 - 郑州智慧工地
施工日志
👤
加载中...
✏️
🏠 首页 🏗️ 设备 📷 随手拍 📋 日志
``` **Step 2: 验证** ```bash grep -c "apiGetLogs\|fab-btn\|log-item" h5/logs.html ``` Expected: `3` --- ### Task 15: 创建施工日志填写页 **Objective:** 模板化表单 + 拍照附件 + 提交 **Files:** - Create: `h5/log.html` **Step 1: 创建 log.html** ```html 写日志 - 郑州智慧工地
写日志
现场照片(选填)
+
``` **Step 2: 验证** ```bash grep -c "apiSubmitLog\|photo-grid\|form-input" h5/log.html ``` Expected: `3` --- ### Task 16: 创建个人中心页面 **Objective:** 个人信息 + 退出登录 **Files:** - Create: `h5/profile.html` **Step 1: 创建 profile.html** ```html 个人中心 - 郑州智慧工地
个人中心
👷
-
-
``` **Step 2: 验证** ```bash grep -c "clearToken\|logout\|MOCK_USER" h5/profile.html ``` Expected: `3` --- ## 最终验证 ### Task 17: 完整文件检查与预览 **Objective:** 验证所有文件存在,整理文件结构 **Step 1: 验证文件结构** ```bash find h5 -type f | sort ``` Expected: ``` h5/ ├── css/ │ ├── style.css │ └── variables.css ├── js/ │ ├── api.js │ ├── app.js │ └── mock.js ├── index.html ├── login.html ├── devices.html ├── device.html ├── alerts.html ├── alert.html ├── report.html ├── logs.html ├── log.html └── profile.html ``` **Step 2: 检查 CSS 变量是否包含主题色** ```bash grep "color-primary\|color-danger\|color-warning" h5/css/variables.css ``` Expected: 包含主色、危险色、警告色定义 **Step 3: 检查所有 HTML 文件都引用了 mock.js + app.js** ```bash for f in h5/*.html; do echo -n "$f: " grep -c "mock.js\|app.js" "$f" done ``` Expected: 每个 HTML 文件 `>= 2` --- ## 提交 ### Task 18: 提交所有 H5 原型文件 ```bash cd /tmp/smart-project && \ git add h5/ && \ git commit -m "feat: H5原型完整实现(10个页面,Mock数据,智慧工地风UI) 实现内容: - login.html: 登录页(Mock任意账号密码) - index.html: 首页仪表盘(设备/预警统计) - devices.html: 设备列表(卡片网格+Tab筛选) - device.html: 设备详情(实时数据面板+30s刷新) - alerts.html: 预警列表(Tab筛选) - alert.html: 预警详情(处理/忽略操作) - report.html: 隐患随手拍(拍照+GPS+完整字段) - logs.html: 施工日志列表 - log.html: 施工日志填写(模板化+拍照附件) - profile.html: 个人中心(退出登录) - css/style.css: 全局样式(智慧工地风配色) - css/variables.css: 主题色变量 - js/mock.js: Mock数据(5设备/6预警/3随手拍/3日志) - js/api.js: API封装(全部Mock) - js/app.js: 全局逻辑(登录/Toast/URL工具)" && \ git push 2>&1 ``` Expected: `git commit` 成功,`git push` 成功 --- ## 运行方式 **直接在浏览器中打开任意 HTML 文件即可运行,无需服务器:** ```bash # macOS open h5/index.html # 或用 Python 简单 HTTP 服务器 cd h5 && python3 -m http.server 8080 # 访问 http://localhost:8080 ```