mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 10:18:57 +08:00
🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="dialog"
|
||||
:show-icon="false"
|
||||
:mask-closable="true"
|
||||
class="install-app-modal"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="app-icon">
|
||||
<img src="@/assets/logo.png" alt="App Icon" />
|
||||
</div>
|
||||
<div class="app-info">
|
||||
<h2 class="app-name">Alger Music Player {{ config.version }}</h2>
|
||||
<p class="app-desc mb-2">在桌面安装应用,获得更好的体验</p>
|
||||
<n-checkbox v-model:checked="noPrompt">不再提示</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<n-button class="cancel-btn" @click="closeModal">暂不安装</n-button>
|
||||
<n-button type="primary" class="install-btn" @click="handleInstall">立即安装</n-button>
|
||||
</div>
|
||||
<div class="modal-desc mt-4 text-center">
|
||||
<p class="text-xs text-gray-400">
|
||||
下载遇到问题?去
|
||||
<a
|
||||
class="text-green-500"
|
||||
target="_blank"
|
||||
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
|
||||
>GitHub</a
|
||||
>
|
||||
下载最新版本
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { isElectron, isMobile } from '@/utils';
|
||||
|
||||
import config from '../../../../package.json';
|
||||
|
||||
const showModal = ref(false);
|
||||
const noPrompt = ref(false);
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
if (noPrompt.value) {
|
||||
localStorage.setItem('installPromptDismissed', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 如果是 electron 环境,不显示安装提示
|
||||
if (isElectron || isMobile.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经点击过"暂不安装"
|
||||
const isDismissed = localStorage.getItem('installPromptDismissed') === 'true';
|
||||
if (isDismissed) {
|
||||
return;
|
||||
}
|
||||
showModal.value = true;
|
||||
});
|
||||
|
||||
const handleInstall = async (): Promise<void> => {
|
||||
const { userAgent } = navigator;
|
||||
console.log('userAgent', userAgent);
|
||||
const isMac: boolean = userAgent.includes('Mac');
|
||||
const isWindows: boolean = userAgent.includes('Win');
|
||||
const isARM: boolean =
|
||||
userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
|
||||
const isX64: boolean =
|
||||
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
|
||||
const isX86: boolean =
|
||||
!isX64 &&
|
||||
(userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
|
||||
|
||||
const getDownloadUrl = (os: string, arch: string): string => {
|
||||
const version = config.version as string;
|
||||
const setup = os !== 'mac' ? 'Setup_' : '';
|
||||
return `https://gh.llkk.cc/https://github.com/algerkong/AlgerMusicPlayer/releases/download/${version}/AlgerMusic_${version}_${setup}${arch}.${os === 'mac' ? 'dmg' : 'exe'}`;
|
||||
};
|
||||
const osType: string | null = isMac ? 'mac' : isWindows ? 'windows' : null;
|
||||
const archType: string | null = isARM ? 'arm64' : isX64 ? 'x64' : isX86 ? 'x86' : null;
|
||||
|
||||
const downloadUrl: string | null = osType && archType ? getDownloadUrl(osType, archType) : null;
|
||||
|
||||
window.open(downloadUrl || 'https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.install-app-modal {
|
||||
:deep(.n-modal) {
|
||||
@apply max-w-sm;
|
||||
}
|
||||
.modal-content {
|
||||
@apply p-4 pb-0;
|
||||
.modal-header {
|
||||
@apply flex items-center mb-6;
|
||||
.app-icon {
|
||||
@apply w-20 h-20 mr-4 rounded-2xl overflow-hidden;
|
||||
img {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
}
|
||||
.app-info {
|
||||
@apply flex-1;
|
||||
.app-name {
|
||||
@apply text-xl font-bold mb-1;
|
||||
}
|
||||
.app-desc {
|
||||
@apply text-sm text-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-actions {
|
||||
@apply flex gap-3 mt-4;
|
||||
.n-button {
|
||||
@apply flex-1;
|
||||
}
|
||||
.cancel-btn {
|
||||
@apply bg-gray-800 text-gray-300 border-none;
|
||||
&:hover {
|
||||
@apply bg-gray-700;
|
||||
}
|
||||
}
|
||||
.install-btn {
|
||||
@apply bg-green-600 border-none;
|
||||
&:hover {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { setAnimationClass } from '@/utils';
|
||||
|
||||
const props = defineProps({
|
||||
showPop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const musicFullClass = computed(() => {
|
||||
if (props.showPop) {
|
||||
return setAnimationClass('animate__fadeInUp');
|
||||
}
|
||||
return setAnimationClass('animate__fadeOutDown');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="props.showPop" class="pop-page" :class="musicFullClass">
|
||||
<i v-if="props.showClose" class="iconfont icon-icon_error close"></i>
|
||||
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pop-page {
|
||||
height: 800px;
|
||||
@apply absolute top-4 left-0 w-full;
|
||||
background-color: #000000f0;
|
||||
.close {
|
||||
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div v-if="isPlay" class="bottom" :style="{ height }"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
const isPlay = computed(() => store.state.isPlay as boolean);
|
||||
defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bottom {
|
||||
@apply h-28;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div class="search-item" :class="item.type" @click="handleClick">
|
||||
<div class="search-item-img">
|
||||
<n-image
|
||||
:src="getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '100y100')"
|
||||
lazy
|
||||
preview-disabled
|
||||
/>
|
||||
<div v-if="item.type === 'mv'" class="play">
|
||||
<i class="iconfont icon icon-play"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-item-info">
|
||||
<p class="search-item-name">{{ item.name }}</p>
|
||||
<p class="search-item-artist">{{ item.desc }}</p>
|
||||
</div>
|
||||
|
||||
<MusicList
|
||||
v-if="['专辑', 'playlist'].includes(item.type)"
|
||||
v-model:show="showPop"
|
||||
:name="item.name"
|
||||
:song-list="songList"
|
||||
:list-info="listInfo"
|
||||
:cover="false"
|
||||
/>
|
||||
<mv-player
|
||||
v-if="item.type === 'mv'"
|
||||
v-model:show="showPop"
|
||||
:current-mv="getCurrentMv()"
|
||||
no-list
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getAlbum, getListDetail } from '@/api/list';
|
||||
import MvPlayer from '@/components/MvPlayer.vue';
|
||||
import { audioService } from '@/services/audioService';
|
||||
import { IMvItem } from '@/type/mv';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import MusicList from '../MusicList.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
picUrl: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}>();
|
||||
|
||||
const songList = ref<any[]>([]);
|
||||
|
||||
const showPop = ref(false);
|
||||
const listInfo = ref<any>(null);
|
||||
|
||||
const getCurrentMv = () => {
|
||||
return {
|
||||
id: props.item.id,
|
||||
name: props.item.name
|
||||
} as unknown as IMvItem;
|
||||
};
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleClick = async () => {
|
||||
listInfo.value = null;
|
||||
if (props.item.type === '专辑') {
|
||||
showPop.value = true;
|
||||
const res = await getAlbum(props.item.id);
|
||||
songList.value = res.data.songs.map((song: any) => {
|
||||
song.al.picUrl = song.al.picUrl || props.item.picUrl;
|
||||
return song;
|
||||
});
|
||||
listInfo.value = {
|
||||
...res.data.album,
|
||||
creator: {
|
||||
avatarUrl: res.data.album.artist.img1v1Url,
|
||||
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`
|
||||
},
|
||||
description: res.data.album.description
|
||||
};
|
||||
}
|
||||
|
||||
if (props.item.type === 'playlist') {
|
||||
showPop.value = true;
|
||||
const res = await getListDetail(props.item.id);
|
||||
songList.value = res.data.playlist.tracks;
|
||||
listInfo.value = res.data.playlist;
|
||||
}
|
||||
|
||||
if (props.item.type === 'mv') {
|
||||
store.commit('setIsPlay', false);
|
||||
store.commit('setPlayMusic', false);
|
||||
audioService.getCurrentSound()?.pause();
|
||||
showPop.value = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-item {
|
||||
@apply rounded-3xl p-3 flex items-center hover:bg-light-200 dark:hover:bg-gray-800 transition cursor-pointer;
|
||||
margin: 0 10px;
|
||||
.search-item-img {
|
||||
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
|
||||
}
|
||||
.search-item-info {
|
||||
@apply flex-1 overflow-hidden;
|
||||
&-name {
|
||||
@apply text-white text-sm text-center;
|
||||
}
|
||||
&-artist {
|
||||
@apply text-gray-400 text-xs text-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mv {
|
||||
&:hover {
|
||||
.play {
|
||||
@apply opacity-60;
|
||||
}
|
||||
}
|
||||
.search-item-img {
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
@apply rounded-lg relative;
|
||||
}
|
||||
.play {
|
||||
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity;
|
||||
.icon {
|
||||
@apply text-white text-5xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div class="song-item" :class="{ 'song-mini': mini, 'song-list': list }">
|
||||
<n-image
|
||||
v-if="item.picUrl"
|
||||
ref="songImg"
|
||||
:src="getImgUrl(item.picUrl, '100y100')"
|
||||
class="song-item-img"
|
||||
preview-disabled
|
||||
:img-props="{
|
||||
crossorigin: 'anonymous'
|
||||
}"
|
||||
@load="imageLoad"
|
||||
/>
|
||||
<div class="song-item-content">
|
||||
<div v-if="list" class="song-item-content-wrapper">
|
||||
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{
|
||||
item.name
|
||||
}}</n-ellipsis>
|
||||
<div class="song-item-content-divider">-</div>
|
||||
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
|
||||
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
||||
>{{ artists.name
|
||||
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||
>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="song-item-content-title">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||
</div>
|
||||
<div class="song-item-content-name">
|
||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
||||
<span
|
||||
v-for="(artists, artistsindex) in item.ar || item.song.artists"
|
||||
:key="artistsindex"
|
||||
>{{ artists.name
|
||||
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||
>
|
||||
</n-ellipsis>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
|
||||
<div v-if="favorite" class="song-item-operating-like">
|
||||
<i
|
||||
class="iconfont icon-likefill"
|
||||
:class="{ 'like-active': isFavorite }"
|
||||
@click.stop="toggleFavorite"
|
||||
></i>
|
||||
</div>
|
||||
<div
|
||||
class="song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated"
|
||||
:class="{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }"
|
||||
@click="playMusicEvent(item)"
|
||||
>
|
||||
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
|
||||
<i v-else class="iconfont icon-playfill"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { audioService } from '@/services/audioService';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import { getImageBackground } from '@/utils/linearColor';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
item: SongResult;
|
||||
mini?: boolean;
|
||||
list?: boolean;
|
||||
favorite?: boolean;
|
||||
}>(),
|
||||
{
|
||||
mini: false,
|
||||
list: false,
|
||||
favorite: true
|
||||
}
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const play = computed(() => store.state.play as boolean);
|
||||
|
||||
const playMusic = computed(() => store.state.playMusic);
|
||||
|
||||
const playLoading = computed(
|
||||
() => playMusic.value.id === props.item.id && playMusic.value.playLoading
|
||||
);
|
||||
|
||||
// 判断是否为正在播放的音乐
|
||||
const isPlaying = computed(() => {
|
||||
return playMusic.value.id === props.item.id;
|
||||
});
|
||||
|
||||
const emits = defineEmits(['play']);
|
||||
|
||||
const songImageRef = useTemplateRef('songImg');
|
||||
|
||||
const imageLoad = async () => {
|
||||
if (!songImageRef.value) {
|
||||
return;
|
||||
}
|
||||
const { backgroundColor } = await getImageBackground(
|
||||
(songImageRef.value as any).imageRef as unknown as HTMLImageElement
|
||||
);
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
props.item.backgroundColor = backgroundColor;
|
||||
};
|
||||
|
||||
// 播放音乐 设置音乐详情 打开音乐底栏
|
||||
const playMusicEvent = async (item: SongResult) => {
|
||||
if (playMusic.value.id === item.id) {
|
||||
if (play.value) {
|
||||
store.commit('setPlayMusic', false);
|
||||
audioService.getCurrentSound()?.pause();
|
||||
} else {
|
||||
store.commit('setPlayMusic', true);
|
||||
audioService.getCurrentSound()?.play();
|
||||
}
|
||||
return;
|
||||
}
|
||||
await store.commit('setPlay', item);
|
||||
store.commit('setIsPlay', true);
|
||||
emits('play', item);
|
||||
};
|
||||
|
||||
// 判断是否已收藏
|
||||
const isFavorite = computed(() => {
|
||||
return store.state.favoriteList.includes(props.item.id);
|
||||
});
|
||||
|
||||
// 切换收藏状态
|
||||
const toggleFavorite = async (e: Event) => {
|
||||
e.stopPropagation();
|
||||
if (isFavorite.value) {
|
||||
store.commit('removeFromFavorite', props.item.id);
|
||||
} else {
|
||||
store.commit('addToFavorite', props.item.id);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 配置文字不可选中
|
||||
.text-ellipsis {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.song-item {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
@apply rounded-3xl p-3 flex items-center transition bg-transparent dark:text-white text-gray-900;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-100 dark:bg-gray-800;
|
||||
}
|
||||
|
||||
&-img {
|
||||
@apply w-12 h-12 rounded-2xl mr-4;
|
||||
}
|
||||
|
||||
&-content {
|
||||
@apply flex-1;
|
||||
|
||||
&-title {
|
||||
@apply text-base text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply text-xs text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
&-operating {
|
||||
@apply flex items-center rounded-full ml-4 border dark:border-gray-700 border-gray-200 bg-light dark:bg-black;
|
||||
|
||||
.iconfont {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
.icon-likefill {
|
||||
@apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-red-500;
|
||||
}
|
||||
|
||||
&-like {
|
||||
@apply mr-2 cursor-pointer ml-4;
|
||||
}
|
||||
|
||||
.like-active {
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
&-play {
|
||||
@apply cursor-pointer rounded-full w-10 h-10 flex justify-center items-center transition
|
||||
border dark:border-gray-700 border-gray-200 text-gray-900 dark:text-white;
|
||||
|
||||
&:hover,
|
||||
&.bg-green-600 {
|
||||
@apply bg-green-500 border-green-500 text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.song-mini {
|
||||
@apply p-2 rounded-2xl;
|
||||
|
||||
.song-item {
|
||||
@apply p-0;
|
||||
|
||||
&-img {
|
||||
@apply w-10 h-10 mr-2;
|
||||
}
|
||||
|
||||
&-content {
|
||||
@apply flex-1;
|
||||
|
||||
&-title {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply text-xs;
|
||||
}
|
||||
}
|
||||
|
||||
&-operating {
|
||||
@apply pl-2;
|
||||
|
||||
.iconfont {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
&-like {
|
||||
@apply mr-1 ml-1;
|
||||
}
|
||||
|
||||
&-play {
|
||||
@apply w-8 h-8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.song-list {
|
||||
@apply p-2 rounded-lg mb-2 border dark:border-gray-800 border-gray-200;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-50 dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.song-item-img {
|
||||
@apply w-10 h-10 rounded-lg mr-3;
|
||||
}
|
||||
|
||||
.song-item-content {
|
||||
@apply flex items-center flex-1;
|
||||
|
||||
&-wrapper {
|
||||
@apply flex items-center flex-1 text-sm;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply flex-shrink-0 max-w-[45%] text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
&-divider {
|
||||
@apply mx-2 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
&-name {
|
||||
@apply flex-1 min-w-0 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.song-item-operating {
|
||||
@apply flex items-center gap-2;
|
||||
|
||||
&-like {
|
||||
@apply cursor-pointer hover:scale-110 transition-transform;
|
||||
|
||||
.iconfont {
|
||||
@apply text-base text-gray-500 dark:text-gray-400 hover:text-red-500;
|
||||
}
|
||||
}
|
||||
|
||||
&-play {
|
||||
@apply w-7 h-7 cursor-pointer hover:scale-110 transition-transform;
|
||||
|
||||
.iconfont {
|
||||
@apply text-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="dialog"
|
||||
:show-icon="false"
|
||||
:mask-closable="true"
|
||||
class="update-app-modal"
|
||||
style="width: 800px; max-width: 90vw"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="app-icon">
|
||||
<img src="@/assets/logo.png" alt="App Icon" />
|
||||
</div>
|
||||
<div class="app-info">
|
||||
<h2 class="app-name">发现新版本 {{ updateInfo.latestVersion }}</h2>
|
||||
<p class="app-desc mb-2">当前版本 {{ updateInfo.currentVersion }}</p>
|
||||
<n-checkbox v-model:checked="noPrompt">不再提示</n-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="update-info">
|
||||
<div class="update-title">更新内容:</div>
|
||||
<n-scrollbar style="max-height: 300px">
|
||||
<div class="update-body" v-html="parsedReleaseNotes"></div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<n-button class="cancel-btn" @click="closeModal">暂不更新</n-button>
|
||||
<n-button type="primary" class="update-btn" @click="handleUpdate">立即更新</n-button>
|
||||
</div>
|
||||
<div class="modal-desc mt-4 text-center">
|
||||
<p class="text-xs text-gray-400">
|
||||
下载遇到问题?去
|
||||
<a
|
||||
class="text-green-500"
|
||||
target="_blank"
|
||||
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
|
||||
>GitHub</a
|
||||
>
|
||||
下载最新版本
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import { checkUpdate } from '@/utils';
|
||||
import config from '../../../../package.json';
|
||||
|
||||
// 配置 marked
|
||||
marked.setOptions({
|
||||
breaks: true, // 支持 GitHub 风格的换行
|
||||
gfm: true // 启用 GitHub 风格的 Markdown
|
||||
});
|
||||
|
||||
interface ReleaseInfo {
|
||||
tag_name: string;
|
||||
body?: string;
|
||||
html_url: string;
|
||||
assets: Array<{
|
||||
browser_download_url: string;
|
||||
name: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const showModal = ref(false);
|
||||
const noPrompt = ref(false);
|
||||
const updateInfo = ref({
|
||||
hasUpdate: false,
|
||||
latestVersion: '',
|
||||
currentVersion: config.version,
|
||||
releaseInfo: null as ReleaseInfo | null
|
||||
});
|
||||
|
||||
// 解析 Markdown
|
||||
const parsedReleaseNotes = computed(() => {
|
||||
if (!updateInfo.value.releaseInfo?.body) return '';
|
||||
try {
|
||||
return marked.parse(updateInfo.value.releaseInfo.body);
|
||||
} catch (error) {
|
||||
console.error('Error parsing markdown:', error);
|
||||
return updateInfo.value.releaseInfo.body;
|
||||
}
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
if (noPrompt.value) {
|
||||
localStorage.setItem('updatePromptDismissed', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
const result = await checkUpdate();
|
||||
updateInfo.value = result;
|
||||
// 如果有更新且用户没有选择不再提示,则显示弹窗
|
||||
if (result.hasUpdate && localStorage.getItem('updatePromptDismissed') !== 'true') {
|
||||
showModal.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
const { userAgent } = navigator;
|
||||
const isMac: boolean = userAgent.includes('Mac');
|
||||
const isWindows: boolean = userAgent.includes('Win');
|
||||
const isARM: boolean =
|
||||
userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
|
||||
const isX64: boolean =
|
||||
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
|
||||
const isX86: boolean =
|
||||
!isX64 &&
|
||||
(userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
|
||||
|
||||
const getDownloadUrl = (os: string, arch: string): string => {
|
||||
const version = updateInfo.value.latestVersion;
|
||||
const setup = os !== 'mac' ? 'Setup_' : '';
|
||||
return `https://gh.llkk.cc/https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusic_${version}_${setup}${arch}.${os === 'mac' ? 'dmg' : 'exe'}`;
|
||||
};
|
||||
|
||||
const osType: string | null = isMac ? 'mac' : isWindows ? 'windows' : null;
|
||||
const archType: string | null = isARM ? 'arm64' : isX64 ? 'x64' : isX86 ? 'x86' : null;
|
||||
|
||||
const downloadUrl: string | null = osType && archType ? getDownloadUrl(osType, archType) : null;
|
||||
window.open(downloadUrl || 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
|
||||
closeModal();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkForUpdates();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.update-app-modal {
|
||||
:deep(.n-modal) {
|
||||
@apply max-w-4xl;
|
||||
}
|
||||
.modal-content {
|
||||
@apply p-6 pb-4;
|
||||
.modal-header {
|
||||
@apply flex items-center mb-6;
|
||||
.app-icon {
|
||||
@apply w-24 h-24 mr-6 rounded-2xl overflow-hidden;
|
||||
img {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
}
|
||||
.app-info {
|
||||
@apply flex-1;
|
||||
.app-name {
|
||||
@apply text-2xl font-bold mb-2;
|
||||
}
|
||||
.app-desc {
|
||||
@apply text-base text-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
.update-info {
|
||||
@apply mb-6 rounded-lg bg-gray-50 dark:bg-gray-800;
|
||||
.update-title {
|
||||
@apply text-base font-medium p-4 pb-2;
|
||||
}
|
||||
.update-body {
|
||||
@apply p-4 pt-2 text-gray-600 dark:text-gray-300;
|
||||
|
||||
:deep(h1) {
|
||||
@apply text-xl font-bold mb-3;
|
||||
}
|
||||
:deep(h2) {
|
||||
@apply text-lg font-bold mb-3;
|
||||
}
|
||||
:deep(h3) {
|
||||
@apply text-base font-bold mb-2;
|
||||
}
|
||||
:deep(p) {
|
||||
@apply mb-3 leading-relaxed;
|
||||
}
|
||||
:deep(ul) {
|
||||
@apply list-disc list-inside mb-3;
|
||||
}
|
||||
:deep(ol) {
|
||||
@apply list-decimal list-inside mb-3;
|
||||
}
|
||||
:deep(li) {
|
||||
@apply mb-2 leading-relaxed;
|
||||
}
|
||||
:deep(code) {
|
||||
@apply px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200;
|
||||
}
|
||||
:deep(pre) {
|
||||
@apply p-3 rounded bg-gray-100 dark:bg-gray-700 overflow-x-auto mb-3;
|
||||
code {
|
||||
@apply bg-transparent p-0;
|
||||
}
|
||||
}
|
||||
:deep(blockquote) {
|
||||
@apply pl-4 border-l-4 border-gray-200 dark:border-gray-600 mb-3;
|
||||
}
|
||||
:deep(a) {
|
||||
@apply text-green-500 hover:text-green-600 dark:hover:text-green-400;
|
||||
}
|
||||
:deep(hr) {
|
||||
@apply my-4 border-gray-200 dark:border-gray-600;
|
||||
}
|
||||
:deep(table) {
|
||||
@apply w-full mb-3;
|
||||
th, td {
|
||||
@apply px-3 py-2 border border-gray-200 dark:border-gray-600;
|
||||
}
|
||||
th {
|
||||
@apply bg-gray-100 dark:bg-gray-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-actions {
|
||||
@apply flex gap-4 mt-6;
|
||||
.n-button {
|
||||
@apply flex-1 text-base py-2;
|
||||
}
|
||||
.cancel-btn {
|
||||
@apply bg-gray-800 text-gray-300 border-none;
|
||||
&:hover {
|
||||
@apply bg-gray-700;
|
||||
}
|
||||
}
|
||||
.update-btn {
|
||||
@apply bg-green-600 border-none;
|
||||
&:hover {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user