feat: 完整环境监测模块——地图/列表视图、设备详情、报警数据
新增员工端环境监测完整功能模块: - 主页面(environment/index.html):4个监测类型Tab切换、地图/列表双视图、设备搜索筛选 - 设备详情页(device-detail.html):指标展示、趋势分析、SVG仪表盘、柱状图 - 报警数据页(alarm-data.html):日期范围筛选、类型筛选、阈值范围展示 数据结构与后台对齐: - 室外空气:温度、湿度、太阳辐射、PM2.5、PM10、照度、风速 - 室内空气:温度、湿度、CO₂、甲醛、PM2.5、PM10 - 水质监测:PH、电导率、盐度、TDS、电阻率、温度 - 花粉监测:柏科、藜科、苋科、花粉总数 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
_Last synced: 2026-05-22 | Base commit: `146c1da`_
|
||||
_Last synced: 2026-05-25 | Base commit: `0e2dcff`_
|
||||
|
||||
# 记忆索引
|
||||
|
||||
| 文件 | 描述 | 类型 | 最后更新 |
|
||||
|------|------|------|----------|
|
||||
| project_overview.md | 项目技术栈、架构与模块结构 | project | 2026-05-14 |
|
||||
| project_progress.md | 各模块原型开发进度 | project | 2026-05-22 |
|
||||
| project_progress.md | 各模块原型开发进度 | project | 2026-05-25 |
|
||||
| decisions.md | 关键设计决策与架构约定 | project | 2026-04-21 |
|
||||
| feedback.md | 协作规范与用户纠正记录 | feedback | 2026-04-21 |
|
||||
| feedback.md | 协作规范与用户纠正记录 | feedback | 2026-05-15 |
|
||||
| user_profile.md | 用户角色与偏好 | user | 2026-04-21 |
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
name: 项目进度
|
||||
description: 各模块原型开发进度与近期里程碑
|
||||
type: project
|
||||
last_updated: 2026-05-25
|
||||
commit: 0e2dcff
|
||||
---
|
||||
|
||||
# 项目进度
|
||||
|
||||
## 已完成原型的模块
|
||||
@@ -49,6 +57,9 @@ Web 管理端和员工端 APP 的其余模块(系统管理、运动管理、
|
||||
- 2026-05-22:味养创新交互完善——人工创新查看详情弹窗/审核双模/AI推演弹窗重构及侧边栏修复,功能说明9.5章节同步更新
|
||||
- 2026-05-22:绿色种采模块全面重构——采供管理整页重构(采购单维度主表+子表、5种状态联动操作、配送/取消弹窗交互、详情抽屉动态信息),采收入库/供应商管理/种植监控/养殖监控多页面优化,功能说明文档9.4章节同步更新
|
||||
- 2026-05-22:营养用餐模块多页面交互优化——数据分析页新增食堂切换栏/周选择器/餐品明细弹窗重构,食材效益页新增级联分类选择器,大模型管理新增API_KEY/API_URL列及接入/编辑弹窗,小模型管理新增新增模型弹窗,终端管理筛选栏/表格列精简,餐线管理操作列移除编辑按钮
|
||||
- 2026-05-22:全模块交互增强——卫生供应8页面统一新增Drawer详情、确认收货/备货/配送弹窗及操作类型列;健康生产8页面统一Drawer详情+toast状态联动+操作类型列;绿色种采3页面详情从Modal重构为Drawer(BEM命名)
|
||||
- 2026-05-21:农场监控模块重构——种植监控改为批次记录主表(新增灌溉/健康/负责人列+巡检记录弹窗),养殖监控新增养殖方式/状态列+疫苗接种记录弹窗,新增食堂档案页 arc-canteen.html,档案模块(arc-algo/employee/unit)多字段优化
|
||||
- 2026-05-21:健康生产模块交互完善——余量处置重构(筛选栏级联+更多下拉+员工福利领取记录弹窗),上餐监控操作列合并为更多hover下拉+上餐弹窗自动回填,生产统计日期范围选择器默认当月
|
||||
- 2026-05-20:营养用餐模块交互完善——用户管理等级配置tab增加新增/查看/编辑弹窗,消费管理筛选优化(日期默认今天、金额范围双输入框、消费类型标签化、单位部门级联筛选、流水号筛选、交易明细弹窗全字段对齐),取餐管理新增总金额列和餐品金额列
|
||||
- 2026-05-19:卫生供应模块8页面交互优化(侧边栏菜单顺序统一、多页面新增弹窗表单、表格列重构、溯源码前移、菜品名称格式统一),补充营养管理功能说明文档及调整记录
|
||||
- 2026-05-14:员工端健康监测模块大量新增(穿戴设备16页、体重3页、健康筛查5页),Web管理端健康监测模块新增(穿戴8页、体重20页、环境25页、健康排查7页、档案/异常5页),健康体检检前专项配置及异常事件优化
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- Last updated: 2026-05-22 | Commit: e50dbad -->
|
||||
<!-- Last updated: 2026-05-25 | Commit: 0e2dcff -->
|
||||
|
||||
# 项目约束
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>报警数据 - 环境监测</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #f0f2f5; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; }
|
||||
.phone-shell { width: 390px; height: 844px; background: #f5f7fa; border-radius: 44px; box-shadow: 0 20px 60px rgba(0,0,0,0.25), inset 0 0 0 2px #e0e0e0; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.status-bar { height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; background: #fff; }
|
||||
.status-bar .time { font-size: 15px; font-weight: 700; color: #333; }
|
||||
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
|
||||
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; margin-right: 28px; }
|
||||
|
||||
/* 筛选区 */
|
||||
.filter-bar { background: #fff; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; }
|
||||
.filter-row { display: flex; align-items: center; gap: 8px; }
|
||||
.filter-label { font-size: 12px; color: #999; flex-shrink: 0; }
|
||||
.filter-date-input { height: 32px; border: 1px solid #e8e8e8; border-radius: 8px; padding: 0 8px; font-size: 12px; color: #333; outline: none; background: #fff; flex: 1; min-width: 0; }
|
||||
.filter-date-input:focus { border-color: #1aad8d; }
|
||||
.filter-sep { font-size: 12px; color: #bbb; flex-shrink: 0; }
|
||||
.filter-select { height: 32px; border: 1px solid #e8e8e8; border-radius: 8px; padding: 0 24px 0 10px; font-size: 12px; color: #333; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 8px center; appearance: none; outline: none; cursor: pointer; flex: 1; }
|
||||
.filter-btn { height: 32px; padding: 0 16px; border-radius: 8px; border: none; font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0; }
|
||||
.filter-btn--primary { background: #1aad8d; color: #fff; }
|
||||
.filter-btn--reset { background: #f5f5f5; color: #666; }
|
||||
|
||||
/* 统计条 */
|
||||
.stat-bar { padding: 10px 16px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.stat-bar__count { font-size: 12px; color: #999; }
|
||||
.stat-bar__count strong { color: #ff4d4f; font-size: 14px; }
|
||||
|
||||
/* 列表 */
|
||||
.list-wrap { flex: 1; overflow-y: auto; padding: 0 14px 16px; }
|
||||
.list-wrap::-webkit-scrollbar { display: none; }
|
||||
.alarm-card { background: #fff; border-radius: 14px; padding: 13px 14px; margin-bottom: 10px; box-shadow: 0 1px 6px rgba(0,0,0,0.06); border-left: 3px solid #ff4d4f; }
|
||||
.alarm-card__top { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 8px; }
|
||||
.alarm-card__type { font-size: 14px; font-weight: 700; color: #1a1a1a; }
|
||||
.alarm-card__time { font-size: 11px; color: #bbb; }
|
||||
.alarm-card__metrics { display: flex; gap: 10px; align-items: center; margin-bottom: 6px; }
|
||||
.alarm-val { display: flex; flex-direction: column; align-items: center; background: #fff1f0; border-radius: 8px; padding: 6px 12px; }
|
||||
.alarm-val__num { font-size: 18px; font-weight: 800; color: #ff4d4f; line-height: 1; }
|
||||
.alarm-val__unit { font-size: 10px; color: #ff7875; margin-top: 2px; }
|
||||
.alarm-val__label { font-size: 10px; color: #999; margin-top: 2px; }
|
||||
.alarm-threshold { display: flex; flex-direction: column; align-items: center; background: #f5f7fa; border-radius: 8px; padding: 6px 10px; }
|
||||
.alarm-threshold__range { font-size: 14px; font-weight: 700; color: #666; line-height: 1; white-space: nowrap; }
|
||||
.alarm-threshold__unit { font-size: 10px; color: #aaa; margin-top: 2px; }
|
||||
.alarm-threshold__label { font-size: 10px; color: #999; margin-top: 2px; }
|
||||
.alarm-arrow { color: #ff4d4f; font-size: 18px; font-weight: 700; }
|
||||
.alarm-card__device { font-size: 11px; color: #999; display: flex; align-items: center; gap: 4px; }
|
||||
.empty-tip { text-align: center; color: #bbb; font-size: 13px; padding: 60px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="phone-shell">
|
||||
<div class="status-bar">
|
||||
<span class="time">9:41</span>
|
||||
<span class="icons">
|
||||
<svg width="16" height="16" fill="none"><rect x="1" y="4" width="3" height="8" rx="1" fill="#333"/><rect x="5" y="3" width="3" height="9" rx="1" fill="#333"/><rect x="9" y="1" width="3" height="11" rx="1" fill="#333"/><rect x="13" y="0" width="3" height="12" rx="1" fill="#333" opacity=".3"/></svg>
|
||||
<svg width="16" height="16" fill="none"><path d="M8 3C5.5 3 3.2 4 1.5 5.5l1.4 1.4C4.3 5.5 6.1 5 8 5s3.7.5 5.1 1.9l1.4-1.4C12.8 4 10.5 3 8 3zm0 4c-1.4 0-2.6.5-3.5 1.3L6 9.8c.6-.5 1.3-.8 2-.8s1.4.3 2 .8l1.5-1.5C10.6 7.5 9.4 7 8 7zm0 4c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1z" fill="#333"/></svg>
|
||||
<svg width="22" height="12" fill="none"><rect x=".5" y=".5" width="19" height="11" rx="2" stroke="#333"/><rect x="20" y="3.5" width="1.5" height="5" rx=".75" fill="#333"/><rect x="2" y="2" width="14" height="8" rx="1" fill="#333"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="nav-bar">
|
||||
<div class="nav-back" onclick="history.back()">
|
||||
<svg width="12" height="20" fill="none"><path d="M10 2L2 10l8 8" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<div class="nav-title">报警数据</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">日期</span>
|
||||
<input class="filter-date-input" type="date" id="dateFrom" value="2026-05-01">
|
||||
<span class="filter-sep">至</span>
|
||||
<input class="filter-date-input" type="date" id="dateTo" value="2026-05-25">
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<span class="filter-label">类型</span>
|
||||
<select class="filter-select" id="typeFilter">
|
||||
<option value="">全部类型</option>
|
||||
</select>
|
||||
<button class="filter-btn filter-btn--reset" onclick="resetFilter()">重置</button>
|
||||
<button class="filter-btn filter-btn--primary" onclick="applyFilter()">查询</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计 -->
|
||||
<div class="stat-bar">
|
||||
<span class="stat-bar__count">共 <strong id="totalCount">0</strong> 条报警</span>
|
||||
</div>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div class="list-wrap" id="listWrap"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 各 Tab 报警类型选项
|
||||
const TYPE_OPTIONS = {
|
||||
outdoor: ['PM2.5','PM10','温度','湿度','风速','太阳辐射','照度'],
|
||||
indoor: ['PM2.5','PM10','CO₂','甲醛','温度','湿度'],
|
||||
water: ['PH','电导率','盐度','TDS','电阻率','温度'],
|
||||
pollen: ['花粉总数']
|
||||
};
|
||||
|
||||
// 报警数据
|
||||
const ALARM_DATA = {
|
||||
outdoor: [
|
||||
{ type:'PM2.5', val:'156', threshold:{min:'0', max:'75'}, unit:'μg/m³', time:'2026-05-15 10:05', device:'空气质量监测站-A01', loc:'厂区大门东侧' },
|
||||
{ type:'PM10', val:'218', threshold:{min:'0', max:'150'}, unit:'μg/m³', time:'2026-05-15 10:05', device:'空气质量监测站-A01', loc:'厂区大门东侧' },
|
||||
{ type:'风速', val:'13.5', threshold:{min:'0', max:'10'}, unit:'m/s', time:'2026-05-14 15:42', device:'气象参数监测仪-D01', loc:'气象观测场' },
|
||||
{ type:'太阳辐射', val:'1180', threshold:{min:'0', max:'1000'}, unit:'W/m²', time:'2026-05-14 09:18', device:'空气质量监测站-A03', loc:'储油罐区南侧' },
|
||||
{ type:'温度', val:'42.3', threshold:{min:'-10',max:'40'}, unit:'℃', time:'2026-05-13 17:30', device:'PM10检测仪-B02', loc:'生产区北围栏' },
|
||||
{ type:'PM2.5', val:'195', threshold:{min:'0', max:'75'}, unit:'μg/m³', time:'2026-05-12 11:20', device:'VOC检测仪-E01', loc:'化工装置区' },
|
||||
{ type:'PM10', val:'285', threshold:{min:'0', max:'150'}, unit:'μg/m³', time:'2026-05-12 11:20', device:'VOC检测仪-E01', loc:'化工装置区' },
|
||||
{ type:'照度', val:'98500',threshold:{min:'0', max:'95000'},unit:'lm/m²', time:'2026-05-11 13:15', device:'臭氧检测仪-C01', loc:'办公楼楼顶' },
|
||||
],
|
||||
indoor: [
|
||||
{ type:'CO₂', val:'1250', threshold:{min:'400', max:'1000'}, unit:'ppm', time:'2026-05-15 14:22', device:'综合空气监测仪-A05', loc:'生产车间1号' },
|
||||
{ type:'甲醛', val:'0.15', threshold:{min:'0', max:'0.10'}, unit:'mg/m³', time:'2026-05-15 09:50', device:'甲醛检测仪-C02', loc:'新装修办公室3层' },
|
||||
{ type:'PM2.5',val:'135', threshold:{min:'0', max:'75'}, unit:'μg/m³', time:'2026-05-14 16:30', device:'综合空气监测仪-A05', loc:'生产车间1号' },
|
||||
{ type:'CO₂', val:'980', threshold:{min:'400', max:'800'}, unit:'ppm', time:'2026-05-14 12:05', device:'CO₂检测仪-D01', loc:'地下车库B1层' },
|
||||
{ type:'湿度', val:'85', threshold:{min:'30', max:'80'}, unit:'%', time:'2026-05-13 08:40', device:'PM2.5检测仪-B03', loc:'办公楼2层会议室' },
|
||||
{ type:'甲醛', val:'0.12', threshold:{min:'0', max:'0.10'}, unit:'mg/m³', time:'2026-05-12 15:18', device:'甲醛检测仪-C02', loc:'新装修办公室3层' },
|
||||
],
|
||||
water: [
|
||||
{ type:'电导率', val:'1250', threshold:{min:'50', max:'1000'}, unit:'μS/cm', time:'2026-05-15 11:30', device:'电导率监测仪-C01', loc:'注水站入口' },
|
||||
{ type:'PH', val:'8.5', threshold:{min:'6.5', max:'8.0'}, unit:'', time:'2026-05-15 11:30', device:'电导率监测仪-C01', loc:'注水站入口' },
|
||||
{ type:'TDS', val:'840', threshold:{min:'0', max:'500'}, unit:'mg/L', time:'2026-05-15 09:00', device:'多参数水质分析仪-A03', loc:'排污口监测点' },
|
||||
{ type:'盐度', val:'920', threshold:{min:'0', max:'500'}, unit:'mg/L', time:'2026-05-14 16:45', device:'多参数水质分析仪-A03', loc:'排污口监测点' },
|
||||
{ type:'电导率', val:'1680', threshold:{min:'50', max:'1000'}, unit:'μS/cm', time:'2026-05-14 08:22', device:'多参数水质分析仪-A03', loc:'排污口监测点' },
|
||||
{ type:'PH', val:'9.2', threshold:{min:'6.5', max:'8.0'}, unit:'', time:'2026-05-13 14:10', device:'多参数水质分析仪-A03', loc:'排污口监测点' },
|
||||
],
|
||||
pollen: [
|
||||
{ type:'花粉总数', val:'1098', threshold:{min:'0', max:'800'}, unit:'粒/千mm²', time:'2026-05-15 08:20', device:'花粉采样器-A03', loc:'杨树林防护带' },
|
||||
{ type:'花粉总数', val:'924', threshold:{min:'0', max:'800'}, unit:'粒/千mm²', time:'2026-05-14 17:10', device:'综合气象花粉站-D01', loc:'气象观测场' },
|
||||
{ type:'花粉总数', val:'860', threshold:{min:'0', max:'800'}, unit:'粒/千mm²', time:'2026-05-14 10:35', device:'花粉浓度监测仪-B01', loc:'厂区东侧林带' },
|
||||
]
|
||||
};
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const tab = params.get('tab') || 'outdoor';
|
||||
const devId = params.get('id') || '';
|
||||
|
||||
// 初始化类型下拉
|
||||
const typeOpts = TYPE_OPTIONS[tab] || [];
|
||||
const typeSelect = document.getElementById('typeFilter');
|
||||
typeOpts.forEach(t => {
|
||||
const o = document.createElement('option');
|
||||
o.value = t; o.textContent = t;
|
||||
typeSelect.appendChild(o);
|
||||
});
|
||||
|
||||
function applyFilter() {
|
||||
const from = document.getElementById('dateFrom').value;
|
||||
const to = document.getElementById('dateTo').value;
|
||||
const type = typeSelect.value;
|
||||
let list = ALARM_DATA[tab] || [];
|
||||
if (from) list = list.filter(r => r.time.slice(0,10) >= from);
|
||||
if (to) list = list.filter(r => r.time.slice(0,10) <= to);
|
||||
if (type) list = list.filter(r => r.type === type);
|
||||
renderList(list);
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
document.getElementById('dateFrom').value = '2026-05-01';
|
||||
document.getElementById('dateTo').value = '2026-05-25';
|
||||
typeSelect.value = '';
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
function renderList(list) {
|
||||
document.getElementById('totalCount').textContent = list.length;
|
||||
const wrap = document.getElementById('listWrap');
|
||||
if (list.length === 0) {
|
||||
wrap.innerHTML = '<div class="empty-tip">暂无报警数据</div>';
|
||||
return;
|
||||
}
|
||||
wrap.innerHTML = list.map(r => `
|
||||
<div class="alarm-card">
|
||||
<div class="alarm-card__top">
|
||||
<div class="alarm-card__type">${r.type} 超标报警</div>
|
||||
<div class="alarm-card__time">${r.time}</div>
|
||||
</div>
|
||||
<div class="alarm-card__metrics">
|
||||
<div class="alarm-val">
|
||||
<div class="alarm-val__num">${r.val}</div>
|
||||
<div class="alarm-val__unit">${r.unit}</div>
|
||||
<div class="alarm-val__label">报警值</div>
|
||||
</div>
|
||||
<div class="alarm-arrow">›</div>
|
||||
<div class="alarm-threshold">
|
||||
<div class="alarm-threshold__range">${r.threshold.min} ~ ${r.threshold.max}</div>
|
||||
<div class="alarm-threshold__unit">${r.unit}</div>
|
||||
<div class="alarm-threshold__label">阈值范围</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 初始渲染
|
||||
applyFilter();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,320 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>设备详情 - 环境监测</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #f0f2f5; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; }
|
||||
.phone-shell { width: 390px; height: 844px; background: #f5f7fa; border-radius: 44px; box-shadow: 0 20px 60px rgba(0,0,0,0.25), inset 0 0 0 2px #e0e0e0; overflow: hidden; position: relative; display: flex; flex-direction: column; }
|
||||
.status-bar { height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; background: #fff; }
|
||||
.status-bar .time { font-size: 15px; font-weight: 700; color: #333; }
|
||||
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
|
||||
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; margin-right: 28px; }
|
||||
.scroll-content { flex: 1; overflow-y: auto; padding-bottom: 80px; }
|
||||
.scroll-content::-webkit-scrollbar { display: none; }
|
||||
.level-header { padding: 16px 18px 12px; display: flex; align-items: flex-start; justify-content: space-between; }
|
||||
.level-name { font-size: 26px; font-weight: 800; color: #1a1a1a; }
|
||||
.level-sub { font-size: 12px; color: #999; margin-top: 3px; }
|
||||
.level-header__right { text-align: right; }
|
||||
.index-val, .grade-val { font-size: 13px; color: #555; font-weight: 500; line-height: 1.8; }
|
||||
.main-card { margin: 0 14px 12px; background: #fff; border-radius: 18px; padding: 14px 16px 18px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); }
|
||||
.card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
|
||||
.card-loc { display: flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 600; color: #1a1a1a; }
|
||||
.card-time { font-size: 11px; color: #999; }
|
||||
.card-desc { font-size: 12px; color: #666; line-height: 1.6; margin-bottom: 14px; }
|
||||
.gauge-wrap { display: flex; justify-content: center; margin-bottom: 16px; }
|
||||
.metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px 8px; }
|
||||
.metric-item { text-align: center; }
|
||||
.metric-item__val { font-size: 18px; font-weight: 700; color: #1a1a1a; }
|
||||
.metric-item__unit { font-size: 10px; color: #999; font-weight: 400; }
|
||||
.metric-item__label{ font-size: 11px; color: #999; margin-top: 2px; }
|
||||
.trend-section { margin: 0 14px 16px; background: #fff; border-radius: 18px; padding: 14px 16px 18px; box-shadow: 0 2px 12px rgba(0,0,0,0.07); }
|
||||
.trend-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.trend-title { font-size: 15px; font-weight: 700; color: #1a1a1a; }
|
||||
.trend-metric-select {
|
||||
height: 28px; border: 1px solid #1aad8d; border-radius: 8px;
|
||||
padding: 0 22px 0 8px; font-size: 12px; color: #1aad8d; font-weight: 600;
|
||||
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%231aad8d'/%3E%3C/svg%3E") no-repeat right 7px center;
|
||||
appearance: none; outline: none; cursor: pointer;
|
||||
}
|
||||
.time-tabs { display: flex; gap: 0; background: #f0f2f5; border-radius: 20px; padding: 3px; margin-bottom: 16px; width: fit-content; }
|
||||
.time-tab { padding: 4px 14px; border-radius: 16px; font-size: 12px; color: #999; cursor: pointer; transition: all 0.2s; font-weight: 500; }
|
||||
.time-tab.active { background: #1aad8d; color: #fff; font-weight: 600; }
|
||||
.bar-chart-outer { position: relative; }
|
||||
.bar-chart { display: flex; align-items: flex-end; justify-content: space-around; height: 110px; border-bottom: 1px solid #f0f0f0; padding: 0 4px; }
|
||||
.bar-col { display: flex; flex-direction: column; align-items: center; gap: 3px; }
|
||||
.bar-val { font-size: 10px; color: #555; font-weight: 600; }
|
||||
.bar-body { width: 16px; border-radius: 4px 4px 0 0; background: linear-gradient(180deg, #36cfc9 0%, #1aad8d 100%); }
|
||||
.bar-x-labels { display: flex; justify-content: space-around; padding: 5px 4px 0; }
|
||||
.bar-x-labels span { font-size: 9px; color: #aaa; flex: 1; text-align: center; }
|
||||
.bottom-btn-wrap { position: absolute; bottom: 0; left: 0; right: 0; padding: 10px 20px 28px; background: linear-gradient(transparent, #f5f7fa 35%); }
|
||||
.alarm-btn { width: 100%; height: 48px; background: linear-gradient(90deg, #1aad8d, #36cfc9); border: none; border-radius: 24px; color: #fff; font-size: 16px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 16px rgba(26,173,141,0.35); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="phone-shell">
|
||||
<div class="status-bar">
|
||||
<span class="time">9:41</span>
|
||||
<span class="icons">
|
||||
<svg width="16" height="16" fill="none"><rect x="1" y="4" width="3" height="8" rx="1" fill="#333"/><rect x="5" y="3" width="3" height="9" rx="1" fill="#333"/><rect x="9" y="1" width="3" height="11" rx="1" fill="#333"/><rect x="13" y="0" width="3" height="12" rx="1" fill="#333" opacity=".3"/></svg>
|
||||
<svg width="16" height="16" fill="none"><path d="M8 3C5.5 3 3.2 4 1.5 5.5l1.4 1.4C4.3 5.5 6.1 5 8 5s3.7.5 5.1 1.9l1.4-1.4C12.8 4 10.5 3 8 3zm0 4c-1.4 0-2.6.5-3.5 1.3L6 9.8c.6-.5 1.3-.8 2-.8s1.4.3 2 .8l1.5-1.5C10.6 7.5 9.4 7 8 7zm0 4c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1z" fill="#333"/></svg>
|
||||
<svg width="22" height="12" fill="none"><rect x=".5" y=".5" width="19" height="11" rx="2" stroke="#333"/><rect x="20" y="3.5" width="1.5" height="5" rx=".75" fill="#333"/><rect x="2" y="2" width="14" height="8" rx="1" fill="#333"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="nav-bar">
|
||||
<div class="nav-back" onclick="history.back()">
|
||||
<svg width="12" height="20" fill="none"><path d="M10 2L2 10l8 8" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<div class="nav-title">设备详情</div>
|
||||
</div>
|
||||
<div class="scroll-content">
|
||||
<div class="level-header">
|
||||
<div>
|
||||
<div class="level-name" id="levelName">—</div>
|
||||
<div class="level-sub" id="levelSub">空气质量评价</div>
|
||||
</div>
|
||||
<div class="level-header__right">
|
||||
<div class="index-val" id="indexVal">综合指数:—</div>
|
||||
<div class="grade-val" id="gradeVal">空气质量级别:—</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-card">
|
||||
<div class="card-top">
|
||||
<div class="card-loc">
|
||||
<svg width="13" height="16" fill="none"><path d="M6.5 0C4 0 2 2 2 4.5c0 3.5 4.5 9 4.5 9s4.5-5.5 4.5-9C11 2 9 0 6.5 0zm0 6a1.5 1.5 0 110-3 1.5 1.5 0 010 3z" fill="#1aad8d"/></svg>
|
||||
<span id="cardLoc">—</span>
|
||||
</div>
|
||||
<div class="card-time" id="cardTime">—</div>
|
||||
</div>
|
||||
<div class="card-desc" id="cardDesc">—</div>
|
||||
<div class="gauge-wrap" id="gaugeWrap"></div>
|
||||
<div class="metrics-grid" id="metricsGrid"></div>
|
||||
</div>
|
||||
<div class="trend-section">
|
||||
<div class="trend-header">
|
||||
<div class="trend-title">变化趋势</div>
|
||||
<select class="trend-metric-select" id="trendMetricSelect" onchange="onTrendMetricChange(this)"></select>
|
||||
</div>
|
||||
<div class="time-tabs">
|
||||
<div class="time-tab active" onclick="switchTimePeriod(this,'24h')">近24小时</div>
|
||||
<div class="time-tab" onclick="switchTimePeriod(this,'7d')">近7日</div>
|
||||
<div class="time-tab" onclick="switchTimePeriod(this,'30d')">近30日</div>
|
||||
</div>
|
||||
<div id="barChartWrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-btn-wrap">
|
||||
<button class="alarm-btn" onclick="location.href='alarm-data.html?tab='+tab+'&id='+id">报警数据</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const ALL_DATA = {
|
||||
outdoor: {
|
||||
sub: '室外空气质量评价', gradeKey: 'aqi',
|
||||
levels: ['lvl-excellent','lvl-good','lvl-light','lvl-moderate','lvl-heavy','lvl-severe'],
|
||||
levelLabels: ['优','良','轻度污染','中度污染','重度污染','严重污染'],
|
||||
grades: ['I级','II级','III级','IV级','V级','VI级'],
|
||||
indexes: ['1.00','2.00','3.00','4.00','5.00','6.00'],
|
||||
descs: [
|
||||
'空气质量令人满意,基本无污染,对人体健康没有危害。',
|
||||
'空气质量可接受,少数对污染物特别敏感的人群健康有较弱影响。',
|
||||
'易感人群症状有轻度加剧,健康人群出现刺激症状,建议减少户外活动。',
|
||||
'敏感人群症状明显加重,健康人群普遍出现刺激症状,建议避免户外活动。',
|
||||
'心脏病和肺病患者症状显著加剧,运动耐受力降低,健康人群普遍出现症状。',
|
||||
'健康人群运动耐受力降低,有明显强烈症状,提前出现某些疾病,建议停止户外活动。'
|
||||
],
|
||||
colors: ['#52c41a','#73d13d','#faad14','#fa541c','#ff4d4f','#722ed1'],
|
||||
devices: [
|
||||
{ id:'OA001', name:'空气质量监测站-A01', code:'OA-2024-001', unit:'采油一厂', loc:'厂区大门东侧', time:'14:30', level:0,
|
||||
metrics:[{k:'温度',v:'32.5',u:'℃'},{k:'湿度',v:'45',u:'%'},{k:'太阳辐射',v:'680',u:'W/m²'},{k:'PM2.5',v:'28',u:'μg/m³'},{k:'PM10',v:'52',u:'μg/m³'},{k:'照度',v:'65000',u:'lm/m²'},{k:'风速',v:'3.2',u:'m/s'}] },
|
||||
{ id:'OA002', name:'PM10检测仪-B02', code:'OA-2024-002', unit:'采油一厂', loc:'生产区北围栏', time:'14:28', level:1,
|
||||
metrics:[{k:'温度',v:'33.1',u:'℃'},{k:'湿度',v:'42',u:'%'},{k:'太阳辐射',v:'720',u:'W/m²'},{k:'PM2.5',v:'38',u:'μg/m³'},{k:'PM10',v:'68',u:'μg/m³'},{k:'照度',v:'72000',u:'lm/m²'},{k:'风速',v:'4.1',u:'m/s'}] },
|
||||
{ id:'OA003', name:'臭氧检测仪-C01', code:'OA-2024-003', unit:'采油二厂', loc:'办公楼楼顶', time:'14:25', level:2,
|
||||
metrics:[{k:'温度',v:'31.8',u:'℃'},{k:'湿度',v:'50',u:'%'},{k:'太阳辐射',v:'580',u:'W/m²'},{k:'PM2.5',v:'55',u:'μg/m³'},{k:'PM10',v:'92',u:'μg/m³'},{k:'照度',v:'58000',u:'lm/m²'},{k:'风速',v:'2.8',u:'m/s'}] },
|
||||
{ id:'OA004', name:'气象参数监测仪-D01', code:'OA-2024-004', unit:'采油三厂', loc:'气象观测场', time:'14:20', level:3,
|
||||
metrics:[{k:'温度',v:'34.2',u:'℃'},{k:'湿度',v:'38',u:'%'},{k:'太阳辐射',v:'850',u:'W/m²'},{k:'PM2.5',v:'82',u:'μg/m³'},{k:'PM10',v:'135',u:'μg/m³'},{k:'照度',v:'85000',u:'lm/m²'},{k:'风速',v:'5.6',u:'m/s'}] },
|
||||
{ id:'OA005', name:'空气质量监测站-A03', code:'OA-2024-005', unit:'采油三厂', loc:'储油罐区南侧', time:'14:15', level:4,
|
||||
metrics:[{k:'温度',v:'35.0',u:'℃'},{k:'湿度',v:'35',u:'%'},{k:'太阳辐射',v:'920',u:'W/m²'},{k:'PM2.5',v:'125',u:'μg/m³'},{k:'PM10',v:'190',u:'μg/m³'},{k:'照度',v:'92000',u:'lm/m²'},{k:'风速',v:'6.3',u:'m/s'}] },
|
||||
{ id:'OA006', name:'VOC检测仪-E01', code:'OA-2024-006', unit:'采油三厂', loc:'化工装置区', time:'14:10', level:5,
|
||||
metrics:[{k:'温度',v:'36.2',u:'℃'},{k:'湿度',v:'30',u:'%'},{k:'太阳辐射',v:'950',u:'W/m²'},{k:'PM2.5',v:'180',u:'μg/m³'},{k:'PM10',v:'260',u:'μg/m³'},{k:'照度',v:'95000',u:'lm/m²'},{k:'风速',v:'7.1',u:'m/s'}] }
|
||||
]
|
||||
},
|
||||
indoor: {
|
||||
sub: '室内空气质量评价', gradeKey: 'iaq',
|
||||
levels: ['lvl-clean','lvl-good','lvl-light','lvl-moderate','lvl-heavy'],
|
||||
levelLabels: ['清洁','未污染','轻污染','中污染','重污染'],
|
||||
grades: ['I级','II级','III级','IV级','V级'],
|
||||
indexes: ['1.00','2.00','3.00','4.00','5.00'],
|
||||
descs: [
|
||||
'空气清洁,各项指标均在安全范围内,适宜正常工作生活。',
|
||||
'至少有一个环境污染物超标。除了敏感者外,一般不会发生慢急性中毒。',
|
||||
'一般有2~3个环境污染物超标,人群健康明显受害,敏感者受害严重。',
|
||||
'多项指标超标,需加强通风并检查污染源,敏感人群应离开该区域。',
|
||||
'空气质量较差,各项指标严重超标,所有人员应立即撤离并通风处理。'
|
||||
],
|
||||
colors: ['#13c2c2','#73d13d','#faad14','#fa541c','#ff4d4f'],
|
||||
devices: [
|
||||
{ id:'IA001', name:'综合空气监测仪-A01', code:'IA-2024-001', unit:'采油一厂', loc:'办公楼1层大厅', time:'14:30', level:0,
|
||||
metrics:[{k:'温度',v:'24.5',u:'℃'},{k:'湿度',v:'52',u:'%'},{k:'CO₂',v:'456',u:'ppm'},{k:'甲醛',v:'0.03',u:'mg/m³'},{k:'PM2.5',v:'35',u:'μg/m³'},{k:'PM10',v:'68',u:'μg/m³'}] },
|
||||
{ id:'IA002', name:'PM2.5检测仪-B03', code:'IA-2024-002', unit:'采油一厂', loc:'办公楼2层会议室', time:'14:28', level:1,
|
||||
metrics:[{k:'温度',v:'25.1',u:'℃'},{k:'湿度',v:'58',u:'%'},{k:'CO₂',v:'512',u:'ppm'},{k:'甲醛',v:'0.05',u:'mg/m³'},{k:'PM2.5',v:'42',u:'μg/m³'},{k:'PM10',v:'76',u:'μg/m³'}] },
|
||||
{ id:'IA003', name:'甲醛检测仪-C02', code:'IA-2024-003', unit:'采油二厂', loc:'新装修办公室3层', time:'14:25', level:2,
|
||||
metrics:[{k:'温度',v:'26.3',u:'℃'},{k:'湿度',v:'61',u:'%'},{k:'CO₂',v:'680',u:'ppm'},{k:'甲醛',v:'0.09',u:'mg/m³'},{k:'PM2.5',v:'58',u:'μg/m³'},{k:'PM10',v:'95',u:'μg/m³'}] },
|
||||
{ id:'IA004', name:'CO₂检测仪-D01', code:'IA-2024-004', unit:'采油二厂', loc:'地下车库B1层', time:'14:20', level:3,
|
||||
metrics:[{k:'温度',v:'22.8',u:'℃'},{k:'湿度',v:'72',u:'%'},{k:'CO₂',v:'920',u:'ppm'},{k:'甲醛',v:'0.07',u:'mg/m³'},{k:'PM2.5',v:'82',u:'μg/m³'},{k:'PM10',v:'128',u:'μg/m³'}] },
|
||||
{ id:'IA005', name:'综合空气监测仪-A05', code:'IA-2024-005', unit:'采油三厂', loc:'生产车间1号', time:'14:15', level:4,
|
||||
metrics:[{k:'温度',v:'28.6',u:'℃'},{k:'湿度',v:'78',u:'%'},{k:'CO₂',v:'1050',u:'ppm'},{k:'甲醛',v:'0.12',u:'mg/m³'},{k:'PM2.5',v:'120',u:'μg/m³'},{k:'PM10',v:'185',u:'μg/m³'}] }
|
||||
]
|
||||
},
|
||||
water: {
|
||||
sub: '水质综合评价', gradeKey: 'wqi',
|
||||
levels: ['lvl-pass','lvl-fail'],
|
||||
levelLabels: ['达标','未达标'],
|
||||
grades: ['I级','V级'],
|
||||
indexes: ['1.00','5.00'],
|
||||
descs: [
|
||||
'水质各项指标符合国家标准要求,可正常使用。',
|
||||
'水质存在超标项目,需检查处理设施并排查污染源,暂停使用。'
|
||||
],
|
||||
colors: ['#52c41a','#ff4d4f'],
|
||||
devices: [
|
||||
{ id:'WQ001', name:'多参数水质分析仪-A01', code:'WQ-2024-001', unit:'采油一厂', loc:'生活区供水站', time:'14:30', level:0,
|
||||
metrics:[{k:'温度',v:'18.5',u:'℃'},{k:'电导率',v:'320',u:'μS/cm'},{k:'PH',v:'7.2',u:''},{k:'盐度',v:'180',u:'mg/L'},{k:'电阻率',v:'3125',u:'Ω·m'},{k:'TDS',v:'160',u:'mg/L'}] },
|
||||
{ id:'WQ002', name:'在线PH计-B02', code:'WQ-2024-002', unit:'采油一厂', loc:'污水处理站出口', time:'14:28', level:0,
|
||||
metrics:[{k:'温度',v:'20.3',u:'℃'},{k:'电导率',v:'580',u:'μS/cm'},{k:'PH',v:'6.8',u:''},{k:'盐度',v:'320',u:'mg/L'},{k:'电阻率',v:'1724',u:'Ω·m'},{k:'TDS',v:'290',u:'mg/L'}] },
|
||||
{ id:'WQ003', name:'电导率监测仪-C01', code:'WQ-2024-003', unit:'采油二厂', loc:'注水站入口', time:'14:25', level:1,
|
||||
metrics:[{k:'温度',v:'22.1',u:'℃'},{k:'电导率',v:'1250',u:'μS/cm'},{k:'PH',v:'8.5',u:''},{k:'盐度',v:'680',u:'mg/L'},{k:'电阻率',v:'800',u:'Ω·m'},{k:'TDS',v:'625',u:'mg/L'}] },
|
||||
{ id:'WQ004', name:'TDS检测仪-D01', code:'WQ-2024-004', unit:'采油二厂', loc:'生产区储水池', time:'14:20', level:0,
|
||||
metrics:[{k:'温度',v:'19.8',u:'℃'},{k:'电导率',v:'450',u:'μS/cm'},{k:'PH',v:'7.0',u:''},{k:'盐度',v:'240',u:'mg/L'},{k:'电阻率',v:'2222',u:'Ω·m'},{k:'TDS',v:'225',u:'mg/L'}] },
|
||||
{ id:'WQ005', name:'多参数水质分析仪-A03', code:'WQ-2024-005', unit:'采油三厂', loc:'排污口监测点', time:'14:15', level:1,
|
||||
metrics:[{k:'温度',v:'23.5',u:'℃'},{k:'电导率',v:'1680',u:'μS/cm'},{k:'PH',v:'9.2',u:''},{k:'盐度',v:'920',u:'mg/L'},{k:'电阻率',v:'595',u:'Ω·m'},{k:'TDS',v:'840',u:'mg/L'}] }
|
||||
]
|
||||
},
|
||||
pollen: {
|
||||
sub: '花粉过敏风险评价', gradeKey: 'pollen',
|
||||
levels: ['lvl-vlow','lvl-low','lvl-medium','lvl-high','lvl-vhigh'],
|
||||
levelLabels: ['很低','低','中等','高','很高'],
|
||||
grades: ['I级','II级','III级','IV级','V级'],
|
||||
indexes: ['1.00','2.00','3.00','4.00','5.00'],
|
||||
descs: [
|
||||
'花粉浓度极低,对过敏人群基本无影响,可正常户外活动。',
|
||||
'花粉浓度较低,轻度过敏人群注意防护,可适当减少户外时间。',
|
||||
'花粉浓度中等,过敏人群需佩戴口罩,减少户外活动时间。',
|
||||
'花粉浓度较高,过敏人群应减少户外活动,外出需做好防护措施。',
|
||||
'花粉浓度极高,过敏人群应尽量避免外出,必要时服用抗过敏药物。'
|
||||
],
|
||||
colors: ['#52c41a','#73d13d','#faad14','#ff4d4f','#722ed1'],
|
||||
devices: [
|
||||
{ id:'PL001', name:'花粉采样器-A01', code:'PL-2024-001', unit:'采油一厂', loc:'办公区绿化带', time:'14:30', level:0,
|
||||
metrics:[{k:'榆属',v:'12',u:'粒/千mm²'},{k:'松属',v:'8',u:'粒/千mm²'},{k:'杨属',v:'15',u:'粒/千mm²'},{k:'柏科',v:'6',u:'粒/千mm²'},{k:'柳属',v:'10',u:'粒/千mm²'},{k:'蒿属',v:'5',u:'粒/千mm²'},{k:'藜科',v:'3',u:'粒/千mm²'},{k:'苋科',v:'2',u:'粒/千mm²'},{k:'花粉总数',v:'61',u:'粒/千mm²'}] },
|
||||
{ id:'PL002', name:'花粉浓度监测仪-B01', code:'PL-2024-002', unit:'采油一厂', loc:'厂区东侧林带', time:'14:28', level:1,
|
||||
metrics:[{k:'榆属',v:'35',u:'粒/千mm²'},{k:'松属',v:'22',u:'粒/千mm²'},{k:'杨属',v:'48',u:'粒/千mm²'},{k:'柏科',v:'18',u:'粒/千mm²'},{k:'柳属',v:'28',u:'粒/千mm²'},{k:'蒿属',v:'15',u:'粒/千mm²'},{k:'藜科',v:'8',u:'粒/千mm²'},{k:'苋科',v:'6',u:'粒/千mm²'},{k:'花粉总数',v:'180',u:'粒/千mm²'}] },
|
||||
{ id:'PL003', name:'花粉自动计数仪-C01', code:'PL-2024-003', unit:'采油二厂', loc:'生活区花园', time:'14:25', level:2,
|
||||
metrics:[{k:'榆属',v:'68',u:'粒/千mm²'},{k:'松属',v:'45',u:'粒/千mm²'},{k:'杨属',v:'82',u:'粒/千mm²'},{k:'柏科',v:'35',u:'粒/千mm²'},{k:'柳属',v:'52',u:'粒/千mm²'},{k:'蒿属',v:'30',u:'粒/千mm²'},{k:'藜科',v:'18',u:'粒/千mm²'},{k:'苋科',v:'12',u:'粒/千mm²'},{k:'花粉总数',v:'342',u:'粒/千mm²'}] },
|
||||
{ id:'PL004', name:'综合气象花粉站-D01', code:'PL-2024-004', unit:'采油二厂', loc:'气象观测场', time:'14:20', level:3,
|
||||
metrics:[{k:'榆属',v:'120',u:'粒/千mm²'},{k:'松属',v:'85',u:'粒/千mm²'},{k:'杨属',v:'150',u:'粒/千mm²'},{k:'柏科',v:'65',u:'粒/千mm²'},{k:'柳属',v:'95',u:'粒/千mm²'},{k:'蒿属',v:'55',u:'粒/千mm²'},{k:'藜科',v:'32',u:'粒/千mm²'},{k:'苋科',v:'22',u:'粒/千mm²'},{k:'花粉总数',v:'624',u:'粒/千mm²'}] },
|
||||
{ id:'PL005', name:'花粉采样器-A03', code:'PL-2024-005', unit:'采油三厂', loc:'杨树林防护带', time:'14:15', level:4,
|
||||
metrics:[{k:'榆属',v:'210',u:'粒/千mm²'},{k:'松属',v:'145',u:'粒/千mm²'},{k:'杨属',v:'280',u:'粒/千mm²'},{k:'柏科',v:'110',u:'粒/千mm²'},{k:'柳属',v:'168',u:'粒/千mm²'},{k:'蒿属',v:'92',u:'粒/千mm²'},{k:'藜科',v:'55',u:'粒/千mm²'},{k:'苋科',v:'38',u:'粒/千mm²'},{k:'花粉总数',v:'1098',u:'粒/千mm²'}] }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// 趋势模拟数据生成
|
||||
function genTrend(base, period) {
|
||||
const labels24 = ['00:00','04:00','08:00','12:00','16:00','20:00','24:00'];
|
||||
const labels7 = ['周一','周二','周三','周四','周五','周六','周日'];
|
||||
const labels30 = ['第1周','第2周','第3周','第4周'];
|
||||
const labels = period==='24h' ? labels24 : period==='7d' ? labels7 : labels30;
|
||||
const vals = labels.map(() => Math.round(base * (0.6 + Math.random() * 0.8)));
|
||||
return { labels, vals };
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const tab = params.get('tab') || 'outdoor';
|
||||
const id = params.get('id') || '';
|
||||
const tabData = ALL_DATA[tab];
|
||||
const dev = tabData.devices.find(d => d.id === id) || tabData.devices[0];
|
||||
const li = dev.level;
|
||||
|
||||
let trendMetricIdx = 0;
|
||||
let trendPeriod = '24h';
|
||||
|
||||
function buildGauge(levelIdx, total, color) {
|
||||
const cx = 100, cy = 100, r = 72, sw = 14;
|
||||
const zoneAngle = 180 / total;
|
||||
// background arc
|
||||
let paths = `<path d="M ${cx-r} ${cy} A ${r} ${r} 0 0 1 ${cx+r} ${cy}" fill="none" stroke="#f0f0f0" stroke-width="${sw}" stroke-linecap="round"/>`;
|
||||
// colored zones
|
||||
const zoneColors = total === 6
|
||||
? ['#52c41a','#73d13d','#faad14','#fa541c','#ff4d4f','#722ed1']
|
||||
: total === 5
|
||||
? ['#52c41a','#73d13d','#faad14','#ff4d4f','#722ed1']
|
||||
: total === 2
|
||||
? ['#52c41a','#ff4d4f']
|
||||
: tabData.colors;
|
||||
for (let i = 0; i < total; i++) {
|
||||
const a1 = (180 - i * zoneAngle) * Math.PI / 180;
|
||||
const a2 = (180 - (i+1) * zoneAngle) * Math.PI / 180;
|
||||
const x1 = cx + r * Math.cos(a1), y1 = cy - r * Math.sin(a1);
|
||||
const x2 = cx + r * Math.cos(a2), y2 = cy - r * Math.sin(a2);
|
||||
paths += `<path d="M ${x1.toFixed(1)} ${y1.toFixed(1)} A ${r} ${r} 0 0 1 ${x2.toFixed(1)} ${y2.toFixed(1)}" fill="none" stroke="${zoneColors[i]}" stroke-width="${sw}"/>`;
|
||||
}
|
||||
// needle
|
||||
const needleAngle = (180 - (levelIdx + 0.5) * zoneAngle) * Math.PI / 180;
|
||||
const nx = cx + 58 * Math.cos(needleAngle), ny = cy - 58 * Math.sin(needleAngle);
|
||||
paths += `<line x1="${cx}" y1="${cy}" x2="${nx.toFixed(1)}" y2="${ny.toFixed(1)}" stroke="#555" stroke-width="2.5" stroke-linecap="round"/>`;
|
||||
paths += `<circle cx="${cx}" cy="${cy}" r="5" fill="#555"/>`;
|
||||
return `<svg viewBox="0 0 200 110" width="200" height="110">${paths}</svg>`;
|
||||
}
|
||||
|
||||
function renderBarChart(metricIdx, period) {
|
||||
const baseVal = parseFloat(dev.metrics[metricIdx].v) || 50;
|
||||
const { labels, vals } = genTrend(baseVal, period);
|
||||
const maxVal = Math.max(...vals);
|
||||
const maxH = 80;
|
||||
const cols = vals.map((v, i) => {
|
||||
const h = Math.max(6, Math.round((v / maxVal) * maxH));
|
||||
return `<div class="bar-col"><div class="bar-val">${v}</div><div class="bar-body" style="height:${h}px"></div></div>`;
|
||||
}).join('');
|
||||
const xlabels = labels.map(l => `<span>${l}</span>`).join('');
|
||||
return `<div class="bar-chart">${cols}</div><div class="bar-x-labels">${xlabels}</div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
document.getElementById('levelName').textContent = tabData.levelLabels[li];
|
||||
document.getElementById('levelSub').textContent = tabData.sub;
|
||||
document.getElementById('indexVal').textContent = '综合指数:' + tabData.indexes[li];
|
||||
document.getElementById('gradeVal').textContent = '空气质量级别:' + tabData.grades[li];
|
||||
document.getElementById('cardLoc').textContent = dev.unit + ' · ' + dev.loc;
|
||||
document.getElementById('cardTime').textContent = '更新于今天 ' + dev.time;
|
||||
document.getElementById('cardDesc').textContent = tabData.descs[li];
|
||||
document.getElementById('gaugeWrap').innerHTML = buildGauge(li, tabData.levels.length, tabData.colors[li]);
|
||||
document.getElementById('metricsGrid').innerHTML = dev.metrics.map(m =>
|
||||
`<div class="metric-item"><div class="metric-item__val">${m.v}<span class="metric-item__unit">${m.u}</span></div><div class="metric-item__label">${m.k}</div></div>`
|
||||
).join('');
|
||||
const sel = document.getElementById('trendMetricSelect');
|
||||
sel.innerHTML = dev.metrics.map((m, i) => `<option value="${i}">${m.k}</option>`).join('');
|
||||
sel.value = trendMetricIdx;
|
||||
document.getElementById('barChartWrap').innerHTML = renderBarChart(trendMetricIdx, trendPeriod);
|
||||
}
|
||||
|
||||
function onTrendMetricChange(sel) {
|
||||
trendMetricIdx = parseInt(sel.value);
|
||||
document.getElementById('barChartWrap').innerHTML = renderBarChart(trendMetricIdx, trendPeriod);
|
||||
}
|
||||
|
||||
function switchTimePeriod(el, period) {
|
||||
trendPeriod = period;
|
||||
document.querySelectorAll('.time-tab').forEach(t => t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
document.getElementById('barChartWrap').innerHTML = renderBarChart(trendMetricIdx, trendPeriod);
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,612 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>环境监测 - 员工端 | 健康长庆原型</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #f0f2f5; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; }
|
||||
.phone-shell { width: 390px; height: 844px; background: #f5f7fa; border-radius: 44px; box-shadow: 0 20px 60px rgba(0,0,0,0.25), inset 0 0 0 2px #e0e0e0; overflow: hidden; position: relative; display: flex; flex-direction: column; }
|
||||
|
||||
/* 状态栏 */
|
||||
.status-bar { height: 44px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; background: #fff; }
|
||||
.status-bar .time { font-size: 15px; font-weight: 700; color: #333; }
|
||||
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; margin-right: 28px; }
|
||||
|
||||
/* Tab 栏 */
|
||||
.tab-bar { background: #fff; display: flex; padding: 0 8px; border-bottom: 1px solid #f0f0f0; flex-shrink: 0; }
|
||||
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px 4px 6px; cursor: pointer; position: relative; transition: color 0.2s; }
|
||||
.tab-item .tab-icon { font-size: 20px; line-height: 1; }
|
||||
.tab-item .tab-label { font-size: 11px; color: #999; font-weight: 500; }
|
||||
.tab-item.active .tab-label { color: #1aad8d; font-weight: 600; }
|
||||
.tab-item::after { content: ''; position: absolute; bottom: 0; left: 20%; right: 20%; height: 2px; border-radius: 1px; background: transparent; transition: background 0.2s; }
|
||||
.tab-item.active::after { background: #1aad8d; }
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content { flex: 1; position: relative; overflow: hidden; }
|
||||
|
||||
/* ===== 地图视图 ===== */
|
||||
.map-view { position: absolute; inset: 0; display: flex; flex-direction: column; }
|
||||
.map-view.hidden { display: none; }
|
||||
|
||||
/* 模拟地图背景 */
|
||||
.map-bg {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #e8f0e8;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 地图道路 */
|
||||
.map-bg::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(90deg, transparent 48%, #d4c89a 48%, #d4c89a 52%, transparent 52%),
|
||||
linear-gradient(0deg, transparent 38%, #d4c89a 38%, #d4c89a 42%, transparent 42%),
|
||||
linear-gradient(30deg, transparent 60%, #cfc282 60%, #cfc282 63%, transparent 63%),
|
||||
linear-gradient(120deg, transparent 30%, #cfc282 30%, #cfc282 33%, transparent 33%);
|
||||
}
|
||||
.map-bg::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80px 50px at 30% 65%, #b8d4a8 0%, transparent 100%),
|
||||
radial-gradient(ellipse 60px 40px at 70% 30%, #c0dab0 0%, transparent 100%),
|
||||
radial-gradient(ellipse 100px 60px at 55% 75%, #aed09e 0%, transparent 100%),
|
||||
radial-gradient(ellipse 40px 40px at 20% 35%, #b8d4a8 0%, transparent 100%);
|
||||
}
|
||||
|
||||
/* 地图定位按钮 */
|
||||
.map-locate-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 80px;
|
||||
width: 36px; height: 36px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 图例 */
|
||||
.map-legend {
|
||||
position: absolute;
|
||||
top: 12px; left: 12px;
|
||||
background: rgba(255,255,255,0.92);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
||||
}
|
||||
.legend-item {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px; color: #333;
|
||||
}
|
||||
.legend-item:last-child { margin-bottom: 0; }
|
||||
.legend-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
.map-search {
|
||||
position: absolute;
|
||||
top: 12px; left: 80px; right: 12px;
|
||||
background: rgba(255,255,255,0.92);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 20px;
|
||||
height: 36px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 12px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
||||
font-size: 13px; color: #999;
|
||||
}
|
||||
|
||||
/* 设备标记 */
|
||||
.device-marker {
|
||||
position: absolute;
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 5px 10px 5px 6px;
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.18);
|
||||
cursor: pointer;
|
||||
font-size: 12px; font-weight: 600;
|
||||
color: #fff;
|
||||
z-index: 5;
|
||||
transition: transform 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.device-marker:active { transform: scale(0.95); }
|
||||
.marker-icon { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 11px; }
|
||||
|
||||
/* 等级颜色 */
|
||||
.lvl-excellent, .marker-icon.lvl-excellent { background: #52c41a; }
|
||||
.lvl-good, .marker-icon.lvl-good { background: #73d13d; }
|
||||
.lvl-light, .marker-icon.lvl-light { background: #faad14; }
|
||||
.lvl-moderate, .marker-icon.lvl-moderate { background: #fa541c; }
|
||||
.lvl-heavy, .marker-icon.lvl-heavy { background: #ff4d4f; }
|
||||
.lvl-severe, .marker-icon.lvl-severe { background: #722ed1; }
|
||||
.lvl-clean, .marker-icon.lvl-clean { background: #13c2c2; }
|
||||
.lvl-pass, .marker-icon.lvl-pass { background: #52c41a; }
|
||||
.lvl-fail, .marker-icon.lvl-fail { background: #ff4d4f; }
|
||||
.lvl-vlow, .marker-icon.lvl-vlow { background: #52c41a; }
|
||||
.lvl-low, .marker-icon.lvl-low { background: #73d13d; }
|
||||
.lvl-medium, .marker-icon.lvl-medium { background: #faad14; }
|
||||
.lvl-high, .marker-icon.lvl-high { background: #ff4d4f; }
|
||||
.lvl-vhigh, .marker-icon.lvl-vhigh { background: #722ed1; }
|
||||
|
||||
.device-marker .marker-label { color: #333; }
|
||||
|
||||
/* 列表切换按钮 */
|
||||
.list-toggle-btn {
|
||||
position: absolute;
|
||||
bottom: 20px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #1aad8d;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
height: 44px;
|
||||
padding: 0 28px;
|
||||
font-size: 15px; font-weight: 600;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px rgba(26,173,141,0.4);
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ===== 列表视图 ===== */
|
||||
.list-view { position: absolute; inset: 0; overflow-y: auto; background: #f5f7fa; padding: 12px 14px; display: none; }
|
||||
.list-view::-webkit-scrollbar { display: none; }
|
||||
.list-view.active { display: block; }
|
||||
.list-view-header {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.list-filter-select {
|
||||
height: 32px; border: 1px solid #e8e8e8; border-radius: 8px;
|
||||
padding: 0 24px 0 10px; font-size: 12px; color: #333;
|
||||
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E") no-repeat right 8px center;
|
||||
appearance: none; cursor: pointer; outline: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.list-search-input {
|
||||
flex: 1; height: 32px; border: 1px solid #e8e8e8; border-radius: 8px;
|
||||
padding: 0 10px; font-size: 12px; color: #333; outline: none;
|
||||
background: #fff; min-width: 0;
|
||||
}
|
||||
.list-search-input:focus { border-color: #1aad8d; }
|
||||
.map-toggle-btn {
|
||||
background: #1aad8d; color: #fff; border: none;
|
||||
border-radius: 16px; height: 32px; padding: 0 12px;
|
||||
font-size: 12px; font-weight: 600; cursor: pointer;
|
||||
display: flex; align-items: center; gap: 4px; flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 设备列表卡片 */
|
||||
.device-card {
|
||||
background: #fff; border-radius: 14px; padding: 13px 14px;
|
||||
margin-bottom: 10px; cursor: pointer;
|
||||
box-shadow: 0 1px 6px rgba(0,0,0,0.06);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.device-card:active { transform: scale(0.99); }
|
||||
.device-card__header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 8px; }
|
||||
.device-card__name { font-size: 13px; font-weight: 600; color: #1a1a1a; }
|
||||
.device-card__loc { font-size: 11px; color: #999; margin-top: 2px; }
|
||||
.device-card__badge {
|
||||
display: inline-flex; align-items: center;
|
||||
font-size: 11px; font-weight: 700;
|
||||
padding: 3px 10px; border-radius: 12px; color: #fff; white-space: nowrap;
|
||||
}
|
||||
.device-card__metrics { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.metric-chip { font-size: 11px; color: #666; background: #f5f7fa; border-radius: 8px; padding: 3px 8px; }
|
||||
.metric-chip strong { color: #333; }
|
||||
|
||||
/* ===== 底部弹窗 ===== */
|
||||
.bottom-sheet-mask {
|
||||
position: absolute; inset: 0; background: rgba(0,0,0,0.3);
|
||||
z-index: 20; display: none; align-items: flex-end;
|
||||
}
|
||||
.bottom-sheet-mask.active { display: flex; }
|
||||
.bottom-sheet {
|
||||
width: 100%; background: #fff;
|
||||
border-radius: 20px 20px 0 0;
|
||||
padding: 0 0 24px;
|
||||
max-height: 65%;
|
||||
overflow-y: auto;
|
||||
animation: slideUp 0.25s ease;
|
||||
}
|
||||
.bottom-sheet::-webkit-scrollbar { display: none; }
|
||||
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
||||
.sheet-handle { width: 36px; height: 4px; background: #e0e0e0; border-radius: 2px; margin: 10px auto 14px; }
|
||||
.sheet-header { display: flex; align-items: flex-start; justify-content: space-between; padding: 0 16px 12px; border-bottom: 1px solid #f5f5f5; }
|
||||
.sheet-title { font-size: 15px; font-weight: 700; color: #1a1a1a; }
|
||||
.sheet-subtitle { font-size: 11px; color: #999; margin-top: 2px; }
|
||||
.sheet-close { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #bbb; font-size: 18px; }
|
||||
.sheet-body { padding: 14px 16px 0; }
|
||||
.sheet-level-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
||||
.sheet-level-badge { font-size: 13px; font-weight: 700; padding: 4px 14px; border-radius: 14px; color: #fff; }
|
||||
.sheet-level-desc { font-size: 12px; color: #666; }
|
||||
.sheet-info-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px; background: #f0f0f0; border-radius: 12px; overflow: hidden; margin-bottom: 14px; }
|
||||
.sheet-info-cell { background: #fff; padding: 10px 12px; }
|
||||
.sheet-info-cell__label { font-size: 11px; color: #999; margin-bottom: 3px; }
|
||||
.sheet-info-cell__value { font-size: 14px; font-weight: 700; color: #1a1a1a; }
|
||||
.sheet-info-cell__unit { font-size: 10px; color: #aaa; font-weight: 400; }
|
||||
.sheet-device-info { background: #f8f9fb; border-radius: 10px; padding: 10px 12px; }
|
||||
.sheet-device-info__row { display: flex; align-items: center; justify-content: space-between; padding: 4px 0; font-size: 12px; }
|
||||
.sheet-device-info__label { color: #999; }
|
||||
.sheet-device-info__val { color: #333; font-weight: 500; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="phone-shell">
|
||||
<!-- 状态栏 -->
|
||||
<div class="status-bar">
|
||||
<span class="time">9:41</span>
|
||||
<span class="icons">
|
||||
<svg width="16" height="16" fill="none"><rect x="1" y="4" width="3" height="8" rx="1" fill="#333"/><rect x="5" y="3" width="3" height="9" rx="1" fill="#333"/><rect x="9" y="1" width="3" height="11" rx="1" fill="#333"/><rect x="13" y="0" width="3" height="12" rx="1" fill="#333" opacity=".3"/></svg>
|
||||
<svg width="16" height="16" fill="none"><path d="M8 3C5.5 3 3.2 4 1.5 5.5l1.4 1.4C4.3 5.5 6.1 5 8 5s3.7.5 5.1 1.9l1.4-1.4C12.8 4 10.5 3 8 3zm0 4c-1.4 0-2.6.5-3.5 1.3L6 9.8c.6-.5 1.3-.8 2-.8s1.4.3 2 .8l1.5-1.5C10.6 7.5 9.4 7 8 7zm0 4c-.6 0-1 .4-1 1s.4 1 1 1 1-.4 1-1-.4-1-1-1z" fill="#333"/></svg>
|
||||
<svg width="22" height="12" fill="none"><rect x=".5" y=".5" width="19" height="11" rx="2" stroke="#333"/><rect x="20" y="3.5" width="1.5" height="5" rx=".75" fill="#333"/><rect x="2" y="2" width="14" height="8" rx="1" fill="#333"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<div class="nav-bar">
|
||||
<div class="nav-back" onclick="location.href='../index.html'">
|
||||
<svg width="12" height="20" fill="none"><path d="M10 2L2 10l8 8" stroke="#1a1a1a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<div class="nav-title">环境监测</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 栏 -->
|
||||
<div class="tab-bar">
|
||||
<div class="tab-item active" data-tab="outdoor" onclick="switchTab('outdoor')">
|
||||
<span class="tab-icon">🏔️</span>
|
||||
<span class="tab-label">室外空气</span>
|
||||
</div>
|
||||
<div class="tab-item" data-tab="indoor" onclick="switchTab('indoor')">
|
||||
<span class="tab-icon">🏠</span>
|
||||
<span class="tab-label">室内空气</span>
|
||||
</div>
|
||||
<div class="tab-item" data-tab="water" onclick="switchTab('water')">
|
||||
<span class="tab-icon">💧</span>
|
||||
<span class="tab-label">水质监测</span>
|
||||
</div>
|
||||
<div class="tab-item" data-tab="pollen" onclick="switchTab('pollen')">
|
||||
<span class="tab-icon">🌸</span>
|
||||
<span class="tab-label">花粉监测</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<div class="main-content">
|
||||
|
||||
<!-- 地图视图 -->
|
||||
<div class="map-view" id="mapView">
|
||||
<div class="map-bg" id="mapBg">
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="map-search">
|
||||
<svg width="14" height="14" fill="none"><circle cx="6" cy="6" r="5" stroke="#999" stroke-width="1.5"/><path d="M10 10l3 3" stroke="#999" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
<span>搜索地址</span>
|
||||
</div>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="map-legend" id="mapLegend"></div>
|
||||
|
||||
<!-- 设备标记容器 -->
|
||||
<div id="markerContainer"></div>
|
||||
|
||||
<!-- 定位按钮 -->
|
||||
<div class="map-locate-btn">
|
||||
<svg width="18" height="18" fill="none"><circle cx="9" cy="9" r="3" fill="#1aad8d"/><path d="M9 1v3M9 15v3M1 9h3M15 9h3" stroke="#1aad8d" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列表切换按钮 -->
|
||||
<button class="list-toggle-btn" onclick="showListView()">
|
||||
<svg width="16" height="14" fill="none"><rect x="0" y="0" width="16" height="2.5" rx="1.25" fill="#fff"/><rect x="0" y="5.5" width="16" height="2.5" rx="1.25" fill="#fff"/><rect x="0" y="11" width="16" height="2.5" rx="1.25" fill="#fff"/></svg>
|
||||
列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 列表视图 -->
|
||||
<div class="list-view" id="listView">
|
||||
<div class="list-view-header">
|
||||
<select class="list-filter-select" id="levelFilter" onchange="applyListFilter()">
|
||||
<option value="">全部等级</option>
|
||||
</select>
|
||||
<input class="list-search-input" id="nameSearch" placeholder="搜索设备名称" oninput="applyListFilter()">
|
||||
<button class="map-toggle-btn" onclick="showMapView()">
|
||||
<svg width="14" height="14" fill="none"><path d="M1 5l4-3 4 3 4-3v9l-4 3-4-3-4 3V5z" stroke="#fff" stroke-width="1.3" stroke-linejoin="round"/></svg>
|
||||
地图
|
||||
</button>
|
||||
</div>
|
||||
<div id="listContainer"></div>
|
||||
</div>
|
||||
|
||||
</div><!-- /main-content -->
|
||||
|
||||
<!-- 底部弹窗 -->
|
||||
<div class="bottom-sheet-mask" id="sheetMask" onclick="closeSheet(event)">
|
||||
<div class="bottom-sheet" id="bottomSheet">
|
||||
<div class="sheet-handle"></div>
|
||||
<div class="sheet-header">
|
||||
<div>
|
||||
<div class="sheet-title" id="sheetTitle">设备名称</div>
|
||||
<div class="sheet-subtitle" id="sheetSubtitle">设备位置</div>
|
||||
</div>
|
||||
<div class="sheet-close" onclick="closeSheetDirect()">×</div>
|
||||
</div>
|
||||
<div class="sheet-body" id="sheetBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /phone-shell -->
|
||||
|
||||
<script>
|
||||
// ===== 数据定义 =====
|
||||
const DATA = {
|
||||
outdoor: {
|
||||
title: '室外空气监测点',
|
||||
legend: [
|
||||
{ cls: 'lvl-excellent', label: '优' },
|
||||
{ cls: 'lvl-good', label: '良' },
|
||||
{ cls: 'lvl-light', label: '轻度污染' },
|
||||
{ cls: 'lvl-moderate', label: '中度污染' },
|
||||
{ cls: 'lvl-heavy', label: '重度污染' },
|
||||
{ cls: 'lvl-severe', label: '严重污染' },
|
||||
],
|
||||
levelDesc: {
|
||||
'lvl-excellent': '空气质量令人满意,基本无污染',
|
||||
'lvl-good': '空气质量可接受,少数敏感人群注意',
|
||||
'lvl-light': '易感人群症状有轻度加剧,健康人群无明显影响',
|
||||
'lvl-moderate': '敏感人群症状明显加重,健康人群普遍出现刺激症状',
|
||||
'lvl-heavy': '心脏病和肺病患者症状显著加剧,运动耐受力降低',
|
||||
'lvl-severe': '健康人群运动耐受力降低,有明显强烈症状,提前出现某些疾病',
|
||||
},
|
||||
devices: [
|
||||
{ id:'OA001', name:'空气质量监测站-A01', code:'OA-2024-001', unit:'采油一厂', loc:'厂区大门东侧', time:'14:30', level:'lvl-excellent', levelLabel:'优', x:200, y:100, metrics:[{k:'温度',v:'32.5',u:'℃'},{k:'湿度',v:'45',u:'%'},{k:'太阳辐射',v:'680',u:'W/m²'},{k:'PM2.5',v:'28',u:'μg/m³'},{k:'PM10',v:'52',u:'μg/m³'},{k:'照度',v:'65000',u:'lm/m²'},{k:'风速',v:'3.2',u:'m/s'}] },
|
||||
{ id:'OA002', name:'PM10检测仪-B02', code:'OA-2024-002', unit:'采油一厂', loc:'生产区北围栏', time:'14:28', level:'lvl-good', levelLabel:'良', x:120, y:180, metrics:[{k:'温度',v:'33.1',u:'℃'},{k:'湿度',v:'42',u:'%'},{k:'太阳辐射',v:'720',u:'W/m²'},{k:'PM2.5',v:'38',u:'μg/m³'},{k:'PM10',v:'68',u:'μg/m³'},{k:'照度',v:'72000',u:'lm/m²'},{k:'风速',v:'4.1',u:'m/s'}] },
|
||||
{ id:'OA003', name:'臭氧检测仪-C01', code:'OA-2024-003', unit:'采油二厂', loc:'办公楼楼顶', time:'14:25', level:'lvl-light', levelLabel:'轻度污染', x:260, y:210, metrics:[{k:'温度',v:'31.8',u:'℃'},{k:'湿度',v:'50',u:'%'},{k:'太阳辐射',v:'580',u:'W/m²'},{k:'PM2.5',v:'55',u:'μg/m³'},{k:'PM10',v:'92',u:'μg/m³'},{k:'照度',v:'58000',u:'lm/m²'},{k:'风速',v:'2.8',u:'m/s'}] },
|
||||
{ id:'OA004', name:'气象参数监测仪-D01', code:'OA-2024-004', unit:'采油三厂', loc:'气象观测场', time:'14:20', level:'lvl-moderate', levelLabel:'中度污染', x:80, y:300, metrics:[{k:'温度',v:'34.2',u:'℃'},{k:'湿度',v:'38',u:'%'},{k:'太阳辐射',v:'850',u:'W/m²'},{k:'PM2.5',v:'82',u:'μg/m³'},{k:'PM10',v:'135',u:'μg/m³'},{k:'照度',v:'85000',u:'lm/m²'},{k:'风速',v:'5.6',u:'m/s'}] },
|
||||
{ id:'OA005', name:'空气质量监测站-A03', code:'OA-2024-005', unit:'采油三厂', loc:'储油罐区南侧', time:'14:15', level:'lvl-heavy', levelLabel:'重度污染', x:210, y:340, metrics:[{k:'温度',v:'35.0',u:'℃'},{k:'湿度',v:'35',u:'%'},{k:'太阳辐射',v:'920',u:'W/m²'},{k:'PM2.5',v:'125',u:'μg/m³'},{k:'PM10',v:'190',u:'μg/m³'},{k:'照度',v:'92000',u:'lm/m²'},{k:'风速',v:'6.3',u:'m/s'}] },
|
||||
{ id:'OA006', name:'VOC检测仪-E01', code:'OA-2024-006', unit:'采油三厂', loc:'化工装置区', time:'14:10', level:'lvl-severe', levelLabel:'严重污染', x:310, y:280, metrics:[{k:'温度',v:'36.2',u:'℃'},{k:'湿度',v:'30',u:'%'},{k:'太阳辐射',v:'950',u:'W/m²'},{k:'PM2.5',v:'180',u:'μg/m³'},{k:'PM10',v:'260',u:'μg/m³'},{k:'照度',v:'95000',u:'lm/m²'},{k:'风速',v:'7.1',u:'m/s'}] },
|
||||
]
|
||||
},
|
||||
indoor: {
|
||||
title: '室内空气监测点',
|
||||
legend: [
|
||||
{ cls: 'lvl-clean', label: '清洁' },
|
||||
{ cls: 'lvl-good', label: '未污染' },
|
||||
{ cls: 'lvl-light', label: '轻污染' },
|
||||
{ cls: 'lvl-moderate', label: '中污染' },
|
||||
{ cls: 'lvl-heavy', label: '重污染' },
|
||||
],
|
||||
levelDesc: {
|
||||
'lvl-clean': '空气清洁,各项指标均在安全范围内',
|
||||
'lvl-good': '空气质量良好,适宜正常工作',
|
||||
'lvl-light': '部分指标轻微超标,建议适当通风',
|
||||
'lvl-moderate': '多项指标超标,需加强通风并检查原因',
|
||||
'lvl-heavy': '空气质量较差,敏感人群应离开该区域',
|
||||
},
|
||||
devices: [
|
||||
{ id:'IA001', name:'综合空气监测仪-A01', code:'IA-2024-001', unit:'采油一厂', loc:'办公楼1层大厅', time:'14:30', level:'lvl-clean', levelLabel:'清洁', x:200, y:100, metrics:[{k:'温度',v:'24.5',u:'℃'},{k:'湿度',v:'52',u:'%'},{k:'CO₂',v:'456',u:'ppm'},{k:'甲醛',v:'0.03',u:'mg/m³'},{k:'PM2.5',v:'35',u:'μg/m³'},{k:'PM10',v:'68',u:'μg/m³'}] },
|
||||
{ id:'IA002', name:'PM2.5检测仪-B03', code:'IA-2024-002', unit:'采油一厂', loc:'办公楼2层会议室', time:'14:28', level:'lvl-good', levelLabel:'未污染', x:130, y:185, metrics:[{k:'温度',v:'25.1',u:'℃'},{k:'湿度',v:'58',u:'%'},{k:'CO₂',v:'512',u:'ppm'},{k:'甲醛',v:'0.05',u:'mg/m³'},{k:'PM2.5',v:'42',u:'μg/m³'},{k:'PM10',v:'76',u:'μg/m³'}] },
|
||||
{ id:'IA003', name:'甲醛检测仪-C02', code:'IA-2024-003', unit:'采油二厂', loc:'新装修办公室3层', time:'14:25', level:'lvl-light', levelLabel:'轻污染', x:270, y:220, metrics:[{k:'温度',v:'26.3',u:'℃'},{k:'湿度',v:'61',u:'%'},{k:'CO₂',v:'680',u:'ppm'},{k:'甲醛',v:'0.09',u:'mg/m³'},{k:'PM2.5',v:'58',u:'μg/m³'},{k:'PM10',v:'95',u:'μg/m³'}] },
|
||||
{ id:'IA004', name:'CO₂检测仪-D01', code:'IA-2024-004', unit:'采油二厂', loc:'地下车库B1层', time:'14:20', level:'lvl-moderate', levelLabel:'中污染', x:90, y:305, metrics:[{k:'温度',v:'22.8',u:'℃'},{k:'湿度',v:'72',u:'%'},{k:'CO₂',v:'920',u:'ppm'},{k:'甲醛',v:'0.07',u:'mg/m³'},{k:'PM2.5',v:'82',u:'μg/m³'},{k:'PM10',v:'128',u:'μg/m³'}] },
|
||||
{ id:'IA005', name:'综合空气监测仪-A05', code:'IA-2024-005', unit:'采油三厂', loc:'生产车间1号', time:'14:15', level:'lvl-heavy', levelLabel:'重污染', x:210, y:340, metrics:[{k:'温度',v:'28.6',u:'℃'},{k:'湿度',v:'78',u:'%'},{k:'CO₂',v:'1050',u:'ppm'},{k:'甲醛',v:'0.12',u:'mg/m³'},{k:'PM2.5',v:'120',u:'μg/m³'},{k:'PM10',v:'185',u:'μg/m³'}] },
|
||||
]
|
||||
},
|
||||
water: {
|
||||
title: '水质监测点',
|
||||
legend: [
|
||||
{ cls: 'lvl-pass', label: '达标' },
|
||||
{ cls: 'lvl-fail', label: '未达标' },
|
||||
],
|
||||
levelDesc: {
|
||||
'lvl-pass': '水质各项指标符合标准要求',
|
||||
'lvl-fail': '水质存在超标项目,需检查处理',
|
||||
},
|
||||
devices: [
|
||||
{ id:'WQ001', name:'多参数水质分析仪-A01', code:'WQ-2024-001', unit:'采油一厂', loc:'生活区供水站', time:'14:30', level:'lvl-pass', levelLabel:'达标', x:180, y:110, metrics:[{k:'温度',v:'18.5',u:'℃'},{k:'电导率',v:'320',u:'μS/cm'},{k:'PH',v:'7.2',u:''},{k:'盐度',v:'180',u:'mg/L'},{k:'电阻率',v:'3125',u:'Ω·m'},{k:'TDS',v:'160',u:'mg/L'}] },
|
||||
{ id:'WQ002', name:'在线PH计-B02', code:'WQ-2024-002', unit:'采油一厂', loc:'污水处理站出口', time:'14:28', level:'lvl-pass', levelLabel:'达标', x:110, y:200, metrics:[{k:'温度',v:'20.3',u:'℃'},{k:'电导率',v:'580',u:'μS/cm'},{k:'PH',v:'6.8',u:''},{k:'盐度',v:'320',u:'mg/L'},{k:'电阻率',v:'1724',u:'Ω·m'},{k:'TDS',v:'290',u:'mg/L'}] },
|
||||
{ id:'WQ003', name:'电导率监测仪-C01', code:'WQ-2024-003', unit:'采油二厂', loc:'注水站入口', time:'14:25', level:'lvl-fail', levelLabel:'未达标', x:260, y:215, metrics:[{k:'温度',v:'22.1',u:'℃'},{k:'电导率',v:'1250',u:'μS/cm'},{k:'PH',v:'8.5',u:''},{k:'盐度',v:'680',u:'mg/L'},{k:'电阻率',v:'800',u:'Ω·m'},{k:'TDS',v:'625',u:'mg/L'}] },
|
||||
{ id:'WQ004', name:'TDS检测仪-D01', code:'WQ-2024-004', unit:'采油二厂', loc:'生产区储水池', time:'14:20', level:'lvl-pass', levelLabel:'达标', x:80, y:310, metrics:[{k:'温度',v:'19.8',u:'℃'},{k:'电导率',v:'450',u:'μS/cm'},{k:'PH',v:'7.0',u:''},{k:'盐度',v:'240',u:'mg/L'},{k:'电阻率',v:'2222',u:'Ω·m'},{k:'TDS',v:'225',u:'mg/L'}] },
|
||||
{ id:'WQ005', name:'多参数水质分析仪-A03', code:'WQ-2024-005', unit:'采油三厂', loc:'排污口监测点', time:'14:15', level:'lvl-fail', levelLabel:'未达标', x:220, y:345, metrics:[{k:'温度',v:'23.5',u:'℃'},{k:'电导率',v:'1680',u:'μS/cm'},{k:'PH',v:'9.2',u:''},{k:'盐度',v:'920',u:'mg/L'},{k:'电阻率',v:'595',u:'Ω·m'},{k:'TDS',v:'840',u:'mg/L'}] },
|
||||
]
|
||||
},
|
||||
pollen: {
|
||||
title: '花粉监测点',
|
||||
legend: [
|
||||
{ cls: 'lvl-vlow', label: '很低' },
|
||||
{ cls: 'lvl-low', label: '低' },
|
||||
{ cls: 'lvl-medium', label: '中等' },
|
||||
{ cls: 'lvl-high', label: '高' },
|
||||
{ cls: 'lvl-vhigh', label: '很高' },
|
||||
],
|
||||
levelDesc: {
|
||||
'lvl-vlow': '花粉浓度极低,对过敏人群基本无影响',
|
||||
'lvl-low': '花粉浓度较低,轻度过敏人群注意防护',
|
||||
'lvl-medium': '花粉浓度中等,过敏人群需佩戴口罩',
|
||||
'lvl-high': '花粉浓度较高,过敏人群减少户外活动',
|
||||
'lvl-vhigh': '花粉浓度极高,过敏人群应尽量避免外出',
|
||||
},
|
||||
devices: [
|
||||
{ id:'PL001', name:'花粉采样器-A01', code:'PL-2024-001', unit:'采油一厂', loc:'办公区绿化带', time:'14:30', level:'lvl-vlow', levelLabel:'很低', x:190, y:105, metrics:[{k:'榆属',v:'12',u:'粒/千mm²'},{k:'松属',v:'8',u:'粒/千mm²'},{k:'杨属',v:'15',u:'粒/千mm²'},{k:'柏科',v:'6',u:'粒/千mm²'},{k:'柳属',v:'10',u:'粒/千mm²'},{k:'蒿属',v:'5',u:'粒/千mm²'},{k:'藜科',v:'3',u:'粒/千mm²'},{k:'苋科',v:'2',u:'粒/千mm²'},{k:'花粉总数',v:'61',u:'粒/千mm²'}] },
|
||||
{ id:'PL002', name:'花粉浓度监测仪-B01', code:'PL-2024-002', unit:'采油一厂', loc:'厂区东侧林带', time:'14:28', level:'lvl-low', levelLabel:'低', x:120, y:195, metrics:[{k:'榆属',v:'35',u:'粒/千mm²'},{k:'松属',v:'22',u:'粒/千mm²'},{k:'杨属',v:'48',u:'粒/千mm²'},{k:'柏科',v:'18',u:'粒/千mm²'},{k:'柳属',v:'28',u:'粒/千mm²'},{k:'蒿属',v:'15',u:'粒/千mm²'},{k:'藜科',v:'8',u:'粒/千mm²'},{k:'苋科',v:'6',u:'粒/千mm²'},{k:'花粉总数',v:'180',u:'粒/千mm²'}] },
|
||||
{ id:'PL003', name:'花粉自动计数仪-C01', code:'PL-2024-003', unit:'采油二厂', loc:'生活区花园', time:'14:25', level:'lvl-medium', levelLabel:'中等', x:265, y:220, metrics:[{k:'榆属',v:'68',u:'粒/千mm²'},{k:'松属',v:'45',u:'粒/千mm²'},{k:'杨属',v:'82',u:'粒/千mm²'},{k:'柏科',v:'35',u:'粒/千mm²'},{k:'柳属',v:'52',u:'粒/千mm²'},{k:'蒿属',v:'30',u:'粒/千mm²'},{k:'藜科',v:'18',u:'粒/千mm²'},{k:'苋科',v:'12',u:'粒/千mm²'},{k:'花粉总数',v:'342',u:'粒/千mm²'}] },
|
||||
{ id:'PL004', name:'综合气象花粉站-D01', code:'PL-2024-004', unit:'采油二厂', loc:'气象观测场', time:'14:20', level:'lvl-high', levelLabel:'高', x:80, y:305, metrics:[{k:'榆属',v:'120',u:'粒/千mm²'},{k:'松属',v:'85',u:'粒/千mm²'},{k:'杨属',v:'150',u:'粒/千mm²'},{k:'柏科',v:'65',u:'粒/千mm²'},{k:'柳属',v:'95',u:'粒/千mm²'},{k:'蒿属',v:'55',u:'粒/千mm²'},{k:'藜科',v:'32',u:'粒/千mm²'},{k:'苋科',v:'22',u:'粒/千mm²'},{k:'花粉总数',v:'624',u:'粒/千mm²'}] },
|
||||
{ id:'PL005', name:'花粉采样器-A03', code:'PL-2024-005', unit:'采油三厂', loc:'杨树林防护带', time:'14:15', level:'lvl-vhigh', levelLabel:'很高', x:215, y:345, metrics:[{k:'榆属',v:'210',u:'粒/千mm²'},{k:'松属',v:'145',u:'粒/千mm²'},{k:'杨属',v:'280',u:'粒/千mm²'},{k:'柏科',v:'110',u:'粒/千mm²'},{k:'柳属',v:'168',u:'粒/千mm²'},{k:'蒿属',v:'92',u:'粒/千mm²'},{k:'藜科',v:'55',u:'粒/千mm²'},{k:'苋科',v:'38',u:'粒/千mm²'},{k:'花粉总数',v:'1098',u:'粒/千mm²'}] },
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
let currentTab = 'outdoor';
|
||||
|
||||
// 等级颜色映射
|
||||
const LVL_COLOR = {
|
||||
'lvl-excellent':'#52c41a','lvl-good':'#73d13d','lvl-light':'#faad14',
|
||||
'lvl-moderate':'#fa541c','lvl-heavy':'#ff4d4f','lvl-severe':'#722ed1',
|
||||
'lvl-clean':'#13c2c2','lvl-pass':'#52c41a','lvl-fail':'#ff4d4f',
|
||||
'lvl-vlow':'#52c41a','lvl-low':'#73d13d','lvl-medium':'#faad14',
|
||||
'lvl-high':'#ff4d4f','lvl-vhigh':'#722ed1'
|
||||
};
|
||||
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
document.querySelectorAll('.tab-item').forEach(el => {
|
||||
el.classList.toggle('active', el.dataset.tab === tab);
|
||||
});
|
||||
renderLegend();
|
||||
renderMarkers();
|
||||
renderList();
|
||||
closeSheetDirect();
|
||||
}
|
||||
|
||||
function renderLegend() {
|
||||
const d = DATA[currentTab];
|
||||
const el = document.getElementById('mapLegend');
|
||||
el.innerHTML = d.legend.map(l =>
|
||||
`<div class="legend-item">
|
||||
<span class="legend-dot" style="background:${LVL_COLOR[l.cls]}"></span>
|
||||
<span>${l.label}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function renderMarkers() {
|
||||
const d = DATA[currentTab];
|
||||
const c = document.getElementById('markerContainer');
|
||||
const mapH = document.getElementById('mapBg').offsetHeight || 400;
|
||||
c.innerHTML = d.devices.map(dev => {
|
||||
const color = LVL_COLOR[dev.level];
|
||||
// y坐标根据实际地图高度适配(数据定义基于400px高度)
|
||||
const yAdj = Math.round(dev.y * mapH / 400);
|
||||
return `<div class="device-marker" style="left:${dev.x}px;top:${yAdj}px" onclick="showSheet('${dev.id}')">
|
||||
<span class="marker-icon" style="background:${color}">
|
||||
<svg width="10" height="10" fill="none"><path d="M5 1l1.5 3h3l-2.5 2 1 3L5 7.5 2 9l1-3L.5 4h3z" fill="#fff"/></svg>
|
||||
</span>
|
||||
<span class="marker-label" style="color:${color}">${dev.levelLabel}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const d = DATA[currentTab];
|
||||
// 重置筛选
|
||||
const sel = document.getElementById('levelFilter');
|
||||
sel.innerHTML = '<option value="">全部等级</option>' +
|
||||
d.legend.map(l => `<option value="${l.cls}">${l.label}</option>`).join('');
|
||||
document.getElementById('nameSearch').value = '';
|
||||
renderListCards();
|
||||
}
|
||||
|
||||
function applyListFilter() {
|
||||
renderListCards();
|
||||
}
|
||||
|
||||
function renderListCards() {
|
||||
const d = DATA[currentTab];
|
||||
const levelVal = document.getElementById('levelFilter').value;
|
||||
const nameVal = document.getElementById('nameSearch').value.trim().toLowerCase();
|
||||
const filtered = d.devices.filter(dev => {
|
||||
if (levelVal && dev.level !== levelVal) return false;
|
||||
if (nameVal && !dev.name.toLowerCase().includes(nameVal)) return false;
|
||||
return true;
|
||||
});
|
||||
const c = document.getElementById('listContainer');
|
||||
if (filtered.length === 0) {
|
||||
c.innerHTML = '<div style="text-align:center;color:#bbb;font-size:13px;padding:40px 0;">暂无匹配设备</div>';
|
||||
return;
|
||||
}
|
||||
c.innerHTML = filtered.map(dev => {
|
||||
const color = LVL_COLOR[dev.level];
|
||||
return `<div class="device-card" onclick="goDetail('${dev.id}')">
|
||||
<div class="device-card__header">
|
||||
<div>
|
||||
<div class="device-card__name">${dev.name}</div>
|
||||
<div class="device-card__loc">${dev.unit} · ${dev.loc}</div>
|
||||
</div>
|
||||
<span class="device-card__badge" style="background:${color}">${dev.levelLabel}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showSheet(id) {
|
||||
const d = DATA[currentTab];
|
||||
const dev = d.devices.find(x => x.id === id);
|
||||
if (!dev) return;
|
||||
const color = LVL_COLOR[dev.level];
|
||||
const desc = d.levelDesc[dev.level] || '';
|
||||
|
||||
document.getElementById('sheetTitle').textContent = dev.name;
|
||||
document.getElementById('sheetSubtitle').textContent = `${dev.time} 更新`;
|
||||
|
||||
const metricsHtml = dev.metrics.map(m =>
|
||||
`<div class="sheet-info-cell">
|
||||
<div class="sheet-info-cell__label">${m.k}</div>
|
||||
<div class="sheet-info-cell__value">${m.v}<span class="sheet-info-cell__unit">${m.u}</span></div>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
document.getElementById('sheetBody').innerHTML = `
|
||||
<div class="sheet-level-row">
|
||||
<span class="sheet-level-badge" style="background:${color}">${dev.levelLabel}</span>
|
||||
<span class="sheet-level-desc">${desc}</span>
|
||||
</div>
|
||||
<div class="sheet-info-grid">${metricsHtml}</div>
|
||||
<div class="sheet-device-info">
|
||||
<div class="sheet-device-info__row"><span class="sheet-device-info__label">设备编码</span><span class="sheet-device-info__val">${dev.code}</span></div>
|
||||
<div class="sheet-device-info__row"><span class="sheet-device-info__label">所属单位</span><span class="sheet-device-info__val">${dev.unit}</span></div>
|
||||
<div class="sheet-device-info__row"><span class="sheet-device-info__label">设备位置</span><span class="sheet-device-info__val">${dev.loc}</span></div>
|
||||
</div>
|
||||
<button onclick="goDetail('${id}')" style="margin-top:14px;width:100%;height:40px;background:linear-gradient(90deg,#1aad8d,#36cfc9);border:none;border-radius:20px;color:#fff;font-size:14px;font-weight:700;cursor:pointer;">查看详情</button>`;
|
||||
|
||||
document.getElementById('sheetMask').classList.add('active');
|
||||
}
|
||||
|
||||
function goDetail(id) {
|
||||
location.href = 'device-detail.html?tab=' + currentTab + '&id=' + id;
|
||||
}
|
||||
|
||||
function closeSheet(e) {
|
||||
if (e.target === document.getElementById('sheetMask')) closeSheetDirect();
|
||||
}
|
||||
function closeSheetDirect() {
|
||||
document.getElementById('sheetMask').classList.remove('active');
|
||||
}
|
||||
|
||||
function showListView() {
|
||||
document.getElementById('mapView').classList.add('hidden');
|
||||
document.getElementById('listView').classList.add('active');
|
||||
}
|
||||
function showMapView() {
|
||||
document.getElementById('listView').classList.remove('active');
|
||||
document.getElementById('mapView').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 初始化
|
||||
renderLegend();
|
||||
renderMarkers();
|
||||
renderList();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,27 +14,66 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
.nav-bar { height: 44px; background: #fff; display: flex; align-items: center; padding: 0 16px; flex-shrink: 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.nav-back { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 600; color: #1a1a1a; margin-right: 28px; }
|
||||
.scroll-content { flex: 1; overflow-y: auto; padding: 20px 16px; }
|
||||
.scroll-content { flex: 1; overflow-y: auto; padding: 16px 16px 8px; }
|
||||
.scroll-content::-webkit-scrollbar { display: none; }
|
||||
/* 入口卡片 */
|
||||
.entry-grid { display: flex; flex-direction: column; gap: 12px; }
|
||||
.entry-card { background: #fff; border-radius: 16px; padding: 16px; display: flex; align-items: center; gap: 14px; cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; text-decoration: none; }
|
||||
|
||||
/* 分组标签 */
|
||||
.section-label { font-size: 11px; font-weight: 600; color: #aaa; letter-spacing: 0.5px; text-transform: uppercase; margin-bottom: 8px; padding-left: 2px; }
|
||||
|
||||
/* ===== 核心板块大卡片 ===== */
|
||||
.core-card { background: #fff; border-radius: 18px; padding: 16px; margin-bottom: 12px; cursor: pointer; text-decoration: none; display: block; transition: transform 0.15s, box-shadow 0.15s; }
|
||||
.core-card:active { transform: scale(0.985); }
|
||||
.core-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.09); }
|
||||
|
||||
.core-card__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||
.core-card__title-wrap { display: flex; align-items: center; gap: 10px; }
|
||||
.core-card__icon { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
|
||||
.icon--wearable { background: linear-gradient(135deg, #e6fffb 0%, #b5f5ec 100%); }
|
||||
.icon--weight { background: linear-gradient(135deg, #f0f5ff 0%, #d6e4ff 100%); }
|
||||
.core-card__name { font-size: 15px; font-weight: 700; color: #1a1a1a; }
|
||||
.core-card__time { font-size: 11px; color: #bbb; margin-top: 1px; }
|
||||
.core-card__arrow { width: 16px; height: 16px; display: flex; align-items: center; }
|
||||
|
||||
/* 指标网格(穿戴:2×2) */
|
||||
.metrics-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.metric-cell { background: #f8f9fb; border-radius: 12px; padding: 10px 12px; }
|
||||
.metric-cell__label { font-size: 11px; color: #999; margin-bottom: 4px; display: flex; align-items: center; gap: 4px; }
|
||||
.metric-cell__dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot--steps { background: #13c2c2; }
|
||||
.dot--heart { background: #ff4d4f; }
|
||||
.dot--oxygen { background: #1890ff; }
|
||||
.dot--temp { background: #fa8c16; }
|
||||
.dot--weight { background: #597ef7; }
|
||||
.dot--bmi { background: #36cfc9; }
|
||||
.dot--fat { background: #ff85c2; }
|
||||
.metric-cell__value { font-size: 22px; font-weight: 700; color: #1a1a1a; line-height: 1; }
|
||||
.metric-cell__unit { font-size: 11px; color: #aaa; margin-top: 2px; }
|
||||
|
||||
/* 指标行(体重:3列) */
|
||||
.metrics-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
|
||||
|
||||
/* 状态徽章 */
|
||||
.badge { display: inline-flex; align-items: center; font-size: 11px; padding: 2px 8px; border-radius: 20px; font-weight: 600; }
|
||||
.badge--good { color: #52c41a; background: #f6ffed; border: 1px solid #b7eb8f; }
|
||||
.badge--normal { color: #1890ff; background: #e6f7ff; border: 1px solid #91d5ff; }
|
||||
|
||||
/* ===== 次级入口卡片 ===== */
|
||||
.section-cards { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
|
||||
.entry-card { background: #fff; border-radius: 16px; padding: 14px 16px; display: flex; align-items: center; gap: 14px; cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; text-decoration: none; }
|
||||
.entry-card:active { transform: scale(0.98); }
|
||||
.entry-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
|
||||
.entry-icon { width: 48px; height: 48px; border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 24px; flex-shrink: 0; }
|
||||
.entry-icon--wearable { background: linear-gradient(135deg, #e6fffb 0%, #b5f5ec 100%); }
|
||||
.entry-icon--weight { background: linear-gradient(135deg, #f0f5ff 0%, #d6e4ff 100%); }
|
||||
.entry-icon { width: 44px; height: 44px; border-radius: 13px; display: flex; align-items: center; justify-content: center; font-size: 22px; flex-shrink: 0; }
|
||||
.entry-icon--environment { background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%); }
|
||||
.entry-icon--screening { background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%); }
|
||||
.entry-icon--screening { background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%); }
|
||||
.entry-info { flex: 1; min-width: 0; }
|
||||
.entry-name { font-size: 15px; font-weight: 600; color: #1a1a1a; display: flex; align-items: center; justify-content: space-between; }
|
||||
.entry-time { font-size: 11px; color: #bbb; font-weight: 400; }
|
||||
.entry-data { display: flex; gap: 12px; align-items: baseline; margin-top: 4px; }
|
||||
.entry-data__value { font-size: 13px; color: #666; }
|
||||
.entry-data__value strong { font-size: 18px; font-weight: 700; color: #1a1a1a; }
|
||||
.entry-status { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
|
||||
.entry-status--good { color: #52c41a; background: #f6ffed; }
|
||||
.entry-name { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 5px; }
|
||||
.entry-metrics { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.entry-metric { font-size: 12px; color: #666; display: flex; align-items: center; gap: 4px; }
|
||||
.entry-metric strong { color: #1a1a1a; font-weight: 700; }
|
||||
.entry-metric .badge { font-size: 10px; padding: 1px 6px; }
|
||||
.entry-arrow { width: 16px; height: 16px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.entry-time-tag { font-size: 10px; color: #bbb; margin-top: 3px; }
|
||||
|
||||
/* 底部安全区 */
|
||||
.bottom-safe { height: 20px; flex-shrink: 0; }
|
||||
</style>
|
||||
@@ -55,57 +94,113 @@ body { background: #f0f2f5; display: flex; justify-content: center; align-items:
|
||||
</div>
|
||||
<div class="nav-title">健康监测</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
<div class="entry-grid">
|
||||
<!-- 穿戴监测 -->
|
||||
<a class="entry-card" href="wearable/index.html">
|
||||
<div class="entry-icon entry-icon--wearable">⌚</div>
|
||||
<div class="entry-info">
|
||||
<div class="entry-name">穿戴监测<span class="entry-time">05-12 09:38</span></div>
|
||||
<div class="entry-data">
|
||||
<span class="entry-data__value">步数 <strong>6,832</strong></span>
|
||||
<span class="entry-data__value">心率 <strong>72</strong>bpm</span>
|
||||
|
||||
|
||||
<!-- 穿戴监测 大卡片 -->
|
||||
<a class="core-card" href="wearable/index.html">
|
||||
<div class="core-card__header">
|
||||
<div class="core-card__title-wrap">
|
||||
<div class="core-card__icon icon--wearable">⌚</div>
|
||||
<div>
|
||||
<div class="core-card__name">穿戴监测</div>
|
||||
<div class="core-card__time">05-25 09:38 同步</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry-arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
</a>
|
||||
<!-- 体重监测 -->
|
||||
<a class="entry-card" href="weight/index.html">
|
||||
<div class="entry-icon entry-icon--weight">⚖️</div>
|
||||
<div class="entry-info">
|
||||
<div class="entry-name">体重监测<span class="entry-time">05-11 07:15</span></div>
|
||||
<div class="entry-data">
|
||||
<span class="entry-data__value"><strong>68.5</strong>kg</span>
|
||||
<span class="entry-data__value">BMI <strong>22.3</strong></span>
|
||||
<div class="core-card__arrow">
|
||||
<svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-cell">
|
||||
<div class="metric-cell__label"><span class="metric-cell__dot dot--steps"></span>步数</div>
|
||||
<div class="metric-cell__value">6,832</div>
|
||||
<div class="metric-cell__unit">步 </div>
|
||||
</div>
|
||||
<div class="metric-cell">
|
||||
<div class="metric-cell__label"><span class="metric-cell__dot dot--heart"></span>心率</div>
|
||||
<div class="metric-cell__value">72</div>
|
||||
<div class="metric-cell__unit">bpm <span class="badge badge--good">正常</span></div>
|
||||
</div>
|
||||
<div class="metric-cell">
|
||||
<div class="metric-cell__label"><span class="metric-cell__dot dot--oxygen"></span>血氧</div>
|
||||
<div class="metric-cell__value">98</div>
|
||||
<div class="metric-cell__unit">% <span class="badge badge--good">正常</span></div>
|
||||
</div>
|
||||
<div class="metric-cell">
|
||||
<div class="metric-cell__label"><span class="metric-cell__dot dot--temp"></span>体温</div>
|
||||
<div class="metric-cell__value">36.5</div>
|
||||
<div class="metric-cell__unit">°C <span class="badge badge--good">正常</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- 体重监测 大卡片 -->
|
||||
<a class="core-card" href="weight/index.html">
|
||||
<div class="core-card__header">
|
||||
<div class="core-card__title-wrap">
|
||||
<div class="core-card__icon icon--weight">⚖️</div>
|
||||
<div>
|
||||
<div class="core-card__name">体重监测</div>
|
||||
<div class="core-card__time">05-24 07:15 测量</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry-arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
</a>
|
||||
<div class="core-card__arrow">
|
||||
<svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metrics-row">
|
||||
<div class="metric-cell">
|
||||
<div class="metric-cell__label"><span class="metric-cell__dot dot--weight"></span>体重</div>
|
||||
<div class="metric-cell__value">68.5</div>
|
||||
<div class="metric-cell__unit">kg</div>
|
||||
</div>
|
||||
<div class="metric-cell">
|
||||
<div class="metric-cell__label"><span class="metric-cell__dot dot--bmi"></span>BMI</div>
|
||||
<div class="metric-cell__value">22.3</div>
|
||||
<div class="metric-cell__unit"><span class="badge badge--good">正常</span></div>
|
||||
</div>
|
||||
<div class="metric-cell">
|
||||
<div class="metric-cell__label"><span class="metric-cell__dot dot--fat"></span>体脂率</div>
|
||||
<div class="metric-cell__value">18.2</div>
|
||||
<div class="metric-cell__unit">% <span class="badge badge--good">正常</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
<div class="section-cards">
|
||||
|
||||
<!-- 环境监测 -->
|
||||
<a class="entry-card" href="environment/index.html">
|
||||
<div class="entry-icon entry-icon--environment">🌿</div>
|
||||
<div class="entry-info">
|
||||
<div class="entry-name">环境监测<span class="entry-time">05-12 09:30</span></div>
|
||||
<div class="entry-data">
|
||||
<span class="entry-data__value">AQI <strong>45</strong></span>
|
||||
<span class="entry-status entry-status--good">优</span>
|
||||
<span class="entry-data__value"><strong>24</strong>°C</span>
|
||||
<div class="entry-name">环境监测</div>
|
||||
<div class="entry-metrics">
|
||||
<span class="entry-metric">空气质量 <strong>AQI 45</strong> <span class="badge badge--good">优</span></span>
|
||||
<span class="entry-metric">水质 <span class="badge badge--good">达标</span></span>
|
||||
<span class="entry-metric">过敏 <span class="badge badge--normal">低风险</span></span>
|
||||
</div>
|
||||
<div class="entry-time-tag">05-25 09:30 更新</div>
|
||||
</div>
|
||||
<div class="entry-arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
</a>
|
||||
|
||||
<!-- 健康排查 -->
|
||||
<a class="entry-card" href="health-screening/index.html">
|
||||
<div class="entry-icon entry-icon--screening">🔍</div>
|
||||
<div class="entry-info">
|
||||
<div class="entry-name">健康排查<span class="entry-time">05-12 08:00</span></div>
|
||||
<div class="entry-data">
|
||||
<span class="entry-data__value">积分 <strong>1,280</strong></span>
|
||||
<span class="entry-data__value">连续签到 <strong>7</strong>天</span>
|
||||
<div class="entry-name">健康排查</div>
|
||||
<div class="entry-metrics">
|
||||
<span class="entry-metric">积分 <strong>1,280</strong></span>
|
||||
<span class="entry-metric">连续签到 <strong>7</strong> 天</span>
|
||||
</div>
|
||||
<div class="entry-time-tag">05-25 08:00 更新</div>
|
||||
</div>
|
||||
<div class="entry-arrow"><svg width="7" height="12" fill="none"><path d="M1 1l5 5-5 5" stroke="#ccc" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-safe"></div>
|
||||
|
||||
Reference in New Issue
Block a user