feat:重构模块

This commit is contained in:
2025-10-08 22:06:39 +08:00
parent f60d78fe8b
commit 2e81e57774
222 changed files with 2267 additions and 3059 deletions

View File

@@ -0,0 +1,20 @@
<template>
<div v-if="appStore.isLoading" id="app-loading">
<div class="cube-container">
<div class="cube cube1"></div>
<div class="cube cube2"></div>
<div class="cube cube3"></div>
</div>
<p class="loading-text">正在加载中...</p>
</div>
</template>
<script setup>
import { useAppStore } from '@/store/app.ts'
const appStore = useAppStore()
</script>
<style lang="scss">
@use "@/styles/app.loading.module";
</style>

View File

@@ -0,0 +1,13 @@
<template>
<span style="color: darkgrey; align-self: center;">
已经到底咯~
</span>
</template>
<script setup lang="ts">
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,101 @@
<template>
<el-button ref="buttonRef" type="primary" :icon="Plus"
circle size="large" :style="style" style="font-size: 1.8rem;"
@mousedown="startDrag" @touchstart="startDrag"/>
</template>
<script setup lang="ts">
import { computed, defineEmits, onMounted, reactive, ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
const buttonRef = ref()
const buttonPos = reactive({
x: 0,
y: 0,
width: 0,
height: 0
})
const startPos = reactive({
x: 0,
y: 0
})
const offsetPos = reactive({
x: 0,
y: 0
})
/**
* 最小拖到阈值
*/
const dragThreshold = 5
const emit = defineEmits(['click'])
/**
* 实时更新位置
*/
const style = computed(() => ({
position: 'absolute',
left: `${buttonPos.x}px`,
top: `${buttonPos.y}px`,
touchAction: 'none'
}))
onMounted(() => {
initButtonPosition()
})
const initButtonPosition = () => {
buttonPos.x = window.innerWidth - 50
buttonPos.y = window.innerHeight - 120
buttonPos.width = buttonRef.value.$el.offsetWidth
buttonPos.height = buttonRef.value.$el.offsetHeight
}
const startDrag = (event) => {
event.preventDefault()
const touch = event.touches ? event.touches[0] : event
startPos.x = touch.clientX
startPos.y = touch.clientY
offsetPos.x = touch.clientX - buttonPos.x
offsetPos.y = touch.clientY - buttonPos.y
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
document.addEventListener('touchmove', onDrag, { passive: false })
document.addEventListener('touchend', stopDrag)
}
const onDrag = (event) => {
const touch = event.touches ? event.touches[0] : event
// 计算新位置,并限制在边界内
const newX = touch.clientX - offsetPos.x
const newY = touch.clientY - offsetPos.y
buttonPos.x = Math.max(0, Math.min(newX, window.innerWidth - buttonPos.width))
buttonPos.y = Math.max(40, Math.min(newY, window.innerHeight - buttonPos.height - 70))
}
const stopDrag = (event) => {
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', stopDrag)
const touch = event.changedTouches ? event.changedTouches[0] : event
const deltaX = Math.abs(touch.clientX - startPos.x)
const deltaY = Math.abs(touch.clientY - startPos.y)
// 此时为点击
if (deltaX <= dragThreshold && deltaY <= dragThreshold) {
emit('click', event)
}
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<div class="setting-container">
<div class="setting-item">
<span>主题色</span>
<el-color-picker v-model="appStore.themeColor"
@change="changeThemeColorEvent"/>
</div>
<div class="setting-item">
<span>模式</span>
<el-button circle @click="appStore.toggleDark" class="theme-button">
{{ appStore.isDark ? '🌙' : '☀️' }}
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/app.ts'
import { onMounted } from 'vue'
const appStore = useAppStore()
onMounted(() => {
appStore.themeColor = appStore.getThemeColor()
})
const changeThemeColorEvent = (value: string) => {
appStore.setThemeColor(value)
}
</script>
<style scoped lang="scss">
.setting-container {
display: flex;
flex-direction: column;
gap: 10px;
.setting-item {
display: grid;
grid-template-columns: 50px 1fr;
align-items: center;
gap: 10px;
}
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<svg-icon v-if="props.weather.includes('晴')" name="weather-sunny" />
<svg-icon v-else-if="props.weather.includes('多云')" name="weather-cloudy" />
<svg-icon v-else-if="props.weather.includes('雨')" name="weather-rainy" />
<svg-icon v-else-if="props.weather.includes('雪')" name="weather-snowy" />
<svg-icon v-else-if="props.weather.includes('雾')" name="weather-smog" />
<svg-icon v-else name="weather-cloudy" />
</template>
<script setup lang="ts">
import { SvgIcon } from 'vue3-common'
import { defineProps } from 'vue'
const props = defineProps({
weather: {
required: true,
type: String
}
})
</script>

View File

@@ -0,0 +1,389 @@
<template>
<div id="weather-card" class="weather-card card-item">
<div class="time-container">
<span class="time">{{ timeInfo.time }}</span>
<span class="date">{{ timeInfo.date }}</span>
<span class="lunar-date">{{ timeInfo.lunarDate }}</span>
</div>
<div class="weather-container" @click="viewFutureWeatherClick">
<div class="temperature">
<span class="value">{{ weatherInfo.temperature }}°</span>
<weather-icon :weather="weatherInfo.weather"/>
</div>
<div class="location">
<span>{{ locationInfo.province + ' ' + locationInfo.city }}</span>
<span>{{ weatherInfo.weather }}</span>
</div>
<div class="others">
<div class="item">
<i class="fa-solid fa-droplet"></i>
<span>{{ weatherInfo.humidity }}%</span>
</div>
<div class="item">
<i class="fa-solid fa-wind"></i>
<span>{{ weatherInfo.winddirection }} </span>
<span>{{ weatherInfo.windpower }}</span>
</div>
</div>
</div>
<el-dialog v-model="weatherInfo.furtherWeatherDialog"
title="未来3日天气" width="90%" :show-close="false"
center class="weather-dialog">
<div v-for="(item, index) in weatherInfo.furtherWeather.slice(0, 3)"
:key="index" class="further-weather">
<span v-if="index === 0">今天</span>
<span v-else-if="index === 1">明天</span>
<span v-else>后天</span>
<weather-icon :weather="item.dayweather"/>
<span>{{ getDayWeather(item.dayweather, item.nightweather) }}</span>
<span class="temperature">{{ item.daytemp }}° / {{ item.nighttemp }}°</span>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import WeatherIcon from '@/components/Common/WeatherIcon.vue'
import { onMounted, onUnmounted, reactive } from 'vue'
import axios from 'axios'
import { Solar } from 'lunar-javascript'
import { ElNotification } from 'element-plus'
import { useAppStore } from '@/store/app.ts'
// 参考https://lbs.amap.com/api/webservice/summary
const gaodeKey = 'a9c0309e3c4a808140ef522e9360c3cb'
const appStore = useAppStore()
type PeriodType = 'morning' | 'afternoon' | 'evening'
interface IFurtherWeather {
date: string;
daypower: string;
daytemp: string;
daytemp_float: string;
dayweather: string;
daywind: string;
nightpower: string;
nighttemp: string;
nighttemp_float: string;
nightweather: string;
nightwind: string;
week: string;
}
const weatherInfo = reactive({
weather: '',
temperature: '',
humidity: '',
winddirection: '',
windpower: '',
furtherWeatherDialog: false,
furtherWeather: [] as IFurtherWeather[]
})
const timeInfo = reactive({
timer: 0,
time: '',
date: '',
lunarDate: ''
})
const locationInfo = reactive({
location: '',
adcode: '',
country: '',
province: '',
city: '',
district: ''
})
onMounted(async () => {
getTimeInfo()
timeInfo.timer = setInterval(() => {
getTimeInfo()
}, 1000)
setWeatherCardBackground()
setWelcomeNotion()
await getGeoLocationByIpApi()
await getGeoCodeApi(gaodeKey, locationInfo.location)
await getWeatherApi(gaodeKey, locationInfo.adcode)
})
onUnmounted(() => {
clearInterval(timeInfo.timer)
})
const setWeatherCardBackground = () => {
let background = ''
switch (getDayPeriod()) {
case 'morning':
background = 'linear-gradient(to right, #ff9a9e, #fad0c4)'
break
case 'afternoon':
background = 'linear-gradient(to right, #00c6ff, #0072ff)'
break
case 'evening':
background = 'linear-gradient(to right, #2c3e50, #000000)'
break
default:
break
}
const cardElement = document.getElementById('weather-card')
if (cardElement) {
cardElement.style.background = background
}
}
const setWelcomeNotion = () => {
let title = ''
let message = ''
switch (getDayPeriod()) {
case 'morning':
title = '早上好 🌅'
message = '清晨的阳光带来美好一天!'
break
case 'afternoon':
title = '下午好 🌞'
message = '忙碌之余别忘放松心情!'
break
case 'evening':
title = '晚上好 🌙'
message = '夜色温柔,静享美好时光!'
break
default:
break
}
if (!appStore.isLogin) {
ElNotification({ title, message, type: 'success' })
}
appStore.isLogin = true
}
const getDayPeriod = (): PeriodType => {
const now = new Date()
const hour = now.getHours()
if (hour < 12) {
return 'morning'
} else if (hour >= 12 && hour < 18) {
return 'afternoon'
} else {
return 'evening'
}
}
const getGeoLocation = () => {
return new Promise((resolve, reject) => {
if (!('geolocation' in navigator)) {
reject(new Error('您的浏览器不支持Geolocation API'))
}
navigator.geolocation.getCurrentPosition(resolve, reject)
})
}
const getGeoLocationApi = async () => {
try {
const position = await getGeoLocation()
locationInfo.location = `${position.coords.longitude},${position.coords.latitude}`
} catch (e) {
locationInfo.location = '119.028958,31.652127'
}
}
const getGeoLocationByIpApi = async () => {
try {
const response = await axios.get('https://api.ip.sb/geoip')
locationInfo.location = `${response.data.longitude},${response.data.latitude}`
} catch (e) {
locationInfo.location = '119.028958,31.652127'
}
}
const getGeoCodeApi = async (key: string, location: string) => {
const response = await axios.get('https://restapi.amap.com/v3/geocode/regeo', {
params: {
location,
key
}
})
const { addressComponent } = response.data.regeocode
locationInfo.country = addressComponent.country
locationInfo.province = addressComponent.province
locationInfo.city = addressComponent.city
locationInfo.district = addressComponent.district
locationInfo.adcode = addressComponent.adcode
}
const getWeatherApi = async (key: string, city: string) => {
const response = await axios.get('https://restapi.amap.com/v3/weather/weatherInfo', {
params: {
city,
key
}
})
const { lives } = response.data
weatherInfo.weather = lives[0].weather
weatherInfo.temperature = lives[0].temperature
weatherInfo.humidity = lives[0].humidity
weatherInfo.winddirection = lives[0].winddirection
weatherInfo.windpower = lives[0].windpower
}
const getFurtherWeatherApi = async (key: string, city: string) => {
const response = await axios.get('https://restapi.amap.com/v3/weather/weatherInfo', {
params: {
city,
key,
extensions: 'all'
}
})
weatherInfo.furtherWeather = response.data.forecasts[0].casts
}
const getDayWeather = (dayWeather: string, nightWeather: string): string => {
if (dayWeather === nightWeather) {
return dayWeather
} else {
return `${dayWeather}${nightWeather}`
}
}
const getTimeInfo = () => {
timeInfo.date = getCurrentDate()
timeInfo.lunarDate = getCurrentLunarDate()
timeInfo.time = getCurrentTime()
}
const getCurrentDate = (): string => {
const now = new Date()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const weekday = weekdays[now.getDay()]
const hour = now.getHours()
const timePeriod = hour < 12 ? '上午' : hour < 18 ? '下午' : '晚上'
return `${month}${day}${weekday} ${timePeriod}`
}
const getCurrentTime = (): string => {
const now = new Date()
const hour = String(now.getHours()).padStart(2, '0')
const minute = String(now.getMinutes()).padStart(2, '0')
const second = String(now.getSeconds()).padStart(2, '0')
return `${hour}:${minute}:${second}`
}
const getCurrentLunarDate = (): string => {
const solar = Solar.fromDate(new Date())
const lunarYear = solar.getLunar().getYearInGanZhi()
const lunarShengxiao = solar.getLunar().getYearShengXiao()
const lunarMonth = solar.getLunar().getMonthInChinese()
const lunarDay = solar.getLunar().getDayInChinese()
return `${lunarYear}(${lunarShengxiao})年 ${lunarMonth}${lunarDay}`
}
const viewFutureWeatherClick = async () => {
await getFurtherWeatherApi(gaodeKey, locationInfo.adcode)
weatherInfo.furtherWeatherDialog = true
}
</script>
<style lang="scss">
.weather-card {
// height: 200px;
border-radius: 10px;
padding: 10px;
display: grid;
grid-template-columns: repeat(2, 1fr);
color: #FFFFFF;
.time-container {
display: flex;
flex-direction: column;
gap: 5px;
.time {
font-size: 2rem;
}
}
.weather-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
.temperature {
font-size: 2rem;
display: flex;
align-items: center;
gap: 10px;
}
.location, .others {
display: flex;
gap: 10px;
.item {
display: flex;
align-items: center;
gap: 5px;
}
}
}
.weather-dialog {
background: linear-gradient(135deg, #708090 0%, #4682B4 100%);
.el-dialog__title {
color: #FFFFFF;
}
.el-dialog__close {
color: #FFFFFF;
}
.el-dialog__body {
color: #FFFFFF;
display: flex;
flex-direction: column;
gap: 15px;
.further-weather {
display: grid;
grid-template-columns: 30px 25px 2fr 1fr;
align-items: center;
gap: 5px;
.temperature {
justify-self: flex-end;
}
}
}
}
}
</style>