index.vue 29 KB

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