feat:重构模块
This commit is contained in:
20
src/components/Common/AppLoading.vue
Normal file
20
src/components/Common/AppLoading.vue
Normal 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>
|
||||
13
src/components/Common/ArriveButton.vue
Normal file
13
src/components/Common/ArriveButton.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<span style="color: darkgrey; align-self: center;">
|
||||
已经到底咯~
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
101
src/components/Common/DraggableButton.vue
Normal file
101
src/components/Common/DraggableButton.vue
Normal 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>
|
||||
45
src/components/Common/MobileSetting.vue
Normal file
45
src/components/Common/MobileSetting.vue
Normal 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>
|
||||
20
src/components/Common/WeatherIcon.vue
Normal file
20
src/components/Common/WeatherIcon.vue
Normal 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>
|
||||
389
src/components/Common/WeatherTimeCard.vue
Normal file
389
src/components/Common/WeatherTimeCard.vue
Normal 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>
|
||||
Reference in New Issue
Block a user