diff --git a/public/weather_img/img/weather_bg_large.webp b/public/weather_img/img/weather_bg_large.webp new file mode 100644 index 0000000..57d4589 Binary files /dev/null and b/public/weather_img/img/weather_bg_large.webp differ diff --git a/src/GlobalModal.tsx b/src/GlobalModal.tsx index 8db28a9..f6d1aa7 100644 --- a/src/GlobalModal.tsx +++ b/src/GlobalModal.tsx @@ -9,7 +9,7 @@ const SearchPage = asyncLoader(() => import('@/layout/header/search/SearchPage') const AdderPage = asyncLoader(() => import('@/layout/adder/AdderPage')) const BackgroundSwtich = asyncLoader(() => import('@/layout/background/BackgroundSwtich')) -const noFullList: RouteStr[] = ['global-search', 'global-adder', 'global-background'] +const fullList: RouteStr[] = [] export default defineComponent(() => { const router = useRouterStore() @@ -43,36 +43,40 @@ export default defineComponent(() => {
{/* 关闭按钮 */}
{ router.back() }} > - +
{/* 全屏按钮 */} - {noFullList.includes(router.path) ? null : ( + {!fullList.includes(router.path) ? null : (
{ full.value = !full.value }} > - +
)} @@ -83,23 +87,22 @@ export default defineComponent(() => { ) : router.path === 'global-adder' ? ( ) : router.path === 'global-background' ? ( - - ) : - router.path.startsWith('widget-') ? ( - (() => { - const name = router.path.split('-')[1] - const selected = widgetList.find((el) => el.name === name) - if (!selected) - return ( -
- 组件维护中 -
- ) - const compo = selected.modal - console.log(compo) - return - })() - ) : null} + + ) : router.path.startsWith('widget-') ? ( + (() => { + const name = router.path.split('-')[1] + const selected = widgetList.find((el) => el.name === name) + if (!selected) + return ( +
+ 组件维护中 +
+ ) + const compo = selected.modal + console.log(compo) + return + })() + ) : null}
diff --git a/src/main.css b/src/main.css index e477f7b..56b04fa 100644 --- a/src/main.css +++ b/src/main.css @@ -3,28 +3,29 @@ @tailwind utilities; :root { - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-family: - SourceHanSans, + SourceHanSansCN-Regular, -apple-system, - BlinkMacSystemFont, - Helvetica Neue, PingFang SC, Microsoft YaHei, - Source Han Sans SC, Noto Sans CJK SC, - WenQuanYi Micro Hei, - sans-serif; + WenQuanYi Micro Hei !important; } @font-face { - font-family: 'SourceHanSans'; + font-family: 'SourceHanSansCN-Regular'; src: url('/fonts/SourceHanSansCN-Regular.subset.otf') format('truetype'); font-weight: normal; font-style: normal; } +@font-face { + font-family: 'SourceHanSansCN-bold'; + src: url('/fonts/SourceHanSansCN-Regular.subset.otf') format('truetype'); + font-weight: 700; + font-style: normal; +} body { /* ! 全局禁用鼠标选择,需要在其他位置放开 */ user-select: none; diff --git a/src/main.ts b/src/main.ts index 6c56604..e08fc26 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,12 +8,14 @@ import getFp from './utils/getFp' import vOutsideClick from './utils/vOutsideClick' import dayjs from 'dayjs' import Toast, { useToast, type PluginOptions } from 'vue-toastification' +import customParseFormat from 'dayjs/plugin/customParseFormat' import 'vue-toastification/dist/index.css' import 'dayjs/locale/zh-cn' dayjs.locale('zh-cn') const app = createApp(App) export const globalToast = useToast() +dayjs.extend(customParseFormat) // ! persist 利用 localstorage,请不要在大量数据时使用 // 大量数据(扩张内容,文件),清直接使用 ./db.ts diff --git a/src/settings/useSettingsStore.ts b/src/settings/useSettingsStore.ts index 996f4a5..4ce8147 100644 --- a/src/settings/useSettingsStore.ts +++ b/src/settings/useSettingsStore.ts @@ -19,7 +19,7 @@ export default defineStore( showTime: true, showAdder: true, // 尺寸 - blockSize: 6, + blockSize: 6.7, blockPadding: 1, mainWidth: 70, blockRadius: 0.2, diff --git a/src/widgets/weather/Large.tsx b/src/widgets/weather/Large.tsx index ce10fde..d6bc224 100644 --- a/src/widgets/weather/Large.tsx +++ b/src/widgets/weather/Large.tsx @@ -1,10 +1,69 @@ import { defineComponent } from 'vue' import useWeatherStore from './useWeatherStore' - +import WeatherIcon from './weatherIcon' +import dayjs from 'dayjs' +import clsx from 'clsx' export default defineComponent(() => { const weather = useWeatherStore() - return () =>
{}
+ return () => ( +
+
+ {weather.weatherData?.base.stemp}° +
+
+ + {weather.weatherData?.city.name} + +
+ + 最低{weather.weatherData?.base.ltemp}°/{weather.weatherData?.base.htemp}°最高 + +
+
+ + {weather.weatherData?.base.weather} +
+
+
+
+ {weather.weatherData?.weathers7.map((item, idx) => { + return ( +
+ + {idx === 0 + ? '今天' + : idx === 1 + ? '明天' + : dayjs(item.date, 'MM月DD日').format('ddd')} + + + + {item.ltemp + '°/' + item.htemp + '°'} + +
+ ) + })} +
+
+
+
+ ) }) diff --git a/src/widgets/weather/Middle.tsx b/src/widgets/weather/Middle.tsx index f24e718..3f3a7c4 100644 --- a/src/widgets/weather/Middle.tsx +++ b/src/widgets/weather/Middle.tsx @@ -1,33 +1,67 @@ import { defineComponent } from 'vue' import useWeatherStore from './useWeatherStore' +import WeatherIcon from './weatherIcon' +import dayjs from 'dayjs' +import clsx from 'clsx' export default defineComponent(() => { const weather = useWeatherStore() return () => ( -
+
{weather.weatherData?.base.stemp}° -
+
- + {weather.weatherData?.city.name}
- - - {weather.weatherData?.base.ltemp}°/{weather.weatherData?.base.htemp}° - + + {weather.weatherData?.base.ltemp}°/{weather.weatherData?.base.htemp}°
- {weather.weatherData?.base.weather} +
+ + {weather.weatherData?.base.weather} +
+
+
+
+ {weather.weatherData?.weathers7 + .filter((_, idx) => idx < 3) + .map((item, idx) => { + return ( +
+ + {idx === 0 ? '今天' : idx === 1 ? '明天' : dayjs().format('ddd')} + + + + {item.ltemp + '°/' + item.htemp + '°'} + +
+ ) + })} +
diff --git a/src/widgets/weather/Modal.tsx b/src/widgets/weather/Modal.tsx index 9c16be7..388bca6 100644 --- a/src/widgets/weather/Modal.tsx +++ b/src/widgets/weather/Modal.tsx @@ -1,5 +1,333 @@ -import { defineComponent } from 'vue' - +import { Cascader, message } from 'ant-design-vue' +import { computed, defineComponent, onMounted, ref, type HTMLAttributes, type VNodeRef } from 'vue' +import useWeatherStore from './useWeatherStore' +import type { DefaultOptionType } from 'ant-design-vue/es/select' +import { addIcons, OhVueIcon } from 'oh-vue-icons' +import clsx from 'clsx' +import { FaMinus, HiSolidLocationMarker } from 'oh-vue-icons/icons' +import WeatherIcon from './weatherIcon' +import dayjs from 'dayjs' +addIcons(FaMinus, HiSolidLocationMarker) export default defineComponent(() => { - return () =>
+ const store = useWeatherStore() + const hourRef = ref(null) + const cascaderTriiger = ref(true) + const searchValue = ref('') + const filter = (inputValue: string, path: DefaultOptionType[]) => + path.some( + (option) => (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1 + ) + onMounted(() => { + store.queryAll() + }) + const nowWeather = computed(() => { + if (store.state.selectedCityCode === -1) return store.weatherData + const idx = store.state.weatherList.findIndex((val) => val.id === store.state.selectedCityCode) + + if (idx !== -1) { + return store.state.weatherList[idx].data + } else { + return store.weatherData + } + }) + function getPracticeIndex(val: string) { + const numVal = parseFloat(val) || 0 + if (numVal < 115) { + return '适宜晨练' + } else { + return '不宜晨练' + } + } + function pollution(val: string) { + const numVal = parseFloat(val) || 0 + if (numVal < 35) { + return '优' + } else if (numVal < 75) { + return '良' + } else if (numVal < 115) { + return '轻度污染' + } else if (numVal < 150) { + return '中度污染' + } else if (numVal < 250) { + return '重度污染' + } else if (numVal >= 250) { + return '严重污染' + } else { + return '未知' + } + } + function shirtIndex(val: string) { + const numVal = parseFloat(val) || 0 + if (numVal < 6) { + return '寒冷' + } else if (numVal < 10.9) { + return '冷' + } else if (numVal < 14.9) { + return '凉' + } else if (numVal < 17.9) { + return '温凉' + } else if (numVal < 20.9) { + return '凉舒适' + } else if (numVal < 23.9) { + return '舒适' + } else if (numVal < 27.9) { + return '热舒适' + } else if (numVal >= 28) { + return '炎热' + } else { + return '温凉' + } + } + function coldIndex(val: string) { + const numVal = parseFloat(val) || 0 + if (numVal < 6) { + return '易发感冒' + } else if (numVal < 10.9) { + return '较易发感冒' + } else { + return '不易发感冒' + } + } + return () => ( +
+
+
+ {cascaderTriiger.value && ( + { + cascaderTriiger.value = false + setTimeout(() => { + cascaderTriiger.value = true + }, 0) + + if (store.state.weatherList.findIndex((val) => val.id === option[2].value) !== -1) { + message.info('该地区已添加') + return + } + if (store.state.weatherList.length >= 5) { + message.info('最多只能添加5个地区') + return + } + store.state.weatherList.push({ + id: option[2].value as number, + name: option[2].label, + data: await store.query(option[2].value as number) + }) + store.state.selectedCityCode = option[2].value as number + }} + notFoundContent="没有找到该地区" + onClick={() => { + if (store.state.cityOptions.length === 0) store.queryCity() + }} + searchValue={searchValue.value} + placeholder="搜索其他地区天气" + showSearch={{ filter }} + onSearch={(value) => { + searchValue.value = value + }} + /> + )} +
+
+
{ + store.state.selectedCityCode = -1 + }} + class={clsx( + 'flex flex-col justify-between gap-x-4 w-full h-[60px] relative py-2 px-3 rounded-lg cursor-pointer hover:bg-gradient-to-b from-[#d6ecff] to-[#dce3ff]', + store.state.selectedCityCode === -1 + ? 'bg-gradient-to-b from-[#d6ecff] to-[#dce3ff]' + : 'bg-black/[0.05]' + )} + > +
+
+ + {store.weatherData?.city.name} +
+ {store.weatherData?.base.stemp}° +
+
+
+ + {store.weatherData?.base.weather} +
+ + {store.weatherData?.base.ltemp}°/{store.weatherData?.base.htemp}° + +
+
+ {store.state.weatherList.map((item) => ( +
{ + store.state.selectedCityCode = item.id + }} + class={clsx( + 'flex flex-col group justify-between gap-x-4 w-full h-[65px] group relative py-2 px-3 rounded-lg cursor-pointer hover:bg-gradient-to-b from-[#d6ecff] to-[#dce3ff]', + store.state.selectedCityCode === item.id + ? 'bg-gradient-to-b from-[#d6ecff] to-[#dce3ff]' + : 'bg-black/[0.05]' + )} + key={item.id} + > +
+
+ {item.data?.city.name} +
+ {item.data?.base.stemp}° +
+
+
+ + {item.data?.base.weather} +
+ + {item.data?.base.ltemp}°/{item.data?.base.htemp}° + +
+ +
+ ))} +
+
+
+
+
+
+ {store.weatherData?.city.name} + {nowWeather.value?.base.stemp}° + + {nowWeather.value?.base.weather} + + {nowWeather.value?.base.ltemp}°/{nowWeather.value?.base.htemp}° + + + + + {nowWeather.value?.base.WD} {nowWeather.value?.base.WS} + +
+
+
+
+ 晨练指数: + {getPracticeIndex(nowWeather.value?.base.aqi || '0')} +
+
+ 空气质量: + {pollution(nowWeather.value?.base.pm25 || '0')} +
+
+ 穿衣指数: + {shirtIndex(nowWeather.value?.base.stemp || '0')} +
+
+ 雨伞指数: + + {nowWeather.value?.hour24.some((val) => val.weather.includes('雨')) + ? '需带伞' + : '无需带伞'} + +
+
+ 感冒指数: + {coldIndex(nowWeather.value?.base.stemp || '0')} +
{' '} +
+ 紫外线指数: + 紫外线{nowWeather.value?.base.ultraviolet} +
+
+
+
+
+ 24小时预报 +
{ + e.preventDefault() + + if (!hourRef.value) return + + hourRef.value.scrollLeft += e.deltaY + }} + > + {nowWeather.value?.hour24.map((item, idx) => ( +
+ + {item.stemp}° + {idx === 0 ? '现在' : item.hour + '时'} +
+ ))} +
+
+
+
+ 7日天气预报 +
+ {nowWeather.value?.weathers7.map((item, idx) => { + return ( +
+ + {item.ltemp + '°/' + item.htemp + '°'} + +
+ + {item.weather} +
+
+ + {dayjs(item.date, 'MM月DD日').format('MM/DD')} + + + {idx === 0 + ? '今天' + : idx === 1 + ? '明天' + : dayjs(item.date, 'MM月DD日').format('ddd')} + +
+
+ ) + })} +
+
+
+
+
+
+ ) }) diff --git a/src/widgets/weather/Small.tsx b/src/widgets/weather/Small.tsx index ef55e43..b000d32 100644 --- a/src/widgets/weather/Small.tsx +++ b/src/widgets/weather/Small.tsx @@ -1,5 +1,6 @@ import { defineComponent } from 'vue' import useWeatherStore from './useWeatherStore' +import WeatherIcon from './weatherIcon' export default defineComponent(() => { const weather = useWeatherStore() @@ -15,6 +16,7 @@ export default defineComponent(() => {
{weather.weatherData?.city.name} +
{weather.weatherData?.base.weather} diff --git a/src/widgets/weather/useWeatherStore.ts b/src/widgets/weather/useWeatherStore.ts index ef70253..0be54ec 100644 --- a/src/widgets/weather/useWeatherStore.ts +++ b/src/widgets/weather/useWeatherStore.ts @@ -19,6 +19,7 @@ type WeatherState = { selected: number selectedCityCode: number cityOptions: AntdCityOptions[] + weatherList: { id: number; data?: WeatherData; name: '' }[] } const baseWeatherUrl = 'https://i.tianqi.com/' @@ -27,7 +28,8 @@ export default defineStore('weather', () => { const state = reactive({ selected: 0, selectedCityCode: -1, - cityOptions: [] + cityOptions: [], + weatherList: [] } as WeatherState) const city = ref('') @@ -53,18 +55,40 @@ export default defineStore('weather', () => { } const fetchWeather = async (code: number) => { try { - const nowResponse = await fetch(baseWeatherUrl + '?c=mfjson' + (code ? '&id=' + code : '')) + const nowResponse = await fetch(baseWeatherUrl + '?c=mfjson' + (code !== -1 ? '&id=' + code : '')) const res = await nowResponse.json() - - weatherData.value = res + return res } catch (e) { return } } + const queryAll = () => { + state.weatherList.forEach(async item => { + item.data = await fetchWeather(item.id) + }) + } + const query = async (code: number) => { + const data = await fetchWeather(code) + return data + } watch( () => state.selectedCityCode, - (e) => { - fetchWeather(e) + async (e) => { + if (e === -1) { + const data = await fetchWeather(e) + + weatherData.value = data + } else { + + const idx = state.weatherList.findIndex((item) => item.id === e) + + if (idx === -1) { + if (!state.weatherList[idx]?.data) return + const data = await fetchWeather(e) + state.weatherList[idx].data = data + } + + } }, { immediate: true @@ -75,7 +99,9 @@ export default defineStore('weather', () => { state, city, weatherData, - queryCity + queryCity, + queryAll, + query } }, { persist: true diff --git a/src/widgets/weather/weatherIcon.tsx b/src/widgets/weather/weatherIcon.tsx new file mode 100644 index 0000000..e7ea211 --- /dev/null +++ b/src/widgets/weather/weatherIcon.tsx @@ -0,0 +1,110 @@ +import { computed, defineComponent, render } from 'vue' + +export default defineComponent({ + name: 'WeatherIcon', + props: { + weather: { + type: String, + default: '' + }, + size: { + type: Number, + default: 24 + } + }, + setup: (props, ctx) => { + const list = [ + '阴', //0 + '大雪', //1 + '暴雪', //2 + '中雪', //3 + '小雪', //4 + '雨夹雪', // 5 + '阵雪', //6 + '雪', //7 + '中雨', //8 + '小阵雨', + '雷阵雨伴有冰雹', // 10 + '阵雨', + '冻雨', + '雷阵雨', + '大雨', + '暴雨', // 15 + '大暴雨', + '特大暴雨', + '夜间小雨', + '小雨', + '晴转小雨', // 20 + '多云转晴', + '睛转多云', + '多云', + '大部份多云', + '打雷', // 25 + '冷', + '霜', + '强沙尘暴', + '沙尘暴', + '龙卷风', // 30 + '杨沙', + '雾', + '晴', + '霾', + '风', // 35 + '大风', + '飓风', + '未知天气', //38 + '多云转阴', + '晴转多云', //40 + '阴转小雨', + '多云转阴', + '轻度雾霾', //43 + '轻度雾霾转多云', //44 + '小雨转多云', //45 + '小雨转晴', + '阴转晴', + '中度雾霾', //48 + '阴转多云', + '阴到小雨', //50 + '晴转雪 ', + '多云转雨', + '风转晴', + '多云转小雪', + '雪转雨', //55 + '小雨转雨', + '多云转雪', + '小雪转多云', + '晴转阴', + '阴转中雨', //60 + '雪转多云', + '小雨转阴', //62 + '小雨转中雨', + '多云转小雨', + '小雨到大雨', //65 + '轻度雾霾转小雨', + '轻度雾霾转晴', + '小雨转雪', + '阴转雨', //69 + '中雨转阴', //70 + '中雨转小雨', //71 + '大雨转小雨' //72 + ] + const idx = computed(() => list.indexOf(props.weather || '未知天气')) + + const url = computed(() => { + return idx.value < 0 ? '/weather_img/38.png' : `/weather_img/${idx.value}.png` + }) + return () => ( + <> + weather icon + + ) + } +})