index.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. <template>
  2. <div class="agri-file-page" :style="{ height: `calc(100vh - ${tabBarHeight}px)` }">
  3. <farm-header />
  4. <div class="nav-cards">
  5. <div v-for="item in navCards" :key="item.key" class="nav-card"
  6. :class="{ 'is-active': activeNav === item.key }" @click="activeNav = item.key">
  7. <img class="nav-card__icon" :src="item.icon" alt="" />
  8. <span class="nav-card__label">{{ t(item.labelKey) }}</span>
  9. </div>
  10. </div>
  11. <div class="content-panel">
  12. <div v-show="activeNav === 'patrol'">
  13. <div v-for="item in patrolList" :key="item.id" class="tracking-item"
  14. :class="`tracking-item--${item.theme}`" @click="goGrowthTrack(item)">
  15. <div class="tracking-item__header">
  16. <div class="tracking-item__header-row">
  17. <div class="tracking-item__header-left">
  18. <span class="tracking-item__level">
  19. <el-icon><BellFilled /></el-icon>
  20. {{ item.level }}
  21. </span>
  22. <span class="tracking-item__title van-ellipsis">
  23. {{ item.title }}<span v-if="item.zone" class="tracking-item__zone"> ({{ item.zone }})</span>
  24. </span>
  25. </div>
  26. <div class="tracking-item__share" @click.stop>
  27. <Icon size="14" color="#2199F8" name="share" />
  28. <span>{{ t("agriFile.forward") }}</span>
  29. </div>
  30. </div>
  31. </div>
  32. <div class="tracking-item__body">
  33. <div class="tracking-item__action">
  34. <div class="tracking-item__action-main">
  35. <div class="tracking-item__icon">
  36. <img :src="item.icon" alt="" />
  37. <span class="tracking-item__reason">{{ item.subject }}</span>
  38. </div>
  39. <div class="tracking-item__issue">{{ item.issue }}</div>
  40. </div>
  41. <div class="tracking-item__btn" @click.stop="openUploadPopup">
  42. <el-icon>
  43. <Plus />
  44. </el-icon>
  45. <span>{{ t("agriFile.uploadPhoto") }}</span>
  46. </div>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="crop-archive">
  51. <div class="crop-archive__title">{{ t("agriFile.cropArchive") }}</div>
  52. <PhenologyTrackTimelineItem :list="cropArchiveList" />
  53. </div>
  54. </div>
  55. <div v-show="activeNav === 'album'">
  56. <div class="map-card" @click="goAlbumMap">
  57. <div ref="mapContainer" class="map-container"></div>
  58. <div class="add-zone-btn" @click.stop="goAddZone">
  59. <el-icon>
  60. <Plus />
  61. </el-icon>
  62. <span>{{ t("agriFile.addZone") }}</span>
  63. </div>
  64. <div v-for="item in zoneList" :key="item.id" :ref="(el) => setMarkerEl(item.id, el)"
  65. class="zone-marker">
  66. <div class="zone-marker__label">{{ item.name }}</div>
  67. <div class="zone-marker__thumb">
  68. <img :src="item.cover" alt="" />
  69. <span class="zone-marker__badge">{{ item.count }}</span>
  70. </div>
  71. <div class="zone-marker__arrow"></div>
  72. </div>
  73. </div>
  74. <div class="album-grid">
  75. <div class="album-add">
  76. <el-icon class="album-add__icon">
  77. <Plus />
  78. </el-icon>
  79. <span>{{ t("agriFile.addAlbum") }}</span>
  80. </div>
  81. <div v-for="item in albumList" :key="item.id" class="album-card">
  82. <div class="album-card__cover">
  83. <img :src="item.cover" alt="" />
  84. <span class="album-card__count">{{ t("agriFile.photoCount", { count: item.count }) }}</span>
  85. <div class="album-card__add">
  86. <el-icon>
  87. <Plus />
  88. </el-icon>
  89. </div>
  90. </div>
  91. <div class="album-card__name">{{ item.name }}</div>
  92. </div>
  93. </div>
  94. </div>
  95. <div v-show="activeNav === 'report'">
  96. <div v-for="item in reportList" :key="item.id" class="report-card">
  97. <div class="report-card__cover">
  98. <img :src="item.cover" alt="" />
  99. <span class="report-card__date">{{ item.date }}</span>
  100. <span class="report-card__status">
  101. <i class="report-card__dot"></i>
  102. {{ t("agriFile.reportUpdate") }}
  103. </span>
  104. </div>
  105. <div class="report-card__body">
  106. <div class="report-card__title">{{ item.title }}</div>
  107. <div class="report-card__desc">{{ item.desc }}</div>
  108. </div>
  109. </div>
  110. </div>
  111. </div>
  112. <album-upload-popup v-model:show="showUploadPopup" />
  113. </div>
  114. </template>
  115. <script setup>
  116. import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
  117. import { useStore } from "vuex";
  118. import { useRouter } from "vue-router";
  119. import Overlay from "ol/Overlay";
  120. import { useI18n } from "@/i18n";
  121. import { Icon } from 'vant';
  122. import FileMap from "./fileMap";
  123. import * as util from "@/common/ol_common.js";
  124. import farmHeader from "@/components/farmHeader.vue";
  125. import albumUploadPopup from "./components/albumUploadPopup.vue";
  126. import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
  127. const { t } = useI18n();
  128. const store = useStore();
  129. const router = useRouter();
  130. const tabBarHeight = computed(() => store.state.home.tabBarHeight);
  131. const defaultCover = require("@/assets/img/home/banner.png");
  132. const DEFAULT_CENTER = [113.6142086995688, 23.585836479509055];
  133. const activeNav = ref("patrol");
  134. const mapContainer = ref(null);
  135. const fileMap = new FileMap();
  136. const markerEls = {};
  137. const mapOverlays = [];
  138. const navCards = [
  139. {
  140. key: "patrol",
  141. labelKey: "agriFile.patrolRecord",
  142. icon: require("@/assets/img/agricultural/icon-1.png"),
  143. },
  144. {
  145. key: "album",
  146. labelKey: "agriFile.agriAlbum",
  147. icon: require("@/assets/img/agricultural/icon-2.png"),
  148. },
  149. {
  150. key: "report",
  151. labelKey: "agriFile.diagnosisReport",
  152. icon: require("@/assets/img/agricultural/icon-3.png"),
  153. },
  154. ];
  155. const zoneList = ref([
  156. { id: 1, name: "", count: 2, cover: defaultCover, longitude: 113.612409, latitude: 23.587036 },
  157. { id: 2, name: "", count: 2, cover: defaultCover, longitude: 113.615809, latitude: 23.586636 },
  158. { id: 3, name: "", count: 2, cover: defaultCover, longitude: 113.613609, latitude: 23.584436 },
  159. { id: 4, name: "", count: 2, cover: defaultCover, longitude: 113.615609, latitude: 23.585036 },
  160. ]);
  161. const albumList = ref([
  162. { id: 1, name: "", count: 125, cover: defaultCover },
  163. { id: 2, name: "", count: 125, cover: defaultCover },
  164. { id: 3, name: "", count: 125, cover: defaultCover },
  165. ]);
  166. const reportCover = require("@/assets/img/common/sd-1.jpg");
  167. const reportList = ref([
  168. { id: 1, title: "", desc: "", date: "", cover: reportCover },
  169. { id: 2, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-2.jpg") },
  170. { id: 3, title: "", desc: "", date: "", cover: require("@/assets/img/common/sd-3.jpg") },
  171. ]);
  172. const showUploadPopup = ref(false);
  173. const patrolList = ref([
  174. {
  175. id: 1,
  176. type: "growth",
  177. theme: "blue",
  178. level: "",
  179. title: "",
  180. subject: "",
  181. issue: "",
  182. icon: require("@/assets/img/report/wh-icon.png"),
  183. },
  184. {
  185. id: 2,
  186. type: "abnormal",
  187. theme: "blue",
  188. level: "",
  189. title: "",
  190. subject: "",
  191. issue: "",
  192. icon: require("@/assets/img/report/wh-icon.png"),
  193. },
  194. ]);
  195. const cropArchiveList = ref([
  196. { id: 1, date: "2025-04-18", zone_name: "", content: "" },
  197. { id: 2, date: "2025-04-18", zone_name: "", content: "" },
  198. { id: 3, date: "2025-04-10", zone_name: "", content: "" },
  199. ]);
  200. const setMarkerEl = (id, el) => {
  201. if (el) markerEls[id] = el;
  202. else delete markerEls[id];
  203. };
  204. const getFarmCenter = () => {
  205. try {
  206. const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
  207. const wkt = farm.wkt || farm.geom_wkt || farm.farm_location;
  208. if (typeof wkt === "string" && /^POINT\s*\(/i.test(wkt.trim())) {
  209. return util.wktCastGeom(wkt).getFirstCoordinate();
  210. }
  211. } catch {
  212. // ignore
  213. }
  214. return DEFAULT_CENTER;
  215. };
  216. const clearMapOverlays = () => {
  217. if (!fileMap.kmap?.map) {
  218. mapOverlays.length = 0;
  219. return;
  220. }
  221. mapOverlays.forEach((overlay) => fileMap.kmap.map.removeOverlay(overlay));
  222. mapOverlays.length = 0;
  223. };
  224. const bindZoneOverlays = () => {
  225. if (!fileMap.kmap?.map) return;
  226. clearMapOverlays();
  227. zoneList.value.forEach((zone) => {
  228. const el = markerEls[zone.id];
  229. if (!el || zone.longitude == null || zone.latitude == null) return;
  230. const overlay = new Overlay({
  231. element: el,
  232. position: [zone.longitude, zone.latitude],
  233. positioning: "bottom-center",
  234. stopEvent: false,
  235. });
  236. fileMap.kmap.map.addOverlay(overlay);
  237. mapOverlays.push(overlay);
  238. });
  239. };
  240. const initAlbumMap = async () => {
  241. await nextTick();
  242. if (!mapContainer.value) return;
  243. const center = getFarmCenter();
  244. fileMap.initMap(`POINT(${center[0]} ${center[1]})`, mapContainer.value);
  245. await nextTick();
  246. bindZoneOverlays();
  247. fileMap.kmap?.map?.updateSize?.();
  248. };
  249. const fillMockLabels = () => {
  250. const zoneName = t("agriFile.zoneLabel", { n: 1 });
  251. const albumName = t("agriFile.zoneOne");
  252. zoneList.value = zoneList.value.map((item) => ({ ...item, name: zoneName }));
  253. albumList.value = albumList.value.map((item) => ({ ...item, name: albumName }));
  254. reportList.value = reportList.value.map((item) => ({
  255. ...item,
  256. title: t("agriFile.reportTitle"),
  257. desc: t("agriFile.reportDesc"),
  258. date: t("agriFile.reportDate"),
  259. }));
  260. patrolList.value = patrolList.value.map((item, index) => ({
  261. ...item,
  262. level: t("agriFile.riskLevel2"),
  263. title: t(index === 0 ? "agriFile.patrolGrowthTitle" : "agriFile.patrolAbnormalTitle"),
  264. zone: albumName,
  265. subject: t("agriFile.patrolSubject"),
  266. issue: t("agriFile.patrolIssue"),
  267. }));
  268. cropArchiveList.value = cropArchiveList.value.map((item, index) => ({
  269. ...item,
  270. zone_name: albumName,
  271. content: t(index % 2 === 0 ? "agriFile.cropArchiveMetric" : "agriFile.cropArchiveAbnormal"),
  272. }));
  273. };
  274. const goAddZone = () => {
  275. router.push("/add_zone");
  276. };
  277. const goAlbumMap = () => {
  278. router.push("/album_map");
  279. };
  280. const goGrowthTrack = (item) => {
  281. router.push({
  282. path: "/growth_track",
  283. query: { id: item?.id, type: item?.type || "growth" },
  284. });
  285. };
  286. const openUploadPopup = () => {
  287. showUploadPopup.value = true;
  288. };
  289. onMounted(() => {
  290. fillMockLabels();
  291. initAlbumMap();
  292. });
  293. onActivated(() => {
  294. fillMockLabels();
  295. initAlbumMap();
  296. });
  297. onBeforeUnmount(clearMapOverlays);
  298. watch(activeNav, (key) => {
  299. if (key === "album") {
  300. initAlbumMap();
  301. }
  302. });
  303. </script>
  304. <style lang="scss" scoped>
  305. .agri-file-page {
  306. position: relative;
  307. display: flex;
  308. flex-direction: column;
  309. width: 100%;
  310. height: 100%;
  311. overflow: hidden;
  312. background: linear-gradient(180deg, #D1EBFF 0%, #FFFFFF 100%);
  313. .nav-cards {
  314. position: relative;
  315. z-index: 2;
  316. display: flex;
  317. justify-content: space-around;
  318. padding: 10px 10px;
  319. margin-bottom: 10px;
  320. .nav-card {
  321. position: relative;
  322. display: flex;
  323. flex-direction: column;
  324. align-items: center;
  325. justify-content: center;
  326. border-radius: 8px;
  327. background: #fff;
  328. width: 90px;
  329. height: 64px;
  330. box-shadow: 0px 4px 4px 0px #0000000D;
  331. .nav-card__icon {
  332. width: 26px;
  333. height: 26px;
  334. margin-bottom: 4px;
  335. }
  336. &.is-active {
  337. background: linear-gradient(180deg, #5BB8FF 0%, #2199F8 100%);
  338. box-shadow: 0 4px 10px rgba(33, 153, 248, 0.28);
  339. .nav-card__icon {
  340. filter: brightness(0) invert(1);
  341. }
  342. .nav-card__label {
  343. color: #fff;
  344. }
  345. &::after {
  346. content: "";
  347. position: absolute;
  348. left: 50%;
  349. bottom: -6px;
  350. transform: translateX(-50%);
  351. border-left: 6px solid transparent;
  352. border-right: 6px solid transparent;
  353. border-top: 6px solid #2199F8;
  354. z-index: 3;
  355. }
  356. }
  357. }
  358. }
  359. .content-panel {
  360. flex: 1;
  361. overflow-y: auto;
  362. background: linear-gradient(360deg, rgba(229, 243, 255, 0.55) 0%, rgba(255, 255, 255, 0.55) 100%);
  363. border-radius: 20px 20px 0 0;
  364. border: 1px solid #FFFFFF;
  365. padding: 18px 10px;
  366. &::-webkit-scrollbar {
  367. display: none;
  368. }
  369. .map-card {
  370. position: relative;
  371. height: 210px;
  372. }
  373. .map-container {
  374. width: 100%;
  375. height: 100%;
  376. clip-path: inset(0px round 8px);
  377. }
  378. .zone-marker {
  379. display: flex;
  380. flex-direction: column;
  381. align-items: center;
  382. pointer-events: none;
  383. &__label {
  384. padding: 3px 14px;
  385. border-radius: 25px;
  386. background: #fff;
  387. color: #0A0A0A;
  388. font-size: 12px;
  389. }
  390. &__thumb {
  391. position: relative;
  392. width: 42px;
  393. height: 42px;
  394. margin-top: 8px;
  395. img {
  396. width: 100%;
  397. height: 100%;
  398. object-fit: cover;
  399. border: 2px solid #fff;
  400. border-radius: 6px;
  401. box-sizing: border-box;
  402. }
  403. }
  404. &__badge {
  405. position: absolute;
  406. top: -6px;
  407. right: -6px;
  408. min-width: 18px;
  409. height: 18px;
  410. line-height: 16px;
  411. border-radius: 50%;
  412. background: #fff;
  413. color: #0A0A0A;
  414. font-size: 12px;
  415. text-align: center;
  416. }
  417. &__arrow {
  418. width: 0;
  419. height: 0;
  420. border-left: 5px solid transparent;
  421. border-right: 5px solid transparent;
  422. border-top: 6px solid #fff;
  423. }
  424. }
  425. .add-zone-btn {
  426. position: absolute;
  427. right: 5px;
  428. bottom: 10px;
  429. z-index: 5;
  430. display: flex;
  431. align-items: center;
  432. gap: 2px;
  433. padding: 5px 10px;
  434. border-radius: 4px;
  435. background: #fff;
  436. color: #2199F8;
  437. font-weight: 500;
  438. }
  439. .album-grid {
  440. display: grid;
  441. grid-template-columns: 1fr 1fr;
  442. gap: 16px;
  443. margin-top: 16px;
  444. box-sizing: border-box;
  445. .album-add {
  446. align-self: start;
  447. display: flex;
  448. flex-direction: column;
  449. align-items: center;
  450. justify-content: center;
  451. width: 100%;
  452. aspect-ratio: 1;
  453. border-radius: 5px;
  454. background: #F5F5F7;
  455. color: #969494;
  456. font-size: 16px;
  457. &__icon {
  458. margin-bottom: 6px;
  459. font-size: 20px;
  460. }
  461. }
  462. .album-card {
  463. width: 100%;
  464. &__cover {
  465. position: relative;
  466. width: 100%;
  467. aspect-ratio: 1;
  468. img {
  469. width: 100%;
  470. height: 100%;
  471. object-fit: cover;
  472. border-radius: 6px;
  473. }
  474. }
  475. &__count {
  476. position: absolute;
  477. top: 1px;
  478. right: 0;
  479. padding: 1px 5px;
  480. border-radius: 0 5px 0 5px;
  481. background: rgba(0, 0, 0, 0.4);
  482. backdrop-filter: blur(4px);
  483. color: #fff;
  484. }
  485. &__add {
  486. position: absolute;
  487. right: 8px;
  488. bottom: 8px;
  489. z-index: 1;
  490. display: flex;
  491. align-items: center;
  492. justify-content: center;
  493. width: 44px;
  494. height: 44px;
  495. border-radius: 50%;
  496. background: #2199F8;
  497. color: #fff;
  498. font-size: 20px;
  499. box-shadow: 0 2px 8px rgba(33, 153, 248, 0.35);
  500. }
  501. &__name {
  502. margin-top: 10px;
  503. color: #1F1F1F;
  504. }
  505. }
  506. }
  507. .report-card {
  508. margin-bottom: 10px;
  509. border-radius: 8px;
  510. background: #fff;
  511. &:last-child {
  512. margin-bottom: 0;
  513. }
  514. &__cover {
  515. position: relative;
  516. width: 100%;
  517. height: 158px;
  518. img {
  519. display: block;
  520. width: 100%;
  521. height: 100%;
  522. object-fit: cover;
  523. border-radius: 5px;
  524. }
  525. }
  526. &__date {
  527. position: absolute;
  528. top: 0;
  529. padding: 5px 12px;
  530. border-radius: 4px;
  531. background: rgba(0, 0, 0, 0.45);
  532. backdrop-filter: blur(4px);
  533. color: #fff;
  534. font-size: 16px;
  535. border-radius: 5px 0 5px 0;
  536. }
  537. &__status {
  538. position: absolute;
  539. top: 10px;
  540. right: 0;
  541. display: flex;
  542. align-items: center;
  543. gap: 4px;
  544. padding: 2px 13px;
  545. border-radius: 20px 0 0 20px;
  546. background: #FF5454;
  547. color: #fff;
  548. font-size: 12px;
  549. }
  550. &__dot {
  551. width: 6px;
  552. height: 6px;
  553. border-radius: 50%;
  554. background: #fff;
  555. }
  556. &__body {
  557. padding: 10px;
  558. margin-top: 4px;
  559. }
  560. &__title {
  561. font-size: 16px;
  562. font-weight: 500;
  563. }
  564. &__desc {
  565. margin-top: 4px;
  566. color: #9E9B9B;
  567. }
  568. }
  569. .tracking-item {
  570. width: 100%;
  571. margin-bottom: 16px;
  572. box-shadow: 0px 4px 4px 0px rgba(33, 153, 248, 0.1);
  573. &:last-of-type {
  574. margin-bottom: 0;
  575. }
  576. &__header {
  577. position: relative;
  578. padding: 10px 10px 20px;
  579. border-radius: 10px 10px 0 0;
  580. background: linear-gradient(180deg, #86CAFF 0%, #2199F8 48.5%);
  581. }
  582. &__header-row {
  583. display: flex;
  584. align-items: center;
  585. justify-content: space-between;
  586. }
  587. &__header-left {
  588. display: flex;
  589. align-items: center;
  590. gap: 5px;
  591. }
  592. &__level {
  593. display: flex;
  594. align-items: center;
  595. gap: 4px;
  596. padding: 1px 4px;
  597. border-radius: 2px;
  598. background: #fff;
  599. color: #FF6A6A;
  600. font-size: 12px;
  601. }
  602. &__title {
  603. color: #fff;
  604. font-size: 16px;
  605. font-weight: 500;
  606. }
  607. &__zone {
  608. font-size: 12px;
  609. }
  610. &__share {
  611. display: flex;
  612. align-items: center;
  613. gap: 4px;
  614. padding: 2px 10px;
  615. border-radius: 25px;
  616. background: #fff;
  617. color: #2199F8;
  618. font-size: 12px;
  619. }
  620. &__body {
  621. position: relative;
  622. padding: 10px;
  623. margin-top: -8px;
  624. border-radius: 10px;
  625. background: #fff;
  626. &::before {
  627. content: "";
  628. position: absolute;
  629. top: -7px;
  630. left: 21%;
  631. width: 0;
  632. height: 0;
  633. border-left: 7px solid transparent;
  634. border-right: 7px solid transparent;
  635. border-bottom: 7px solid #fff;
  636. transform: translateX(-50%);
  637. }
  638. }
  639. &__action {
  640. display: flex;
  641. align-items: center;
  642. justify-content: space-between;
  643. border: 1px solid rgba(33, 153, 248, 0.2);
  644. border-radius: 6px;
  645. padding: 8px;
  646. }
  647. &__icon {
  648. display: flex;
  649. align-items: center;
  650. gap: 5px;
  651. img {
  652. width: 18px;
  653. height: 16px;
  654. }
  655. }
  656. &__reason {
  657. color: #2199f8;
  658. font-weight: 500;
  659. }
  660. &__issue {
  661. margin-top: 2px;
  662. color: #2199f8;
  663. font-size: 12px;
  664. }
  665. &__btn {
  666. display: flex;
  667. align-items: center;
  668. gap: 2px;
  669. padding: 7px 10px;
  670. border-radius: 5px;
  671. background: #2199f8;
  672. color: #fff;
  673. font-size: 12px;
  674. }
  675. }
  676. .crop-archive {
  677. padding: 10px;
  678. border-radius: 10px;
  679. background: #fff;
  680. &__title {
  681. position: relative;
  682. display: inline-block;
  683. z-index: 1;
  684. font-size: 16px;
  685. font-weight: 500;
  686. margin-bottom: 5px;
  687. &::after {
  688. content: "";
  689. position: absolute;
  690. left: 0;
  691. bottom: 2px;
  692. z-index: -1;
  693. width: 90px;
  694. height: 6px;
  695. border-radius: 25px;
  696. background: linear-gradient(90deg, #2199f8 0%, rgba(255, 255, 255, 0.37) 100%);
  697. opacity: 0.4;
  698. }
  699. }
  700. }
  701. }
  702. }
  703. </style>