index.vue 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075
  1. <template>
  2. <view class="container">
  3. <!-- 搜索区域 -->
  4. <view class="search-section">
  5. <view class="search-box" :class="{ 'search-focus': isSearchFocused }">
  6. <view class="search-icon">
  7. <!-- <text class="iconfont icon-search"></text> -->
  8. <image src="@/static/icons/search.png" style="width: 40rpx; height: 40rpx; padding-right: 10rpx;"
  9. mode="widthFix" />
  10. </view>
  11. <input
  12. class="search-input"
  13. type="text"
  14. v-model="searchKey"
  15. placeholder="搜索设备名称 / 编号"
  16. confirm-type="search"
  17. @confirm="onSearch"
  18. @focus="isSearchFocused = true"
  19. @blur="isSearchFocused = false"
  20. />
  21. <view class="clear-icon" v-if="searchKey" @click="clearSearch">
  22. <text class="iconfont icon-close"></text>
  23. </view>
  24. </view>
  25. </view>
  26. <!-- 状态筛选区域 -->
  27. <view class="filter-section">
  28. <view
  29. class="filter-item"
  30. :class="{ active: currentStatus === -1 }"
  31. @click="selectStatus(-1)"
  32. >
  33. 全部
  34. </view>
  35. <view
  36. class="filter-item"
  37. :class="{ active: currentStatus === 1 }"
  38. @click="selectStatus(1)"
  39. >
  40. <view class="filter-dot online-dot"></view>
  41. 在线 ({{ onlineDevices }})
  42. </view>
  43. <view
  44. class="filter-item"
  45. :class="{ active: currentStatus === 0 }"
  46. @click="selectStatus(0)"
  47. >
  48. <view class="filter-dot offline-dot"></view>
  49. 离线 ({{ offlineDevices }})
  50. </view>
  51. <!-- <view
  52. class="filter-item"
  53. :class="{ active: currentStatus === 2 }"
  54. @click="selectStatus(2)"
  55. >
  56. <view class="filter-dot fault-dot"></view>
  57. 故障 ({{ getStatusCount(2) }})
  58. </view> -->
  59. </view>
  60. <!-- 设备列表区域 -->
  61. <scroll-view
  62. scroll-y
  63. class="device-list"
  64. @scrolltolower="onReachBottom"
  65. :scroll-with-animation="true"
  66. :enable-back-to-top="true"
  67. :refresher-enabled="true"
  68. :refresher-threshold="80"
  69. :refresher-triggered="isRefreshing"
  70. @refresherrefresh="handleRefresh"
  71. @refresherrestore="isRefreshing = false"
  72. >
  73. <!-- 无数据提示 -->
  74. <view v-if="deviceList.length === 0 && !loading" class="empty-tips">
  75. <view class="empty-icon">
  76. <text class="iconfont icon-empty"></text>
  77. </view>
  78. <text class="empty-text">{{ searchKey ? '未找到匹配的设备' : '暂无设备' }}</text>
  79. <view v-if="searchKey" class="empty-action" @click="clearSearch">
  80. <text>清除搜索条件</text>
  81. </view>
  82. </view>
  83. <!-- 首次加载中状态 -->
  84. <view v-if="loading && deviceList.length === 0" class="loading-container">
  85. <view class="loading-spinner"></view>
  86. <text class="loading-text">加载中...</text>
  87. </view>
  88. <!-- 设备列表 -->
  89. <view
  90. v-for="(item, index) in deviceList"
  91. :key="index"
  92. class="device-card"
  93. :class="{
  94. 'has-alert': item.hasAlert,
  95. 'is-offline': item.status === 0
  96. }"
  97. hover-class="device-card-hover"
  98. hover-stay-time="70"
  99. @click="navigateToDeviceDetail(item)"
  100. >
  101. <!-- 设备基本信息 -->
  102. <view class="device-info">
  103. <view class="device-icon-wrapper">
  104. <!-- 告警标记 -->
  105. <view v-if="item.hasAlert" class="alarm-badge">
  106. 1
  107. </view>
  108. <view class="device-icon-container" :class="{'offline-icon': item.status === 0}">
  109. <image :src="getDeviceTypeIcon(item.deviceTypeId)" mode="aspectFit" class="device-icon"></image>
  110. </view>
  111. </view>
  112. <view class="device-meta">
  113. <view class="device-name-row">
  114. <text class="device-name" :class="{'offline-text': item.status === 0}">{{ item.deviceName }}</text>
  115. <view
  116. class="status-tag"
  117. :class="{
  118. 'status-online': item.status === 1,
  119. 'status-offline': item.status === 0,
  120. 'status-fault': item.status === 2,
  121. 'status-maintain': item.status === 3
  122. }"
  123. >
  124. <text class="status-dot" :class="{
  125. 'offline-dot': item.status === 0,
  126. 'fault-dot': item.status === 2,
  127. 'maintain-dot': item.status === 3
  128. }"></text>
  129. {{ getStatusText(item.status) }}
  130. </view>
  131. </view>
  132. <view class="device-id">
  133. <text class="id-label">设备编号:</text>
  134. <text class="id-value">{{ item.deviceId }}</text>
  135. </view>
  136. <view class="device-location">
  137. <text class="location-label">安装位置:</text>
  138. <text class="location-value">{{ item.fieldName || '未指定位置' }}</text>
  139. </view>
  140. </view>
  141. </view>
  142. <!-- 底部信息栏 -->
  143. <view class="device-footer">
  144. <text class="update-time">最后活跃: {{ formatDate(item.lastActiveTime) }}</text>
  145. </view>
  146. </view>
  147. <!-- 加载更多提示 -->
  148. <view v-if="deviceList.length > 0" class="load-more">
  149. <view class="load-more-content" v-if="loadMoreStatus === 'loading'">
  150. <view class="loading-icon"></view>
  151. <text>正在加载...</text>
  152. </view>
  153. <view class="load-more-content" v-if="loadMoreStatus === 'noMore'">
  154. <text>没有更多了</text>
  155. </view>
  156. <view class="load-more-content" v-if="loadMoreStatus === 'more'" @click="onReachBottom">
  157. <text>点击加载更多</text>
  158. </view>
  159. </view>
  160. </scroll-view>
  161. </view>
  162. </template>
  163. <script>
  164. import { fetchDevicesByType } from "@/api/services/device.js";
  165. import storage from "@/utils/storage.js";
  166. export default {
  167. data() {
  168. return {
  169. deviceType: '', // monitor, sensor, control, irrigation, tractor
  170. deviceTypeName: '',
  171. deviceList: [],
  172. searchKey: '',
  173. isSearchFocused: false,
  174. currentStatus: -1, // -1代表全部
  175. pageNum: 1,
  176. pageSize: 10,
  177. total: 0,
  178. loading: false,
  179. isRefreshing: false,
  180. loadMoreStatus: 'more', // 加载更多状态: more-加载更多 loading-加载中 noMore-没有更多了
  181. deviceTypeMap: {
  182. 'monitor': { name: '监控设备', icon: '/static/icons/camera.png', class: 'type-monitor' },
  183. 'sensor': { name: '采集设备', icon: '/static/icons/sensor.png', class: 'type-sensor' },
  184. 'control': { name: '控制设备', icon: '/static/icons/control.png', class: 'type-control' },
  185. 'irrigation': { name: '灌溉设备', icon: '/static/icons/water.png', class: 'type-irrigation' },
  186. 'tractor': { name: '农机设备', icon: '/static/icons/tractor.png', class: 'type-tractor' }
  187. },
  188. currentFieldId: null,
  189. onlineDevices:0,
  190. offlineDevices:0
  191. };
  192. },
  193. computed: {
  194. // 设备类型对应的样式类
  195. deviceTypeClass() {
  196. return this.deviceTypeMap[this.deviceType]?.class || '';
  197. }
  198. },
  199. onLoad(options) {
  200. // 获取传递的设备类型
  201. console.log("options类型:", options);
  202. const { type, typeOnline, typeOffline } = options;
  203. // 处理传递过来的统计数量
  204. this.offlineDevices = typeOffline
  205. this.onlineDevices = typeOnline
  206. if (type && this.deviceTypeMap[type]) {
  207. this.deviceType = type;
  208. this.deviceTypeName = this.deviceTypeMap[type].name;
  209. }
  210. // 获取当前地块ID
  211. this.initFieldInfo();
  212. // 加载设备列表
  213. this.loadDeviceList();
  214. },
  215. methods: {
  216. // 初始化地块信息
  217. initFieldInfo() {
  218. const currentPlots = JSON.parse(storage.getPlots() || '{}');
  219. if (currentPlots) {
  220. this.currentFieldId = currentPlots.id;
  221. }
  222. },
  223. // 获取特定状态的设备数量
  224. getStatusCount(status) {
  225. return this.deviceList.filter(device => device.status === status).length;
  226. },
  227. // 加载设备列表
  228. loadDeviceList(reset = true) {
  229. if (this.loading) return;
  230. if (reset) {
  231. this.pageNum = 1;
  232. this.deviceList = [];
  233. }
  234. this.loading = true;
  235. this.loadMoreStatus = 'loading';
  236. // 构建查询参数
  237. const params = {
  238. pageNum: this.pageNum,
  239. pageSize: this.pageSize,
  240. deviceQueryParams: this.searchKey || undefined,
  241. deviceType: this.deviceType || undefined,
  242. fieldId: this.currentFieldId || undefined
  243. };
  244. // 如果状态不是全部,添加状态筛选
  245. if (this.currentStatus !== -1) {
  246. params.status = this.currentStatus;
  247. }
  248. // 调用API获取设备列表
  249. fetchDevicesByType(params)
  250. .then(res => {
  251. console.log("res", res);
  252. if (res.data.code === 200 && res.data.rows) {
  253. const { rows, total } = res.data;
  254. // 更新设备列表
  255. if (reset) {
  256. this.deviceList = rows;
  257. } else {
  258. this.deviceList = [...this.deviceList, ...rows];
  259. }
  260. this.total = total;
  261. // 为农技增加模拟数据 后续可删除
  262. if(this.deviceType === 'tractor' ){
  263. const newDevices = this.generateMockDevices();
  264. console.log("newDevices",newDevices);
  265. this.deviceList = [...this.deviceList, ...newDevices];
  266. this.total = this.deviceList.length
  267. }
  268. // 标记有告警的设备
  269. this.deviceList.forEach(device => {
  270. // 这里可以根据实际情况设置hasAlert属性
  271. device.hasAlert = false; // 示例,实际应该根据后台数据判断
  272. });
  273. // 更新加载更多状态
  274. if (this.deviceList.length >= total) {
  275. this.loadMoreStatus = 'noMore';
  276. } else {
  277. this.loadMoreStatus = 'more';
  278. }
  279. } else {
  280. this.handleApiError(res);
  281. }
  282. })
  283. .catch(error => {
  284. console.error('获取设备列表失败', error);
  285. uni.showToast({
  286. title: '获取设备列表失败',
  287. icon: 'none'
  288. });
  289. this.loadMoreStatus = 'more';
  290. })
  291. .finally(() => {
  292. this.loading = false;
  293. this.isRefreshing = false;
  294. uni.hideLoading();
  295. });
  296. },
  297. // 处理API错误
  298. handleApiError(res) {
  299. console.error('API错误', res);
  300. uni.showToast({
  301. title: res.msg || '获取数据失败',
  302. icon: 'none'
  303. });
  304. },
  305. // 搜索
  306. onSearch() {
  307. this.loadDeviceList();
  308. },
  309. // 清除搜索
  310. clearSearch() {
  311. this.searchKey = '';
  312. this.loadDeviceList();
  313. },
  314. // 选择状态
  315. selectStatus(value) {
  316. this.currentStatus = value;
  317. this.loadDeviceList();
  318. },
  319. // 处理下拉刷新
  320. handleRefresh() {
  321. this.isRefreshing = true;
  322. this.loadDeviceList();
  323. },
  324. // 跳转到设备详情页
  325. navigateToDeviceDetail(device) {
  326. console.log("device",device);
  327. // 根据设备类型跳转到不同的详情页
  328. let url = '';
  329. // 先发送事件
  330. uni.$emit('passDeviceData', {
  331. deviceId: device.deviceId,
  332. deviceTypeId: device.deviceTypeId,
  333. fieldName: device.fieldName,
  334. deviceName: device.deviceName
  335. });
  336. if (device.deviceTypeId === '2') {
  337. url = `/pages/device/device-list/detail-camera?id=${device.id}`;
  338. } else if (device.deviceTypeId === '1' || device.deviceTypeId === '4') {
  339. // 采集设备跳转到采集设备详情页,同时传递设备编码,便于判断设备子类型
  340. // url = `/pages/device/device-list/detail-collector?id=${device.deviceId}&deviceTypeId=${device.deviceTypeId}&fieldName=${device.fieldName}&deviceName=${device.deviceName}`;
  341. url = `/pages/device/device-list/detail-collector`;
  342. } else if (device.type === 'tractor') {
  343. // 农机设备跳转到农机设备详情页
  344. url = `/pages/device/device-list/detail-machine?id=${device.id}&deviceId=${device.code}`;
  345. } else {
  346. // 其他类型设备暂时使用通用详情页
  347. url = `/pages/device/device-detail/index?id=${device.id}&type=${device.type}`;
  348. }
  349. // 先跳转
  350. uni.navigateTo({
  351. url: url,
  352. success: () => {
  353. // 跳转成功后再发送事件,延迟一点确保页面onLoad注册完成
  354. setTimeout(() => {
  355. uni.$emit('passDeviceData', {
  356. deviceId: device.deviceId,
  357. deviceTypeId: device.deviceTypeId,
  358. fieldName: device.fieldName,
  359. deviceName: device.deviceName,
  360. status:device.status
  361. });
  362. }, 100); // 100ms 通常足够,必要时可加到 200
  363. },
  364. fail: (e)=>{
  365. console.log(e);
  366. }
  367. });
  368. },
  369. // 获取设备类型图标
  370. getDeviceTypeIcon(typeId) {
  371. // 根据后端设备类型ID获取对应前端类型的图标
  372. const typeMapping = {
  373. '1': 'sensor', // 传感器
  374. '2': 'monitor', // 摄像头
  375. '3': 'control', // 控制器
  376. '4': 'irrigation', // 气象设备/灌溉设备
  377. '5': 'tractor' // 农机设备
  378. };
  379. const frontendType = typeMapping[typeId] || this.deviceType;
  380. return this.deviceTypeMap[frontendType]?.icon || '/static/icons/device.png';
  381. },
  382. // 获取设备类型名称
  383. getDeviceTypeName(typeId) {
  384. const typeNames = {
  385. '1': '采集设备',
  386. '2': '监控设备',
  387. '3': '控制设备',
  388. '4': '灌溉设备',
  389. '5': '农机设备'
  390. };
  391. return typeNames[typeId] || '未知类型';
  392. },
  393. // 获取状态文本
  394. getStatusText(status) {
  395. const statusMap = {
  396. 0: '离线',
  397. 1: '在线',
  398. 2: '故障',
  399. 3: '维护中'
  400. };
  401. return statusMap[status] || '未知状态';
  402. },
  403. // 格式化日期
  404. formatDate(dateStr) {
  405. if (!dateStr) return '未知';
  406. // 解析为 Date
  407. const parsedStr = dateStr.replace(' ', 'T');
  408. const date = new Date(parsedStr);
  409. if (isNaN(date)) return '无效时间';
  410. const now = new Date();
  411. // 计算差时长(分钟)
  412. const diff = Math.floor((now - date) / 1000 / 60);
  413. if (diff < 1) return '刚刚更新';
  414. if (diff < 5) return '1分钟前更新';
  415. if (diff < 10) return '5分钟前更新';
  416. if (diff < 60) return `${diff}分钟前更新`;
  417. if (diff < 120) return '1小时前更新';
  418. if (diff < 24 * 60) return `${Math.floor(diff / 60)}小时前更新`;
  419. if (diff < 7 * 24 * 60) return `${Math.floor(diff / (60 * 24))}天前更新`;
  420. return parsedStr.split('T')[0] + ' 更新';
  421. },
  422. // 生成模拟设备数据
  423. generateMockDevices() {
  424. const devices = [];
  425. const locations = ['东区A1地块', '西区B2地块', '南区C3地块', '北区D4地块'];
  426. const updateTimes = ['刚刚更新', '1分钟前更新', '5分钟前更新', '10分钟前更新', '1小时前更新'];
  427. // 根据当前页码和限制数量生成对应数量的模拟数据
  428. const startIndex = (this.pageNum - 1) * this.pageSize;
  429. for (let i = 0; i < this.pageSize; i++) {
  430. const index = startIndex + i;
  431. // 如果已经生成了30条数据,则停止
  432. if (index >= 30) break;
  433. // 对于采集设备类型,生成随机的气象或土壤设备
  434. let deviceType = this.deviceType;
  435. let deviceCode = `DEV${String(index + 1001).padStart(4, '0')}`;
  436. // 如果是采集设备,随机生成气象站或土壤墒情设备
  437. if (this.deviceType === 'sensor') {
  438. // 随机分配采集设备子类型:气象站或土壤墒情
  439. const sensorSubType = Math.random() > 0.5 ? 'weather' : 'soil';
  440. deviceCode = sensorSubType === 'weather' ? `W${deviceCode}` : `S${deviceCode}`;
  441. }
  442. devices.push({
  443. id: `device-${index + 1}`,
  444. deviceName: `${this.getDeviceTypeName(this.deviceType)}-${index + 1}`,
  445. deviceId: deviceCode,
  446. type: deviceType,
  447. status: Math.random() > 0.3 ? 'online' : 'offline', // 70% 概率在线
  448. fieldName: locations[Math.floor(Math.random() * locations.length)],
  449. updateTime: updateTimes[Math.floor(Math.random() * updateTimes.length)],
  450. alarmCount: Math.random() > 0.7 ? Math.floor(Math.random() * 3) + 1 : 0 // 30% 概率有告警
  451. });
  452. }
  453. return devices;
  454. },
  455. /* formatDate(dateStr) {
  456. if (!dateStr) return '未知';
  457. const date = new Date(dateStr);
  458. const now = new Date();
  459. // 今天的日期
  460. if (date.toDateString() === now.toDateString()) {
  461. return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  462. }
  463. // 一周内
  464. const days = ['日', '一', '二', '三', '四', '五', '六'];
  465. const dayDiff = Math.floor((now - date) / (24 * 60 * 60 * 1000));
  466. if (dayDiff < 7) {
  467. return `周${days[date.getDay()]} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
  468. }
  469. // 超过一周
  470. return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
  471. }, */
  472. // 页面上拉触底事件
  473. onReachBottom() {
  474. if (this.loadMoreStatus === 'more') {
  475. this.pageNum++;
  476. this.loadDeviceList(false);
  477. }
  478. }
  479. },
  480. // 下拉刷新
  481. onPullDownRefresh() {
  482. this.loadDeviceList();
  483. setTimeout(() => {
  484. uni.stopPullDownRefresh();
  485. }, 1000);
  486. },
  487. // 页面显示
  488. onShow() {
  489. uni.setNavigationBarTitle({
  490. title: this.deviceTypeName || '设备列表'
  491. });
  492. }
  493. };
  494. </script>
  495. <style scoped>
  496. /* 图标字体 */
  497. @font-face {
  498. font-family: "iconfont";
  499. src: url('data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAOMAAsAAAAACFAAAAM+AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDMgqDGIJmATYCJAMUCwwABCAFhGcHRhvqBsgOJaDgwABgBNFYPPy3H/3OSB4kWdI1Pb2QBw8gSpZIsxBFsTCNTIm//9dv/7FzF1E1S9ysIZWDPNdPJdSYbfLy/+fpTTMwKn5ZZ3OImpRUTZ8FHAcYFrLCBqyAHZD3CP9I09oXUgNcDSEAS8oGR507tO6KhRQTNQGQyeNGD8PCkVATUkFYsMgGcuqQ5YiFWruCPAVYnn9f/KGFBQxrghRq0KjzAXS5TP3SHp3ZmXUyEIfTAYDvDEiQARSQiaJjIjKaZiDJ+hpQYnY1i8tK1ZfWl83/fxYLm8TS//EADBBU6C9ghIBwJXxprxqwhC8d6x9AvAB4BcARgGWgRlWY71Cn5I5XvJ5Ot5sXy91Ud9Oj1nXnfbK9vK09bVsrGq2rrB1sxMcbxZmXntbdI8u6cRGhGy9r92Hj7hCjx1G6Dd9G9N6sbSvwVZiw+/hC0D7WFi5tbcltVyPOdqm41HkQr9T0bKFa2zrLLVeKJufZy7vn7aXtY+3XMvl65fJ7wnHT7ZJWozcvrHC5XNnTGy5/0NvX1xdSu8W9Ml0ePXEqZ3Jm0r6SLSdPbJnc27Mm05YsrLjfI8Ypvb5dL5pAj9m5c3Tzw+UvtK/SzO/Vv+Dt/9Lfq3+Xr30Hf9pz0JsnGgsPDBWHv+v12l5zGsE7Pz9A2d0W9WJ2dUZ/a/PkxQW58o/asvHDfyvJEw39G/5zf2p9OmZFYYGKoioVKPzPpwQA0+d2xv7C/aL/Dx/m7QfkjDLZL8AH4F3p+8PBrGIuqFLmM3uqfI6+YxAFCRZWAQ9LM8Q3JuRm2/bCgWEDCgzzIFgrEYk0AxgciQCDtQxgSRr76yYgfkIiHoB0FxGEEKfAEOQMJCHukYj0CQyjPsMQ0oJgyQD6hI6DL3FILJJLiqDqQs9kbYnLnTRuoA+5j7HsWmUJG2QdvdPrY51xR0WpwQZZIJvsGJ8OWYgVpomO28rIDCGTHHGJpqFe7xSPlUXs04FBiQV1FxSCqhboGVlrEldXpOGP94G4Hwydnaj6DjJBj1dvVtZizE8WlMYD9rHWaVsLsmF6QhZEmMDsIk1wOZWLgSHSSTkEJzGNQvvLc3HLFVXz9euBBWkm+YbLZKGH3uISrbIXRuNaAA==');
  500. }
  501. .iconfont {
  502. font-family: "iconfont" !important;
  503. font-size: 24rpx;
  504. font-style: normal;
  505. -webkit-font-smoothing: antialiased;
  506. -moz-osx-font-smoothing: grayscale;
  507. }
  508. .icon-search:before {
  509. content: "\e6e1";
  510. font-size: 32rpx;
  511. }
  512. .icon-close:before {
  513. content: "\e6a7";
  514. font-size: 28rpx;
  515. }
  516. .icon-right:before {
  517. content: "\e6a3";
  518. font-size: 28rpx;
  519. }
  520. .icon-empty:before {
  521. content: "\e6a9";
  522. font-size: 80rpx;
  523. }
  524. /* 容器样式 */
  525. .container {
  526. display: flex;
  527. flex-direction: column;
  528. min-height: 100vh;
  529. background-color: #F9FCFA;
  530. padding-bottom: 20rpx;
  531. }
  532. /* 搜索区域 */
  533. .search-section {
  534. padding: 20rpx 30rpx;
  535. background-color: #FFFFFF;
  536. margin-bottom: 2rpx;
  537. width: 100%;
  538. box-sizing: border-box;
  539. position: relative;
  540. z-index: 5;
  541. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
  542. }
  543. .search-box {
  544. display: flex;
  545. align-items: center;
  546. background-color: #F7F7F7;
  547. height: 80rpx;
  548. border-radius: 40rpx;
  549. padding: 0 24rpx;
  550. width: 100%;
  551. box-sizing: border-box;
  552. border: 2rpx solid transparent;
  553. transition: all 0.3s ease;
  554. }
  555. .search-box.search-focus {
  556. border-color: #4CAF50;
  557. background-color: #FFFFFF;
  558. box-shadow: 0 0 10rpx rgba(76, 175, 80, 0.1);
  559. }
  560. .search-icon {
  561. color: #4CAF50;
  562. width: 60rpx;
  563. display: flex;
  564. justify-content: center;
  565. }
  566. .search-input {
  567. flex: 1;
  568. height: 80rpx;
  569. font-size: 28rpx;
  570. color: #333333;
  571. }
  572. .clear-icon {
  573. width: 60rpx;
  574. display: flex;
  575. justify-content: center;
  576. color: #999;
  577. }
  578. /* 状态筛选区域 */
  579. .filter-section {
  580. display: flex;
  581. padding: 24rpx 24rpx;
  582. background-color: #FFFFFF;
  583. margin-bottom: 20rpx;
  584. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
  585. position: relative;
  586. z-index: 4;
  587. flex-wrap: wrap;
  588. }
  589. .filter-item {
  590. display: flex;
  591. align-items: center;
  592. font-size: 28rpx;
  593. color: #666666;
  594. margin-right: 30rpx;
  595. padding: 12rpx 20rpx;
  596. border-radius: 30rpx;
  597. transition: all 0.2s ease;
  598. position: relative;
  599. margin-bottom: 10rpx;
  600. }
  601. .filter-item.active {
  602. background-color: #F0F9F0;
  603. color: #4CAF50;
  604. font-weight: 500;
  605. }
  606. .filter-dot {
  607. width: 14rpx;
  608. height: 14rpx;
  609. border-radius: 50%;
  610. margin-right: 10rpx;
  611. }
  612. .online-dot {
  613. background-color: #4CAF50;
  614. box-shadow: 0 0 6rpx rgba(76, 175, 80, 0.5);
  615. }
  616. .offline-dot {
  617. background-color: #F56C6C;
  618. box-shadow: 0 0 6rpx rgba(245, 108, 108, 0.5);
  619. }
  620. .fault-dot {
  621. background-color: #FF9800;
  622. box-shadow: 0 0 6rpx rgba(255, 152, 0, 0.5);
  623. }
  624. .maintain-dot {
  625. background-color: #2196F3;
  626. box-shadow: 0 0 6rpx rgba(33, 150, 243, 0.5);
  627. }
  628. /* 设备列表区域 */
  629. .device-list {
  630. flex: 1;
  631. padding: 0 30rpx;
  632. box-sizing: border-box;
  633. width: 100%;
  634. position: relative;
  635. z-index: 3;
  636. height: calc(100vh - 220rpx);
  637. }
  638. /* 空数据提示 */
  639. .empty-tips {
  640. padding: 120rpx 0;
  641. display: flex;
  642. flex-direction: column;
  643. align-items: center;
  644. justify-content: center;
  645. }
  646. .empty-icon {
  647. color: #DDDDDD;
  648. margin-bottom: 20rpx;
  649. }
  650. .empty-text {
  651. font-size: 28rpx;
  652. color: #999999;
  653. margin-bottom: 20rpx;
  654. }
  655. .empty-action {
  656. font-size: 26rpx;
  657. color: #4CAF50;
  658. padding: 12rpx 30rpx;
  659. border-radius: 30rpx;
  660. background-color: rgba(76, 175, 80, 0.1);
  661. }
  662. /* 首次加载中状态 */
  663. .loading-container {
  664. padding: 80rpx 0;
  665. display: flex;
  666. flex-direction: column;
  667. align-items: center;
  668. justify-content: center;
  669. }
  670. .loading-spinner {
  671. width: 60rpx;
  672. height: 60rpx;
  673. border: 4rpx solid #E0E0E0;
  674. border-top: 4rpx solid #4CAF50;
  675. border-radius: 50%;
  676. animation: spin 1s linear infinite;
  677. margin-bottom: 20rpx;
  678. }
  679. .loading-text {
  680. font-size: 28rpx;
  681. color: #999999;
  682. }
  683. /* 设备卡片 */
  684. .device-card {
  685. position: relative;
  686. background-color: #FFFFFF;
  687. border-radius: 24rpx;
  688. padding: 28rpx;
  689. margin-bottom: 24rpx;
  690. box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.05);
  691. transition: all 0.25s ease;
  692. width: 100%;
  693. box-sizing: border-box;
  694. border: 1rpx solid rgba(0, 0, 0, 0.05);
  695. overflow: hidden;
  696. }
  697. .device-card::before {
  698. content: "";
  699. position: absolute;
  700. top: 0;
  701. left: 0;
  702. width: 6rpx;
  703. height: 100%;
  704. background: linear-gradient(to bottom, #66CC6A, #3BB44A);
  705. opacity: 0;
  706. transition: opacity 0.3s ease;
  707. }
  708. .device-card:active::before {
  709. opacity: 1;
  710. }
  711. .device-card-hover {
  712. background-color: #f2fef6;
  713. box-shadow: 0 10rpx 25rpx rgba(59, 180, 74, 0.1);
  714. transform: translateY(-3rpx);
  715. }
  716. .device-card.has-alert {
  717. box-shadow: 0 6rpx 20rpx rgba(245, 108, 108, 0.08);
  718. border: 1rpx solid rgba(245, 108, 108, 0.08);
  719. }
  720. .device-card.has-alert::before {
  721. background: linear-gradient(to bottom, #FF8F8F, #F56C6C);
  722. }
  723. .device-card.is-offline {
  724. opacity: 0.9;
  725. }
  726. .device-card.is-offline.device-card-hover {
  727. background-color: #f5f5f5;
  728. box-shadow: 0 10rpx 25rpx rgba(0, 0, 0, 0.05);
  729. }
  730. /* 告警角标 */
  731. .alarm-badge {
  732. position: absolute;
  733. top: -8rpx;
  734. right: -8rpx;
  735. min-width: 36rpx;
  736. height: 36rpx;
  737. border-radius: 18rpx;
  738. background-color: #F56C6C;
  739. color: #FFFFFF;
  740. font-size: 22rpx;
  741. font-weight: 600;
  742. display: flex;
  743. align-items: center;
  744. justify-content: center;
  745. padding: 0 8rpx;
  746. z-index: 3;
  747. box-shadow: 0 3rpx 8rpx rgba(245, 108, 108, 0.3);
  748. animation: pulse 1.5s infinite;
  749. }
  750. @keyframes pulse {
  751. 0% {
  752. transform: scale(1);
  753. }
  754. 50% {
  755. transform: scale(1.1);
  756. }
  757. 100% {
  758. transform: scale(1);
  759. }
  760. }
  761. /* 设备基本信息 */
  762. .device-info {
  763. display: flex;
  764. margin-bottom: 24rpx;
  765. width: 100%;
  766. }
  767. .device-icon-wrapper {
  768. position: relative;
  769. margin-right: 24rpx;
  770. flex-shrink: 0;
  771. }
  772. .device-icon-container {
  773. width: 96rpx;
  774. height: 96rpx;
  775. border-radius: 50%;
  776. background: linear-gradient(135deg, #66CC6A 0%, #3BB44A 100%);
  777. display: flex;
  778. align-items: center;
  779. justify-content: center;
  780. flex-shrink: 0;
  781. box-shadow: 0 6rpx 16rpx rgba(59, 180, 74, 0.2);
  782. transition: all 0.3s ease;
  783. }
  784. .device-icon-container.offline-icon {
  785. background: linear-gradient(135deg, #AAB2BD 0%, #656D78 100%);
  786. box-shadow: 0 6rpx 16rpx rgba(101, 109, 120, 0.2);
  787. }
  788. .device-icon {
  789. width: 52rpx;
  790. height: 52rpx;
  791. filter: brightness(0) invert(1);
  792. }
  793. .device-meta {
  794. flex: 1;
  795. width: calc(100% - 120rpx);
  796. overflow: hidden;
  797. }
  798. .device-name-row {
  799. display: flex;
  800. justify-content: space-between;
  801. align-items: center;
  802. margin-bottom: 14rpx;
  803. width: 100%;
  804. }
  805. .device-name {
  806. font-size: 34rpx;
  807. font-weight: 600;
  808. color: #333333;
  809. max-width: 65%;
  810. overflow: hidden;
  811. text-overflow: ellipsis;
  812. white-space: nowrap;
  813. transition: color 0.3s ease;
  814. }
  815. .device-name.offline-text {
  816. color: #656D78;
  817. }
  818. .status-tag {
  819. padding: 6rpx 16rpx 6rpx 32rpx;
  820. border-radius: 8rpx;
  821. font-size: 24rpx;
  822. font-weight: 500;
  823. flex-shrink: 0;
  824. border: 1rpx solid;
  825. position: relative;
  826. overflow: hidden;
  827. }
  828. .status-online {
  829. background-color: rgba(76, 175, 80, 0.1);
  830. color: #4CAF50;
  831. border-color: rgba(76, 175, 80, 0.3);
  832. }
  833. .status-offline {
  834. background-color: rgba(245, 108, 108, 0.1);
  835. color: #F56C6C;
  836. border-color: rgba(245, 108, 108, 0.3);
  837. }
  838. .status-fault {
  839. background-color: rgba(255, 152, 0, 0.1);
  840. color: #FF9800;
  841. border-color: rgba(255, 152, 0, 0.3);
  842. }
  843. .status-maintain {
  844. background-color: rgba(33, 150, 243, 0.1);
  845. color: #2196F3;
  846. border-color: rgba(33, 150, 243, 0.3);
  847. }
  848. .status-dot {
  849. position: absolute;
  850. width: 8rpx;
  851. height: 8rpx;
  852. background-color: #4CAF50;
  853. border-radius: 50%;
  854. top: 50%;
  855. left: 16rpx;
  856. transform: translateY(-50%);
  857. box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
  858. animation: blink 1.5s infinite;
  859. display: inline-block;
  860. }
  861. .status-dot.offline-dot {
  862. background-color: #F56C6C;
  863. box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
  864. }
  865. .status-dot.fault-dot {
  866. background-color: #FF9800;
  867. box-shadow: 0 0 4rpx rgba(255, 152, 0, 0.8);
  868. }
  869. .status-dot.maintain-dot {
  870. background-color: #2196F3;
  871. box-shadow: 0 0 4rpx rgba(33, 150, 243, 0.8);
  872. }
  873. @keyframes blink {
  874. 0% {
  875. opacity: 0.4;
  876. }
  877. 50% {
  878. opacity: 1;
  879. }
  880. 100% {
  881. opacity: 0.4;
  882. }
  883. }
  884. .device-id, .device-location {
  885. display: flex;
  886. font-size: 26rpx;
  887. margin-top: 10rpx;
  888. color: #666666;
  889. width: 100%;
  890. overflow: hidden;
  891. }
  892. .id-label, .location-label {
  893. color: #999999;
  894. margin-right: 8rpx;
  895. flex-shrink: 0;
  896. }
  897. .id-value, .location-value {
  898. color: #666666;
  899. overflow: hidden;
  900. text-overflow: ellipsis;
  901. white-space: nowrap;
  902. }
  903. /* 底部信息栏 */
  904. .device-footer {
  905. display: flex;
  906. justify-content: space-between;
  907. align-items: center;
  908. padding-top: 18rpx;
  909. border-top: 1rpx solid #F2F2F2;
  910. }
  911. .update-time {
  912. font-size: 24rpx;
  913. color: #999999;
  914. }
  915. .device-actions {
  916. display: flex;
  917. align-items: center;
  918. color: #CCCCCC;
  919. }
  920. /* 加载更多区域 */
  921. .load-more {
  922. padding: 20rpx 0 40rpx;
  923. }
  924. .load-more-content {
  925. display: flex;
  926. justify-content: center;
  927. align-items: center;
  928. height: 60rpx;
  929. font-size: 24rpx;
  930. color: #999999;
  931. }
  932. .loading-icon {
  933. width: 30rpx;
  934. height: 30rpx;
  935. margin-right: 10rpx;
  936. border: 2rpx solid #E0E0E0;
  937. border-top: 2rpx solid #4CAF50;
  938. border-radius: 50%;
  939. animation: spin 1s linear infinite;
  940. }
  941. @keyframes spin {
  942. 0% { transform: rotate(0deg); }
  943. 100% { transform: rotate(360deg); }
  944. }
  945. </style>