mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-28 19:07:23 +08:00
✨ feat: 优化歌词进度 添加下载 优化播放 优化历史记录
This commit is contained in:
Vendored
+2
@@ -7,6 +7,7 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
InstallAppModal: typeof import('./src/components/common/InstallAppModal.vue')['default']
|
||||||
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
MPop: typeof import('./src/components/common/MPop.vue')['default']
|
||||||
MusicList: typeof import('./src/components/MusicList.vue')['default']
|
MusicList: typeof import('./src/components/MusicList.vue')['default']
|
||||||
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
|
MvPlayer: typeof import('./src/components/MvPlayer.vue')['default']
|
||||||
@@ -21,6 +22,7 @@ declare module 'vue' {
|
|||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NPopover: typeof import('naive-ui')['NPopover']
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
|
|||||||
+6
-5
@@ -13,7 +13,8 @@
|
|||||||
"b:win:x64": "cross-env NODE_ENV=production electron-builder --config ./build/win64.json",
|
"b:win:x64": "cross-env NODE_ENV=production electron-builder --config ./build/win64.json",
|
||||||
"b:win:x86": "cross-env NODE_ENV=production electron-builder --config ./build/win32.json",
|
"b:win:x86": "cross-env NODE_ENV=production electron-builder --config ./build/win32.json",
|
||||||
"b:win:arm": "cross-env NODE_ENV=production electron-builder --config ./build/winarm64.json",
|
"b:win:arm": "cross-env NODE_ENV=production electron-builder --config ./build/winarm64.json",
|
||||||
"b:mac": "cross-env NODE_ENV=production electron-builder --config ./build/mac.json"
|
"b:mac": "cross-env NODE_ENV=production electron-builder --config ./build/mac.json",
|
||||||
|
"b:win": "cross-env NODE_ENV=production vite build && npm run b:win:x64 && npm run b:win:x86 && npm run b:win:arm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-store": "^8.1.0"
|
"electron-store": "^8.1.0"
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"@vueuse/electron": "^11.0.3",
|
"@vueuse/electron": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"electron": "^32.0.1",
|
"electron": "^32.0.1",
|
||||||
"electron-builder": "^25.0.5",
|
"electron-builder": "^25.0.5",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
@@ -42,11 +44,11 @@
|
|||||||
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
"eslint-plugin-vue-scoped-css": "^2.7.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"naive-ui": "^2.39.0",
|
"naive-ui": "^2.39.0",
|
||||||
"postcss": "^8.4.44",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"remixicon": "^4.2.0",
|
"remixicon": "^4.2.0",
|
||||||
"sass": "^1.78.0",
|
"sass": "^1.78.0",
|
||||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
"tailwindcss": "^3.4.15",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"unplugin-auto-import": "^0.18.2",
|
"unplugin-auto-import": "^0.18.2",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-vue-components": "^0.27.4",
|
||||||
@@ -57,7 +59,6 @@
|
|||||||
"vue": "^3.5.0",
|
"vue": "^3.5.0",
|
||||||
"vue-router": "^4.4.3",
|
"vue-router": "^4.4.3",
|
||||||
"vue-tsc": "^2.1.4",
|
"vue-tsc": "^2.1.4",
|
||||||
"vuex": "^4.1.0",
|
"vuex": "^4.1.0"
|
||||||
"cross-env": "^7.0.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-10
@@ -1,24 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app" :class="isMobile ? 'mobile' : ''">
|
<div class="app-container" :class="{ mobile: isMobile }">
|
||||||
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
|
|
||||||
<n-config-provider :theme="darkTheme">
|
<n-config-provider :theme="darkTheme">
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
|
<install-app-modal></install-app-modal>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import { darkTheme } from 'naive-ui';
|
import { darkTheme } from 'naive-ui';
|
||||||
|
|
||||||
|
import InstallAppModal from '@/components/common/InstallAppModal.vue';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
|
|
||||||
import { isMobile } from './utils';
|
import { isMobile } from './utils';
|
||||||
|
|
||||||
const playMusicUrl = computed(() => store.state.playMusicUrl as string);
|
|
||||||
// 是否播放
|
|
||||||
const play = computed(() => store.state.play as boolean);
|
|
||||||
const windowData = window as any;
|
const windowData = window as any;
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (windowData.electron) {
|
if (windowData.electron) {
|
||||||
@@ -29,10 +27,8 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
div {
|
.app-container {
|
||||||
box-sizing: border-box;
|
@apply h-full w-full;
|
||||||
}
|
|
||||||
.app {
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<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</h2>
|
||||||
|
<p class="app-desc">在桌面安装应用,获得更好的体验</p>
|
||||||
|
</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>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const showModal = ref(false);
|
||||||
|
const isElectron = ref((window as any).electron !== undefined);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
showModal.value = false;
|
||||||
|
localStorage.setItem('installPromptDismissed', 'true');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
// 新页面打开
|
||||||
|
// 识别当前环境是 mac 还是 windows
|
||||||
|
const os = navigator.platform;
|
||||||
|
const isMac = os.includes('Mac');
|
||||||
|
const isWindows = os.includes('Win');
|
||||||
|
const urls = {
|
||||||
|
mac: 'http://file.alger.fun/d/ali/%E8%BD%AF%E4%BB%B6/AlgerMusic/AlgerMusic.dmg',
|
||||||
|
windows: 'http://file.alger.fun/d/ali/%E8%BD%AF%E4%BB%B6/AlgerMusic/AlgerMusic_2.1.0_Setup_x64.exe',
|
||||||
|
};
|
||||||
|
// 根据操作系统选择下载链接
|
||||||
|
let downloadUrl = '';
|
||||||
|
if (isMac) {
|
||||||
|
downloadUrl = urls.mac;
|
||||||
|
} else if (isWindows) {
|
||||||
|
downloadUrl = urls.windows;
|
||||||
|
}
|
||||||
|
if (downloadUrl) {
|
||||||
|
window.open(downloadUrl, '_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果是 electron 环境,不显示安装提示
|
||||||
|
if (isElectron.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经点击过"暂不安装"
|
||||||
|
const isDismissed = localStorage.getItem('installPromptDismissed') === 'true';
|
||||||
|
if (isDismissed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showModal.value = true;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.install-app-modal {
|
||||||
|
:deep(.n-modal) {
|
||||||
|
@apply max-w-sm;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
@apply p-4;
|
||||||
|
.modal-header {
|
||||||
|
@apply flex items-center mb-6;
|
||||||
|
.app-icon {
|
||||||
|
@apply w-16 h-16 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;
|
||||||
|
.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>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="song-item" :class="{ 'song-mini': mini }">
|
<div class="song-item" :class="{ 'song-mini': mini, 'song-list': list }">
|
||||||
<n-image
|
<n-image
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
ref="songImg"
|
ref="songImg"
|
||||||
@@ -12,18 +12,29 @@
|
|||||||
@load="imageLoad"
|
@load="imageLoad"
|
||||||
/>
|
/>
|
||||||
<div class="song-item-content">
|
<div class="song-item-content">
|
||||||
<div class="song-item-content-title">
|
<div v-if="list" class="song-item-content-wrapper">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
|
||||||
</div>
|
<div class="song-item-content-divider">-</div>
|
||||||
<div class="song-item-content-name">
|
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
|
||||||
<n-ellipsis class="text-ellipsis" line-clamp="1">
|
|
||||||
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
|
||||||
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
|
||||||
>
|
>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</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>
|
||||||
<div class="song-item-operating">
|
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
|
||||||
<div class="song-item-operating-like">
|
<div class="song-item-operating-like">
|
||||||
<i class="iconfont icon-likefill"></i>
|
<i class="iconfont icon-likefill"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,9 +62,11 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
item: SongResult;
|
item: SongResult;
|
||||||
mini?: boolean;
|
mini?: boolean;
|
||||||
|
list?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
mini: false,
|
mini: false,
|
||||||
|
list: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -175,4 +188,41 @@ const playMusicEvent = async (item: SongResult) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.song-list {
|
||||||
|
@apply p-2 rounded-lg hover:bg-gray-800/50 border border-gray-800/50 mb-2;
|
||||||
|
.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 text-white flex-shrink-0 max-w-[45%];
|
||||||
|
}
|
||||||
|
&-divider {
|
||||||
|
@apply mx-2 text-gray-400;
|
||||||
|
}
|
||||||
|
&-name {
|
||||||
|
@apply text-gray-400 flex-1 min-w-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.song-item-operating {
|
||||||
|
@apply flex items-center gap-2;
|
||||||
|
&-like {
|
||||||
|
@apply cursor-pointer hover:scale-110 transition-transform;
|
||||||
|
.iconfont {
|
||||||
|
@apply text-base 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>
|
</style>
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ const getSongUrl = async (id: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getSongDetail = async (playMusic: SongResult) => {
|
const getSongDetail = async (playMusic: SongResult) => {
|
||||||
if (playMusic.playMusicUrl) {
|
|
||||||
return playMusic;
|
|
||||||
}
|
|
||||||
playMusic.playLoading = true;
|
playMusic.playLoading = true;
|
||||||
const playMusicUrl = await getSongUrl(playMusic.id);
|
const playMusicUrl = await getSongUrl(playMusic.id);
|
||||||
const { backgroundColor, primaryColor } =
|
const { backgroundColor, primaryColor } =
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
:id="`music-lrc-text-${index}`"
|
:id="`music-lrc-text-${index}`"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="music-lrc-text"
|
class="music-lrc-text"
|
||||||
:class="{ 'now-text': index === nowIndex }"
|
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
|
||||||
@click="setAudioTime(index, audio)"
|
@click="setAudioTime(index, audio)"
|
||||||
>
|
>
|
||||||
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||||
@@ -259,11 +259,19 @@ defineExpose({
|
|||||||
|
|
||||||
span {
|
span {
|
||||||
padding-right: 100px;
|
padding-right: 100px;
|
||||||
display: inline-block;
|
// display: inline-block;
|
||||||
background-clip: text !important;
|
background-clip: text !important;
|
||||||
-webkit-background-clip: text !important;
|
-webkit-background-clip: text !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-tr {
|
||||||
|
@apply font-normal;
|
||||||
|
opacity: 0.7;
|
||||||
|
color: var(--text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-text {
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply font-bold opacity-100 rounded-xl;
|
@apply font-bold opacity-100 rounded-xl;
|
||||||
background-color: var(--hover-bg-color);
|
background-color: var(--hover-bg-color);
|
||||||
@@ -272,12 +280,6 @@ defineExpose({
|
|||||||
color: var(--text-color-active) !important;
|
color: var(--text-color-active) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-tr {
|
|
||||||
@apply font-normal;
|
|
||||||
opacity: 0.7;
|
|
||||||
color: var(--text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
:class="setAnimationClass('animate__bounceIn')"
|
:class="setAnimationClass('animate__bounceIn')"
|
||||||
:style="setAnimationDelay(index, 30)"
|
:style="setAnimationDelay(index, 30)"
|
||||||
>
|
>
|
||||||
<song-item class="history-item-content" :item="item" @play="handlePlay" />
|
<song-item class="history-item-content" :item="item" list @play="handlePlay" />
|
||||||
<div class="history-item-count">
|
<div class="history-item-count min-w-[60px]">
|
||||||
{{ item.count }}
|
{{ item.count }}
|
||||||
</div>
|
</div>
|
||||||
<div class="history-item-delete">
|
<div class="history-item-delete">
|
||||||
@@ -56,7 +56,7 @@ const handlePlay = () => {
|
|||||||
@apply flex-1;
|
@apply flex-1;
|
||||||
}
|
}
|
||||||
&-count {
|
&-count {
|
||||||
@apply px-4 text-lg;
|
@apply px-4 text-lg text-center;
|
||||||
}
|
}
|
||||||
&-delete {
|
&-delete {
|
||||||
@apply cursor-pointer rounded-full border-2 border-gray-400 w-8 h-8 flex justify-center items-center;
|
@apply cursor-pointer rounded-full border-2 border-gray-400 w-8 h-8 flex justify-center items-center;
|
||||||
|
|||||||
+2
-5
@@ -1,11 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
darkMode: false, // or 'media' or 'class'
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
variants: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user