index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  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. </view>
  9. <input
  10. class="search-input"
  11. type="text"
  12. v-model="searchKeyword"
  13. placeholder="搜索设备名称 / 编号"
  14. confirm-type="search"
  15. @confirm="handleSearch"
  16. @focus="isSearchFocused = true"
  17. @blur="isSearchFocused = false"
  18. />
  19. <view class="clear-icon" v-if="searchKeyword" @click="handleClearSearch">
  20. <text class="iconfont icon-close"></text>
  21. </view>
  22. </view>
  23. </view>
  24. <!-- 状态筛选区域 -->
  25. <view class="filter-section">
  26. <view
  27. class="filter-item"
  28. :class="{ active: statusFilter === 'all' }"
  29. @click="setStatusFilter('all')"
  30. >
  31. 全部
  32. </view>
  33. <view
  34. class="filter-item"
  35. :class="{ active: statusFilter === 'online' }"
  36. @click="setStatusFilter('online')"
  37. >
  38. <view class="filter-dot online-dot"></view>
  39. 在线 ({{ onlineCount }})
  40. </view>
  41. <view
  42. class="filter-item"
  43. :class="{ active: statusFilter === 'offline' }"
  44. @click="setStatusFilter('offline')"
  45. >
  46. <view class="filter-dot offline-dot"></view>
  47. 离线 ({{ offlineCount }})
  48. </view>
  49. </view>
  50. <!-- 设备列表区域 -->
  51. <scroll-view
  52. scroll-y
  53. class="device-list"
  54. @scrolltolower="loadMore"
  55. :scroll-with-animation="true"
  56. :enable-back-to-top="true"
  57. :refresher-enabled="true"
  58. :refresher-threshold="80"
  59. :refresher-triggered="isRefreshing"
  60. @refresherrefresh="handleRefresh"
  61. @refresherrestore="isRefreshing = false"
  62. >
  63. <!-- 无数据提示 -->
  64. <view v-if="filteredDeviceList.length === 0 && !isLoading" class="empty-tips">
  65. <view class="empty-icon">
  66. <text class="iconfont icon-empty"></text>
  67. </view>
  68. <text class="empty-text">{{ searchKeyword ? '未找到匹配的设备' : '暂无设备' }}</text>
  69. <view v-if="searchKeyword" class="empty-action" @click="handleClearSearch">
  70. <text>清除搜索条件</text>
  71. </view>
  72. </view>
  73. <!-- 首次加载中状态 -->
  74. <view v-if="isLoading && deviceList.length === 0" class="loading-container">
  75. <view class="loading-spinner"></view>
  76. <text class="loading-text">加载中...</text>
  77. </view>
  78. <!-- 设备列表 -->
  79. <view
  80. v-for="(item, index) in filteredDeviceList"
  81. :key="index"
  82. class="device-card"
  83. :class="{
  84. 'has-alert': item.alarmCount > 0,
  85. 'is-offline': item.status === 'offline'
  86. }"
  87. hover-class="device-card-hover"
  88. hover-stay-time="70"
  89. @click="navigateToDetail(item)"
  90. >
  91. <!-- 设备基本信息 -->
  92. <view class="device-info">
  93. <view class="device-icon-wrapper">
  94. <!-- 告警角标 -->
  95. <view v-if="item.alarmCount > 0" class="alarm-badge">
  96. {{ item.alarmCount }}
  97. </view>
  98. <view class="device-icon-container" :class="{'offline-icon': item.status === 'offline'}">
  99. <image :src="getDeviceIcon(item.type)" mode="aspectFit" class="device-icon"></image>
  100. </view>
  101. </view>
  102. <view class="device-meta">
  103. <view class="device-name-row">
  104. <text class="device-name" :class="{'offline-text': item.status === 'offline'}">{{ item.name }}</text>
  105. <view
  106. class="status-tag"
  107. :class="item.status === 'online' ? 'status-online' : 'status-offline'"
  108. >
  109. <text class="status-dot" :class="{'offline-dot': item.status === 'offline'}"></text>
  110. {{ item.status === 'online' ? '在线' : '离线' }}
  111. </view>
  112. </view>
  113. <view class="device-id">
  114. <text class="id-label">设备编号:</text>
  115. <text class="id-value">{{ item.code }}</text>
  116. </view>
  117. <view class="device-location">
  118. <text class="location-label">安装位置:</text>
  119. <text class="location-value">{{ item.location }}</text>
  120. </view>
  121. </view>
  122. </view>
  123. <!-- 底部信息栏 -->
  124. <view class="device-footer">
  125. <text class="update-time">{{ item.updateTime }}</text>
  126. <view class="device-actions">
  127. <text class="iconfont icon-right"></text>
  128. </view>
  129. </view>
  130. </view>
  131. <!-- 加载更多提示 -->
  132. <view v-if="filteredDeviceList.length > 0" class="load-more">
  133. <view class="load-more-content" v-if="loadMoreStatus === 'loading'">
  134. <view class="loading-icon"></view>
  135. <text>正在加载...</text>
  136. </view>
  137. <view class="load-more-content" v-if="loadMoreStatus === 'nomore'">
  138. <text>没有更多了</text>
  139. </view>
  140. <view class="load-more-content" v-if="loadMoreStatus === 'loadmore'" @click="loadMore">
  141. <text>点击加载更多</text>
  142. </view>
  143. </view>
  144. </scroll-view>
  145. </view>
  146. </template>
  147. <script>
  148. export default {
  149. data() {
  150. return {
  151. deviceType: '',
  152. searchKeyword: '',
  153. isSearchFocused: false,
  154. statusFilter: 'all', // 'all', 'online', 'offline'
  155. deviceList: [],
  156. page: 1,
  157. limit: 10,
  158. loadMoreStatus: 'loading', // 'loadmore', 'loading', 'nomore'
  159. isLoading: true,
  160. isRefreshing: false
  161. }
  162. },
  163. computed: {
  164. // 过滤后的设备列表
  165. filteredDeviceList() {
  166. let result = this.deviceList;
  167. // 按状态筛选
  168. if (this.statusFilter !== 'all') {
  169. result = result.filter(device => device.status === this.statusFilter);
  170. }
  171. // 按关键词搜索
  172. if (this.searchKeyword) {
  173. const keyword = this.searchKeyword.toLowerCase();
  174. result = result.filter(device =>
  175. device.name.toLowerCase().includes(keyword) ||
  176. device.code.toLowerCase().includes(keyword)
  177. );
  178. }
  179. return result;
  180. },
  181. // 在线设备数量
  182. onlineCount() {
  183. return this.deviceList.filter(device => device.status === 'online').length;
  184. },
  185. // 离线设备数量
  186. offlineCount() {
  187. return this.deviceList.filter(device => device.status === 'offline').length;
  188. }
  189. },
  190. onLoad(options) {
  191. // 获取路由参数
  192. if (options.type) {
  193. this.deviceType = options.type;
  194. this.setPageTitle();
  195. }
  196. // 加载设备数据
  197. this.loadDeviceData();
  198. },
  199. methods: {
  200. // 设置页面标题
  201. setPageTitle() {
  202. const titleMap = {
  203. 'monitor': '监控设备列表',
  204. 'sensor': '采集设备列表',
  205. 'control': '控制设备列表',
  206. 'irrigation': '灌溉设备列表',
  207. 'tractor': '农机设备列表'
  208. };
  209. const title = titleMap[this.deviceType] || '设备列表';
  210. uni.setNavigationBarTitle({
  211. title: title
  212. });
  213. },
  214. // 获取设备图标
  215. getDeviceIcon(type) {
  216. const iconMap = {
  217. 'monitor': '/static/icons/camera.png',
  218. 'sensor': '/static/icons/sensor.png',
  219. 'control': '/static/icons/control.png',
  220. 'irrigation': '/static/icons/water.png',
  221. 'tractor': '/static/icons/tractor.png'
  222. };
  223. return iconMap[type] || '/static/icons/device-default.png';
  224. },
  225. // 设置状态筛选
  226. setStatusFilter(status) {
  227. this.statusFilter = status;
  228. },
  229. // 加载设备数据
  230. loadDeviceData() {
  231. this.isLoading = true;
  232. // 模拟API请求数据
  233. setTimeout(() => {
  234. // 这里应该是真实的API请求
  235. // 模拟一些设备数据用于展示
  236. const newDevices = this.generateMockDevices();
  237. this.deviceList = [...this.deviceList, ...newDevices];
  238. if (this.deviceList.length >= 30) {
  239. this.loadMoreStatus = 'nomore';
  240. } else {
  241. this.loadMoreStatus = 'loadmore';
  242. }
  243. this.isLoading = false;
  244. this.isRefreshing = false;
  245. }, 1000);
  246. },
  247. // 生成模拟设备数据
  248. generateMockDevices() {
  249. const devices = [];
  250. const locations = ['东区A1地块', '西区B2地块', '南区C3地块', '北区D4地块'];
  251. const updateTimes = ['刚刚更新', '1分钟前更新', '5分钟前更新', '10分钟前更新', '1小时前更新'];
  252. // 根据当前页码和限制数量生成对应数量的模拟数据
  253. const startIndex = (this.page - 1) * this.limit;
  254. for (let i = 0; i < this.limit; i++) {
  255. const index = startIndex + i;
  256. // 如果已经生成了30条数据,则停止
  257. if (index >= 30) break;
  258. // 对于采集设备类型,生成随机的气象或土壤设备
  259. let deviceType = this.deviceType;
  260. let deviceCode = `DEV${String(index + 1001).padStart(4, '0')}`;
  261. // 如果是采集设备,随机生成气象站或土壤墒情设备
  262. if (this.deviceType === 'sensor') {
  263. // 随机分配采集设备子类型:气象站或土壤墒情
  264. const sensorSubType = Math.random() > 0.5 ? 'weather' : 'soil';
  265. deviceCode = sensorSubType === 'weather' ? `W${deviceCode}` : `S${deviceCode}`;
  266. }
  267. devices.push({
  268. id: `device-${index + 1}`,
  269. name: `${this.getDeviceTypeName(this.deviceType)}-${index + 1}`,
  270. code: deviceCode,
  271. type: deviceType,
  272. status: Math.random() > 0.3 ? 'online' : 'offline', // 70% 概率在线
  273. location: locations[Math.floor(Math.random() * locations.length)],
  274. updateTime: updateTimes[Math.floor(Math.random() * updateTimes.length)],
  275. alarmCount: Math.random() > 0.7 ? Math.floor(Math.random() * 3) + 1 : 0 // 30% 概率有告警
  276. });
  277. }
  278. return devices;
  279. },
  280. // 获取设备类型名称
  281. getDeviceTypeName(type) {
  282. const nameMap = {
  283. 'monitor': '监控设备',
  284. 'sensor': '采集设备',
  285. 'control': '控制设备',
  286. 'irrigation': '灌溉设备',
  287. 'tractor': '农机设备'
  288. };
  289. return nameMap[type] || '未知设备';
  290. },
  291. // 处理搜索
  292. handleSearch() {
  293. // 执行搜索逻辑
  294. console.log('搜索关键词:', this.searchKeyword);
  295. },
  296. // 处理清空搜索
  297. handleClearSearch() {
  298. this.searchKeyword = '';
  299. },
  300. // 加载更多数据
  301. loadMore() {
  302. if (this.loadMoreStatus !== 'nomore') {
  303. this.loadMoreStatus = 'loading';
  304. this.page += 1;
  305. this.loadDeviceData();
  306. }
  307. },
  308. // 跳转到设备详情页
  309. navigateToDetail(device) {
  310. // 根据设备类型跳转到不同的详情页
  311. let url = '';
  312. if (device.type === 'monitor') {
  313. url = `/pages/device-list/detail-camera?id=${device.id}`;
  314. } else if (device.type === 'sensor') {
  315. // 采集设备跳转到采集设备详情页,同时传递设备编码,便于判断设备子类型
  316. url = `/pages/device-list/detail-collector?id=${device.id}&code=${device.code}`;
  317. } else if (device.type === 'tractor') {
  318. // 农机设备跳转到农机设备详情页
  319. url = `/pages/device-list/detail-machine?id=${device.id}&deviceId=${device.code}`;
  320. } else {
  321. // 其他类型设备暂时使用通用详情页
  322. url = `/pages/device-detail/index?id=${device.id}&type=${device.type}`;
  323. }
  324. uni.navigateTo({
  325. url: url
  326. });
  327. },
  328. // 处理刷新
  329. handleRefresh() {
  330. this.isRefreshing = true;
  331. this.page = 1;
  332. this.deviceList = [];
  333. this.loadDeviceData();
  334. }
  335. }
  336. }
  337. </script>
  338. <style scoped>
  339. /* 图标字体 */
  340. @font-face {
  341. font-family: "iconfont";
  342. 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==');
  343. }
  344. .iconfont {
  345. font-family: "iconfont" !important;
  346. font-size: 24rpx;
  347. font-style: normal;
  348. -webkit-font-smoothing: antialiased;
  349. -moz-osx-font-smoothing: grayscale;
  350. }
  351. .icon-search:before {
  352. content: "\e6e1";
  353. font-size: 32rpx;
  354. }
  355. .icon-close:before {
  356. content: "\e6a7";
  357. font-size: 28rpx;
  358. }
  359. .icon-right:before {
  360. content: "\e6a3";
  361. font-size: 28rpx;
  362. }
  363. .icon-empty:before {
  364. content: "\e6a9";
  365. font-size: 80rpx;
  366. }
  367. /* 容器样式 */
  368. .container {
  369. display: flex;
  370. flex-direction: column;
  371. min-height: 100vh;
  372. background-color: #F9FCFA;
  373. padding-bottom: 20rpx;
  374. }
  375. /* 搜索区域 */
  376. .search-section {
  377. padding: 20rpx 30rpx;
  378. background-color: #FFFFFF;
  379. margin-bottom: 2rpx;
  380. width: 100%;
  381. box-sizing: border-box;
  382. position: relative;
  383. z-index: 5;
  384. box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
  385. }
  386. .search-box {
  387. display: flex;
  388. align-items: center;
  389. background-color: #F7F7F7;
  390. height: 80rpx;
  391. border-radius: 40rpx;
  392. padding: 0 24rpx;
  393. width: 100%;
  394. box-sizing: border-box;
  395. border: 2rpx solid transparent;
  396. transition: all 0.3s ease;
  397. }
  398. .search-box.search-focus {
  399. border-color: #4CAF50;
  400. background-color: #FFFFFF;
  401. box-shadow: 0 0 10rpx rgba(76, 175, 80, 0.1);
  402. }
  403. .search-icon {
  404. color: #4CAF50;
  405. width: 60rpx;
  406. display: flex;
  407. justify-content: center;
  408. }
  409. .search-input {
  410. flex: 1;
  411. height: 80rpx;
  412. font-size: 28rpx;
  413. color: #333333;
  414. }
  415. .clear-icon {
  416. width: 60rpx;
  417. display: flex;
  418. justify-content: center;
  419. color: #999;
  420. }
  421. /* 状态筛选区域 */
  422. .filter-section {
  423. display: flex;
  424. padding: 24rpx 30rpx;
  425. background-color: #FFFFFF;
  426. margin-bottom: 20rpx;
  427. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
  428. position: relative;
  429. z-index: 4;
  430. }
  431. .filter-item {
  432. display: flex;
  433. align-items: center;
  434. font-size: 28rpx;
  435. color: #666666;
  436. margin-right: 30rpx;
  437. padding: 12rpx 20rpx;
  438. border-radius: 30rpx;
  439. transition: all 0.2s ease;
  440. position: relative;
  441. }
  442. .filter-item.active {
  443. background-color: #F0F9F0;
  444. color: #4CAF50;
  445. font-weight: 500;
  446. }
  447. .filter-dot {
  448. width: 14rpx;
  449. height: 14rpx;
  450. border-radius: 50%;
  451. margin-right: 10rpx;
  452. }
  453. .online-dot {
  454. background-color: #4CAF50;
  455. box-shadow: 0 0 6rpx rgba(76, 175, 80, 0.5);
  456. }
  457. .offline-dot {
  458. background-color: #F56C6C;
  459. box-shadow: 0 0 6rpx rgba(245, 108, 108, 0.5);
  460. }
  461. /* 设备列表区域 */
  462. .device-list {
  463. flex: 1;
  464. padding: 0 30rpx;
  465. box-sizing: border-box;
  466. width: 100%;
  467. position: relative;
  468. z-index: 3;
  469. }
  470. /* 空数据提示 */
  471. .empty-tips {
  472. padding: 120rpx 0;
  473. display: flex;
  474. flex-direction: column;
  475. align-items: center;
  476. justify-content: center;
  477. }
  478. .empty-icon {
  479. color: #DDDDDD;
  480. margin-bottom: 20rpx;
  481. }
  482. .empty-text {
  483. font-size: 28rpx;
  484. color: #999999;
  485. margin-bottom: 20rpx;
  486. }
  487. .empty-action {
  488. font-size: 26rpx;
  489. color: #4CAF50;
  490. padding: 12rpx 30rpx;
  491. border-radius: 30rpx;
  492. background-color: rgba(76, 175, 80, 0.1);
  493. }
  494. /* 首次加载中状态 */
  495. .loading-container {
  496. padding: 80rpx 0;
  497. display: flex;
  498. flex-direction: column;
  499. align-items: center;
  500. justify-content: center;
  501. }
  502. .loading-spinner {
  503. width: 60rpx;
  504. height: 60rpx;
  505. border: 4rpx solid #E0E0E0;
  506. border-top: 4rpx solid #4CAF50;
  507. border-radius: 50%;
  508. animation: spin 1s linear infinite;
  509. margin-bottom: 20rpx;
  510. }
  511. .loading-text {
  512. font-size: 28rpx;
  513. color: #999999;
  514. }
  515. /* 设备卡片 */
  516. .device-card {
  517. position: relative;
  518. background-color: #FFFFFF;
  519. border-radius: 24rpx;
  520. padding: 28rpx;
  521. margin-bottom: 24rpx;
  522. box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.05);
  523. transition: all 0.25s ease;
  524. width: 100%;
  525. box-sizing: border-box;
  526. border: 1rpx solid rgba(0, 0, 0, 0.05);
  527. overflow: hidden;
  528. }
  529. .device-card::before {
  530. content: "";
  531. position: absolute;
  532. top: 0;
  533. left: 0;
  534. width: 6rpx;
  535. height: 100%;
  536. background: linear-gradient(to bottom, #66CC6A, #3BB44A);
  537. opacity: 0;
  538. transition: opacity 0.3s ease;
  539. }
  540. .device-card:active::before {
  541. opacity: 1;
  542. }
  543. .device-card-hover {
  544. background-color: #f2fef6;
  545. box-shadow: 0 10rpx 25rpx rgba(59, 180, 74, 0.1);
  546. transform: translateY(-3rpx);
  547. }
  548. .device-card.has-alert {
  549. box-shadow: 0 6rpx 20rpx rgba(245, 108, 108, 0.08);
  550. border: 1rpx solid rgba(245, 108, 108, 0.08);
  551. }
  552. .device-card.has-alert::before {
  553. background: linear-gradient(to bottom, #FF8F8F, #F56C6C);
  554. }
  555. .device-card.is-offline {
  556. opacity: 0.9;
  557. }
  558. .device-card.is-offline.device-card-hover {
  559. background-color: #f5f5f5;
  560. box-shadow: 0 10rpx 25rpx rgba(0, 0, 0, 0.05);
  561. }
  562. /* 告警角标 */
  563. .alarm-badge {
  564. position: absolute;
  565. top: -8rpx;
  566. right: -8rpx;
  567. min-width: 36rpx;
  568. height: 36rpx;
  569. border-radius: 18rpx;
  570. background-color: #F56C6C;
  571. color: #FFFFFF;
  572. font-size: 22rpx;
  573. font-weight: 600;
  574. display: flex;
  575. align-items: center;
  576. justify-content: center;
  577. padding: 0 8rpx;
  578. z-index: 3;
  579. box-shadow: 0 3rpx 8rpx rgba(245, 108, 108, 0.3);
  580. animation: pulse 1.5s infinite;
  581. }
  582. @keyframes pulse {
  583. 0% {
  584. transform: scale(1);
  585. }
  586. 50% {
  587. transform: scale(1.1);
  588. }
  589. 100% {
  590. transform: scale(1);
  591. }
  592. }
  593. /* 设备基本信息 */
  594. .device-info {
  595. display: flex;
  596. margin-bottom: 24rpx;
  597. width: 100%;
  598. }
  599. .device-icon-wrapper {
  600. position: relative;
  601. margin-right: 24rpx;
  602. flex-shrink: 0;
  603. }
  604. .device-icon-container {
  605. width: 96rpx;
  606. height: 96rpx;
  607. border-radius: 50%;
  608. background: linear-gradient(135deg, #66CC6A 0%, #3BB44A 100%);
  609. display: flex;
  610. align-items: center;
  611. justify-content: center;
  612. flex-shrink: 0;
  613. box-shadow: 0 6rpx 16rpx rgba(59, 180, 74, 0.2);
  614. transition: all 0.3s ease;
  615. }
  616. .device-icon-container.offline-icon {
  617. background: linear-gradient(135deg, #AAB2BD 0%, #656D78 100%);
  618. box-shadow: 0 6rpx 16rpx rgba(101, 109, 120, 0.2);
  619. }
  620. .device-icon {
  621. width: 52rpx;
  622. height: 52rpx;
  623. filter: brightness(0) invert(1);
  624. }
  625. .device-meta {
  626. flex: 1;
  627. width: calc(100% - 120rpx);
  628. overflow: hidden;
  629. }
  630. .device-name-row {
  631. display: flex;
  632. justify-content: space-between;
  633. align-items: center;
  634. margin-bottom: 14rpx;
  635. width: 100%;
  636. }
  637. .device-name {
  638. font-size: 34rpx;
  639. font-weight: 600;
  640. color: #333333;
  641. max-width: 65%;
  642. overflow: hidden;
  643. text-overflow: ellipsis;
  644. white-space: nowrap;
  645. transition: color 0.3s ease;
  646. }
  647. .device-name.offline-text {
  648. color: #656D78;
  649. }
  650. .status-tag {
  651. padding: 6rpx 16rpx 6rpx 32rpx;
  652. border-radius: 8rpx;
  653. font-size: 24rpx;
  654. font-weight: 500;
  655. flex-shrink: 0;
  656. border: 1rpx solid;
  657. position: relative;
  658. overflow: hidden;
  659. }
  660. .status-online {
  661. background-color: rgba(76, 175, 80, 0.1);
  662. color: #4CAF50;
  663. border-color: rgba(76, 175, 80, 0.3);
  664. }
  665. .status-dot {
  666. position: absolute;
  667. width: 8rpx;
  668. height: 8rpx;
  669. background-color: #4CAF50;
  670. border-radius: 50%;
  671. top: 50%;
  672. left: 16rpx;
  673. transform: translateY(-50%);
  674. box-shadow: 0 0 4rpx rgba(76, 175, 80, 0.8);
  675. animation: blink 1.5s infinite;
  676. display: inline-block;
  677. }
  678. .status-dot.offline-dot {
  679. background-color: #F56C6C;
  680. box-shadow: 0 0 4rpx rgba(245, 108, 108, 0.8);
  681. }
  682. .status-offline {
  683. background-color: rgba(245, 108, 108, 0.1);
  684. color: #F56C6C;
  685. border-color: rgba(245, 108, 108, 0.3);
  686. padding-left: 32rpx;
  687. }
  688. @keyframes blink {
  689. 0% {
  690. opacity: 0.4;
  691. }
  692. 50% {
  693. opacity: 1;
  694. }
  695. 100% {
  696. opacity: 0.4;
  697. }
  698. }
  699. .device-id, .device-location {
  700. display: flex;
  701. font-size: 26rpx;
  702. margin-top: 10rpx;
  703. color: #666666;
  704. width: 100%;
  705. overflow: hidden;
  706. }
  707. .id-label, .location-label {
  708. color: #999999;
  709. margin-right: 8rpx;
  710. flex-shrink: 0;
  711. }
  712. .id-value, .location-value {
  713. color: #666666;
  714. overflow: hidden;
  715. text-overflow: ellipsis;
  716. white-space: nowrap;
  717. }
  718. /* 底部信息栏 */
  719. .device-footer {
  720. display: flex;
  721. justify-content: space-between;
  722. align-items: center;
  723. padding-top: 18rpx;
  724. border-top: 1rpx solid #F2F2F2;
  725. }
  726. .update-time {
  727. font-size: 24rpx;
  728. color: #999999;
  729. }
  730. .device-actions {
  731. display: flex;
  732. align-items: center;
  733. color: #CCCCCC;
  734. }
  735. /* 加载更多区域 */
  736. .load-more {
  737. padding: 20rpx 0 40rpx;
  738. }
  739. .load-more-content {
  740. display: flex;
  741. justify-content: center;
  742. align-items: center;
  743. height: 60rpx;
  744. font-size: 24rpx;
  745. color: #999999;
  746. }
  747. .loading-icon {
  748. width: 30rpx;
  749. height: 30rpx;
  750. margin-right: 10rpx;
  751. border: 2rpx solid #E0E0E0;
  752. border-top: 2rpx solid #4CAF50;
  753. border-radius: 50%;
  754. animation: spin 1s linear infinite;
  755. }
  756. @keyframes spin {
  757. 0% { transform: rotate(0deg); }
  758. 100% { transform: rotate(360deg); }
  759. }
  760. </style>