WeatherCard.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. <template>
  2. <div class="weather-card">
  3. <div class="cloud-bg">
  4. <div class="basic-info">
  5. <div class="location">
  6. <span>上海市</span>
  7. <div class="date-time">
  8. <span>{{ currentDate }}</span>
  9. <span>{{ currentWeek }}</span>
  10. <span>{{ currentTime }}</span>
  11. </div>
  12. </div>
  13. <div class="weather-info">
  14. <div class="temperature">{{ curTemperature || '--' }}℃</div>
  15. <div class="wind">
  16. <div>
  17. <span class="wind-value"> 风力: {{ windSpeedLevel || '--' }}级</span>
  18. </div>
  19. <div>
  20. <span class="wind-value">风速: {{ curWindVelocity || '--' }} km/h</span>
  21. </div>
  22. </div>
  23. </div>
  24. <el-button type="text" class="history-temperature" @click="getHistoryTempDataAndOpenDialog">
  25. <img :src="historyTemperatureIcon" alt="empty" class="icon-history-temperature">
  26. <div class="history-temperature-text">历史温度</div>
  27. </el-button>
  28. <el-dialog v-model="isDialogOpen" title="历史温度统计" width="550px" class="weather-dialog"
  29. :close-on-click-modal="false" @close="handleCloseDialog">
  30. <!-- 加载状态 -->
  31. <el-loading v-if="isLoading" target=".dialog-content" text="正在加载数据...">
  32. <div class="dialog-content" style="min-height: 300px;"></div>
  33. </el-loading>
  34. <!-- 表格 -->
  35. <div v-else>
  36. <div class="highTemperatureWrapper">
  37. <img :src="fireIcon" alt="empty" class="icon-fire">
  38. <span>本年度高温天气: {{ countHighTemperatureDays }}天</span>
  39. </div>
  40. <el-table :data="historyTemperatureList" border stripe max-height="calc(80vh - 120px)">
  41. <el-table-column label="日期" align="center" width="170">
  42. <template #default="scope">
  43. {{ scope.row.dataTime ? scope.row.dataTime.slice(0, 10) : '--' }}
  44. </template>
  45. </el-table-column>
  46. <el-table-column label="最高温度(℃)" prop="maxTemperature" align="center" width="170">
  47. <template #default="scope">
  48. <span> {{ scope.row.maxTemperature ?? "--" }} </span>
  49. </template>
  50. </el-table-column>
  51. <el-table-column label="最低温度(℃)" prop="minTemperature" align="center">
  52. <template #default="scope">
  53. <span> {{ scope.row.minTemperature ?? "--" }} </span>
  54. </template>
  55. </el-table-column>
  56. </el-table>
  57. <!-- 无数据提示 -->
  58. <div v-if="historyTemperatureList.length === 0" class="no-data">
  59. <el-empty description="暂无历史温度数据" />
  60. </div>
  61. </div>
  62. </el-dialog>
  63. </div>
  64. <div class="disaster-emergency-tips">
  65. <WeatherTips :title="measureTitle" :measure="measureInfo" :disasterWarningList="todayWarningInfo" />
  66. </div>
  67. </div>
  68. </div>
  69. </template>
  70. <script setup lang="ts">
  71. import { onMounted, onUnmounted, ref, watch } from 'vue';
  72. import dayjs from 'dayjs';
  73. import WeatherTips from './weather-info/WeatherTips.vue';
  74. import { SysDictDataDetail, queryDictTypeDetail } from '@/api/dict';
  75. import { getTodayDisasterWarnInfoList, getRealTimeWeatherData, getHighTemperatureDays, getHistoryTemperatureList, HistoryTemperatureInfo } from '@/api/disaster-overview';
  76. import fireIcon from '@/assets/images/weather-icons/fire.png'
  77. import historyTemperatureIcon from '@/assets/images/weather-icons/history-temperature.png'
  78. import {
  79. ElButton,
  80. ElDialog,
  81. ElMessage
  82. } from 'element-plus';
  83. import { Calendar, Download } from "@element-plus/icons-vue"
  84. import { Clock } from '@element-plus/icons-vue';
  85. export interface DisasterWarningListType {
  86. disasterType: string;
  87. disasterName: String;
  88. }
  89. const currentDate = ref('');
  90. const currentWeek = ref('');
  91. const currentTime = ref('');
  92. const countHighTemperatureDays = ref<number>(0);
  93. let timer: NodeJS.Timeout;
  94. const weatherDisasterDic = ref<SysDictDataDetail[]>([]); // 气象灾害预警字典
  95. const disasterMeasureDic = ref<SysDictDataDetail[]>([]); // 灾害应急措施字典
  96. const todayWarningInfo = ref<DisasterWarningListType[]>([]); // 今日灾害预警信息
  97. const measureTitle = ref<string | undefined>('');
  98. const measureInfo = ref<string | undefined>('');
  99. const curTemperature = ref('0');
  100. const curWindVelocity = ref(0);
  101. const windSpeedLevel = ref(0);
  102. const isDialogOpen = ref(false);
  103. const isLoading = ref(false);
  104. const historyTemperatureList = ref<HistoryTemperatureInfo[]>([]);
  105. // 风速km/h等级换算表
  106. const windLevels = [
  107. { max: 1, level: 0 },
  108. { max: 5, level: 1 },
  109. { max: 11, level: 2 },
  110. { max: 19, level: 3 },
  111. { max: 28, level: 4 },
  112. { max: 38, level: 5 },
  113. { max: 49, level: 6 },
  114. { max: 61, level: 7 },
  115. { max: 74, level: 8 },
  116. { max: 88, level: 9 },
  117. { max: 102, level: 10 },
  118. { max: 117, level: 11 },
  119. { max: 133, level: 12 },
  120. { max: 149, level: 13 },
  121. { max: 166, level: 14 },
  122. { max: 183, level: 15 },
  123. { max: 201, level: 16 },
  124. { max: 220, level: 17 },
  125. { max: Infinity, level: 18 },
  126. ];
  127. const getWindLevel = (windSpeedKmh: number): number => {
  128. const matched = windLevels.find(({ max }) => windSpeedKmh <= max);
  129. return matched ? matched.level : 18;
  130. };
  131. watch(
  132. () => curWindVelocity.value,
  133. (newVal) => {
  134. windSpeedLevel.value = getWindLevel(newVal);
  135. },
  136. );
  137. // 更新时间函数
  138. const updateDateTime = () => {
  139. const now = dayjs();
  140. currentDate.value = now.format('MM月DD日');
  141. currentWeek.value = `星期${now.format('dd').slice(-1)}`;
  142. currentTime.value = now.format('HH:mm:ss');
  143. };
  144. // 获取实时天气数据
  145. const getRealTimeWeatherDataInfo = async () => {
  146. const res = await getRealTimeWeatherData();
  147. curTemperature.value = res?.temperature || '0';
  148. curWindVelocity.value = (res?.windVelocity || 0) * 3.6;
  149. };
  150. //获取历史温度数据
  151. const getHistoryTempDataAndOpenDialog = async () => {
  152. try {
  153. isLoading.value = true;
  154. const res = await getHistoryTemperatureList();
  155. historyTemperatureList.value = res || [];
  156. isDialogOpen.value = true;
  157. } catch (error) {
  158. ElMessage.error({
  159. message: "请求后端数据失败",
  160. showClose: true,
  161. duration: 2000
  162. });
  163. console.error("获取历史温度数据失败", error);
  164. isLoading.value = false;
  165. historyTemperatureList.value = [];
  166. } finally {
  167. isLoading.value = false;
  168. }
  169. }
  170. const handleCloseDialog = () => {
  171. historyTemperatureList.value = [];
  172. isDialogOpen.value = false;
  173. }
  174. // 获取高温天气天数
  175. const getHighTemperatureDaysRes = async () => {
  176. const res = await getHighTemperatureDays();
  177. countHighTemperatureDays.value = res > 0 && res <= 366 ? res : 0;
  178. };
  179. function getWeatherWarningType(val: string) {
  180. const match = val.match(/^[a-zA-Z0-9]+/);
  181. if (!match) return;
  182. return match[0];
  183. };
  184. // 获取今日灾害预警信息
  185. const getTodayWarningInfo = async () => {
  186. todayWarningInfo.value = (await getTodayDisasterWarnInfoList())?.map((item) => ({
  187. disasterType: item.disasterType,
  188. disasterName:
  189. weatherDisasterDic.value.find((dic) => dic.itemCode === item.disasterType)?.itemValue || '未知预警信息',
  190. }));
  191. const normalMeasure = disasterMeasureDic.value.find((item) => item.itemCode === 'normal_measure');
  192. if (todayWarningInfo.value.length === 0) {
  193. measureTitle.value = '安全提示';
  194. measureInfo.value = normalMeasure?.itemValue || '暂无提示';
  195. } else {
  196. const todayWarningValue = todayWarningInfo.value.find(
  197. (item) => item.disasterType === todayWarningInfo.value[0].disasterType,
  198. )?.disasterType;
  199. if (todayWarningValue) {
  200. const weatherWarningType = getWeatherWarningType(todayWarningValue);
  201. if (weatherWarningType) {
  202. const targetMeasure = disasterMeasureDic.value.find((item) => item.itemCode.includes(weatherWarningType));
  203. measureTitle.value = targetMeasure?.itemValue ? '应急提示' : '安全提示';
  204. measureInfo.value = targetMeasure?.itemValue || normalMeasure?.itemValue;
  205. }
  206. }
  207. }
  208. };
  209. onMounted(async () => {
  210. await queryDictTypeDetail('weather_warning').then((res) => {
  211. weatherDisasterDic.value = res.sysDictDataList;
  212. });
  213. await queryDictTypeDetail('disaster_emergency_measure').then((res) => {
  214. disasterMeasureDic.value = res.sysDictDataList;
  215. });
  216. updateDateTime();
  217. timer = setInterval(updateDateTime, 1000);
  218. getRealTimeWeatherDataInfo();
  219. getHighTemperatureDaysRes();
  220. getTodayWarningInfo();
  221. });
  222. onUnmounted(() => {
  223. clearInterval(timer);
  224. });
  225. </script>
  226. <style lang="scss" scoped>
  227. .weather-card {
  228. width: 100%;
  229. height: 267px;
  230. background: linear-gradient(90deg, #b4ccff 0%, #e4fbf9 100%);
  231. border-radius: 4px;
  232. }
  233. .cloud-bg {
  234. width: 100%;
  235. height: 100%;
  236. display: flex;
  237. padding: 16px;
  238. background-image: url('@/assets/images/disaster-overview/cloud-bg.png');
  239. background-repeat: no-repeat;
  240. background-position: left top;
  241. background-size: auto 100%;
  242. }
  243. .basic-info {
  244. width: 305px;
  245. margin: 4px 35px 32px 12px;
  246. display: flex;
  247. flex-direction: column;
  248. justify-content: space-between;
  249. .location {
  250. gap: 20px;
  251. font-weight: 600;
  252. font-size: 16px;
  253. color: #062B5D;
  254. line-height: 22px;
  255. text-align: left;
  256. font-style: normal;
  257. display: flex;
  258. text-shadow: 1px 1px 1px rgba(15, 61, 125, 0.2);
  259. }
  260. .date-time {
  261. gap: 6px;
  262. text-align: left;
  263. font-style: normal;
  264. display: flex;
  265. }
  266. .weather-info {
  267. display: flex;
  268. align-items: flex-start;
  269. justify-content: space-between;
  270. padding: 0;
  271. margin-top: 80px;
  272. }
  273. .temperature {
  274. font-family: Helvetica, Helvetica;
  275. font-weight: bold;
  276. font-size: 64px;
  277. color: #0f3d7d;
  278. text-align: left;
  279. font-style: normal;
  280. line-height: 1;
  281. margin: 0;
  282. padding: 0;
  283. }
  284. .wind-value {
  285. font-weight: 500;
  286. }
  287. .wind {
  288. display: flex;
  289. flex-direction: column;
  290. gap: 20px;
  291. font-weight: 400;
  292. font-size: 16px;
  293. color: #0f3d7d;
  294. padding-top: 0;
  295. margin: 0;
  296. text-align: left;
  297. line-height: 1;
  298. }
  299. .history-temperature {
  300. width: 108px;
  301. height: 32px;
  302. background: rgba(255, 255, 255, 0.4);
  303. border-radius: 16px;
  304. backdrop-filter: blur(10px);
  305. display: flex;
  306. align-items: center;
  307. justify-content: center;
  308. margin-top: 15px
  309. }
  310. .icon-history-temperature {
  311. width: 12px;
  312. height: 12px;
  313. }
  314. .history-temperature-text {
  315. font-weight: 500;
  316. font-size: 14px;
  317. color: #0F3D7D;
  318. line-height: 20px;
  319. text-align: center;
  320. font-style: normal;
  321. margin-left: 6px;
  322. }
  323. }
  324. :deep(.el-dialog) {
  325. background-image: url('@/assets/images/weather-icons/dialoag-title-bg.png');
  326. background-size: cover;
  327. background-repeat: no-repeat;
  328. background-position: center;
  329. border-radius: 8px;
  330. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  331. .highTemperatureWrapper {
  332. background: rgba(255, 77, 77, 0.12);
  333. border-radius: 4px;
  334. backdrop-filter: blur(5.42253521126761px);
  335. height: 40px;
  336. margin: 10px 0;
  337. font-weight: 800;
  338. font-size: 16px;
  339. color: #FF4D4F;
  340. text-align: left;
  341. font-style: normal;
  342. display: flex;
  343. align-items: center;
  344. justify-content: center;
  345. gap: 6px;
  346. }
  347. .icon-fire {
  348. width: 20px;
  349. height: 20px;
  350. }
  351. .el-dialog__header {
  352. padding:5px 0;
  353. border: none;
  354. display: flex;
  355. position: relative;
  356. align-items: center;
  357. justify-content: flex-start;
  358. }
  359. .el-dialog__headerbtn {
  360. width: 16px;
  361. height: 16px;
  362. right: 0px;
  363. top: 5px;
  364. }
  365. .el-dialog__title {
  366. font-family: PingFangSC, PingFang SC;
  367. font-weight: 800;
  368. font-size: 16px;
  369. color: rgba(0, 0, 0, 0.88);
  370. line-height:24px;
  371. text-align: left;
  372. font-style: normal;
  373. }
  374. .el-dialog__close {
  375. font-size: 16px;
  376. }
  377. }
  378. :deep(.el-table) {
  379. border-radius: 8px;
  380. .el-table__header {
  381. .el-table__cell {
  382. font-family: 'PingFang SC';
  383. font-weight: bold;
  384. font-size: 14px;
  385. color: #000;
  386. line-height: 22px;
  387. text-align: left;
  388. font-style: normal;
  389. }
  390. }
  391. .el-table__body {
  392. overflow: auto;
  393. scrollbar-width: none;
  394. -ms-overflow-style: none;
  395. .el-table__cell {
  396. font-family: 'PingFang SC';
  397. font-weight: 400;
  398. font-size: 14px;
  399. color: #303133;
  400. line-height: 22px;
  401. text-align: left;
  402. font-style: normal;
  403. }
  404. }
  405. }
  406. .no-data {
  407. width: 100%;
  408. padding: 40px 0;
  409. text-align: center;
  410. }
  411. .disaster-emergency-tips {
  412. flex: 1;
  413. }
  414. </style>