mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-06 16:40:50 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f728191a8f | ||
|
|
dfa8b51a53 | ||
|
|
b2c13121fd | ||
|
|
d28adb61a4 | ||
|
|
9a7d5a3834 | ||
|
|
2037798fbe | ||
|
|
85bd0ad015 | ||
|
|
e1557a51a3 | ||
|
|
1ecc6f136f | ||
|
|
53b3061b03 | ||
|
|
3d2f6a2330 | ||
|
|
3b1470f28f | ||
|
|
100268448a | ||
|
|
51f67bb2c2 | ||
|
|
7be126cf5f | ||
|
|
f2f5d3ac15 | ||
|
|
34c45e0105 | ||
|
|
f9333f5f78 |
@@ -39,6 +39,7 @@
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"no-nested-ternary": "off",
|
||||
"no-console": "off",
|
||||
"no-continue": "off",
|
||||
"no-restricted-syntax": "off",
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
## 预览地址
|
||||
[http://mc.alger.fun/](http://mc.alger.fun/)
|
||||
|
||||
QQ群:789288579
|
||||
|
||||
## 软件截图
|
||||

|
||||

|
||||
|
||||
2
app.js
2
app.js
@@ -66,7 +66,7 @@ function createWindow() {
|
||||
store.set('set', setJson);
|
||||
}
|
||||
|
||||
loadLyricWindow(ipcMain);
|
||||
loadLyricWindow(ipcMain, mainWin);
|
||||
}
|
||||
|
||||
// 限制只能启动一个应用
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { BrowserWindow, screen } = require('electron');
|
||||
const path = require('path');
|
||||
const Store = require('electron-store');
|
||||
const config = require('./config');
|
||||
|
||||
const store = new Store();
|
||||
let lyricWindow = null;
|
||||
|
||||
const createWin = () => {
|
||||
console.log('Creating lyric window');
|
||||
|
||||
// 获取保存的窗口位置
|
||||
const windowBounds = store.get('lyricWindowBounds') || {};
|
||||
const { x, y, width, height } = windowBounds;
|
||||
|
||||
// 获取屏幕尺寸
|
||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
// 验证保存的位置是否有效
|
||||
const validPosition = x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
|
||||
|
||||
lyricWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 300,
|
||||
width: width || 800,
|
||||
height: height || 200,
|
||||
x: validPosition ? x : undefined,
|
||||
y: validPosition ? y : undefined,
|
||||
frame: false,
|
||||
show: false,
|
||||
transparent: true,
|
||||
hasShadow: false,
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
@@ -19,16 +36,26 @@ const createWin = () => {
|
||||
webSecurity: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 监听窗口关闭事件
|
||||
lyricWindow.on('closed', () => {
|
||||
console.log('Lyric window closed');
|
||||
lyricWindow = null;
|
||||
});
|
||||
};
|
||||
|
||||
const loadLyricWindow = (ipcMain) => {
|
||||
const loadLyricWindow = (ipcMain, mainWin) => {
|
||||
ipcMain.on('open-lyric', () => {
|
||||
console.log('Received open-lyric request');
|
||||
if (lyricWindow) {
|
||||
console.log('Lyric window exists, focusing');
|
||||
if (lyricWindow.isMinimized()) lyricWindow.restore();
|
||||
lyricWindow.focus();
|
||||
lyricWindow.show();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Creating new lyric window');
|
||||
createWin();
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
lyricWindow.webContents.openDevTools({ mode: 'detach' });
|
||||
@@ -39,26 +66,39 @@ const loadLyricWindow = (ipcMain) => {
|
||||
}
|
||||
|
||||
lyricWindow.setMinimumSize(600, 200);
|
||||
|
||||
// 隐藏任务栏
|
||||
lyricWindow.setSkipTaskbar(true);
|
||||
|
||||
lyricWindow.show();
|
||||
lyricWindow.once('ready-to-show', () => {
|
||||
console.log('Lyric window ready to show');
|
||||
lyricWindow.show();
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('send-lyric', (e, data) => {
|
||||
if (lyricWindow) {
|
||||
lyricWindow.webContents.send('receive-lyric', data);
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
try {
|
||||
lyricWindow.webContents.send('receive-lyric', data);
|
||||
} catch (error) {
|
||||
console.error('Error processing lyric data:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('Cannot send lyric: window not available or destroyed');
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('top-lyric', (e, data) => {
|
||||
lyricWindow.setAlwaysOnTop(data);
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
lyricWindow.setAlwaysOnTop(data);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('close-lyric', () => {
|
||||
lyricWindow.close();
|
||||
lyricWindow = null;
|
||||
if (lyricWindow && !lyricWindow.isDestroyed()) {
|
||||
lyricWindow.webContents.send('lyric-window-close');
|
||||
mainWin.webContents.send('lyric-control-back', 'close');
|
||||
lyricWindow.close();
|
||||
lyricWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('mouseenter-lyric', () => {
|
||||
@@ -68,6 +108,47 @@ const loadLyricWindow = (ipcMain) => {
|
||||
ipcMain.on('mouseleave-lyric', () => {
|
||||
lyricWindow.setIgnoreMouseEvents(false);
|
||||
});
|
||||
|
||||
// 处理拖动移动
|
||||
ipcMain.on('lyric-drag-move', (e, { deltaX, deltaY }) => {
|
||||
if (!lyricWindow) return;
|
||||
|
||||
const [currentX, currentY] = lyricWindow.getPosition();
|
||||
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
|
||||
const [windowWidth, windowHeight] = lyricWindow.getSize();
|
||||
|
||||
// 计算新位置,确保窗口不会移出屏幕
|
||||
const newX = Math.max(0, Math.min(currentX + deltaX, screenWidth - windowWidth));
|
||||
const newY = Math.max(0, Math.min(currentY + deltaY, screenHeight - windowHeight));
|
||||
|
||||
lyricWindow.setPosition(newX, newY);
|
||||
|
||||
// 保存新位置
|
||||
store.set('lyricWindowBounds', {
|
||||
...lyricWindow.getBounds(),
|
||||
x: newX,
|
||||
y: newY,
|
||||
});
|
||||
});
|
||||
|
||||
// 添加鼠标穿透事件处理
|
||||
ipcMain.on('set-ignore-mouse', (e, shouldIgnore) => {
|
||||
if (!lyricWindow) return;
|
||||
|
||||
if (shouldIgnore) {
|
||||
// 设置鼠标穿透,但保留拖动区域可交互
|
||||
lyricWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
// 取消鼠标穿透
|
||||
lyricWindow.setIgnoreMouseEvents(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加播放控制处理
|
||||
ipcMain.on('control-back', (e, command) => {
|
||||
console.log('Received control-back request:', command);
|
||||
mainWin.webContents.send('lyric-control-back', command);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
||||
46
index.html
46
index.html
@@ -5,16 +5,17 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
|
||||
<!-- SEO 元数据 -->
|
||||
<title>网抑云音乐 | AlgerKong | AlgerMusicPlayer</title>
|
||||
<meta name="description" content="AlgerMusicPlayer 网抑云音乐 基于 网易云音乐API 的一款免费的在线音乐播放器,支持在线播放、歌词显示、音乐下载等功能。提供海量音乐资源,让您随时随地享受音乐。" />
|
||||
<meta name="description"
|
||||
content="AlgerMusicPlayer 网抑云音乐 基于 网易云音乐API 的一款免费的在线音乐播放器,支持在线播放、歌词显示、音乐下载等功能。提供海量音乐资源,让您随时随地享受音乐。" />
|
||||
<meta name="keywords" content="AlgerMusic, AlgerMusicPlayer, 网抑云, 音乐播放器, 在线音乐, 免费音乐, 歌词显示, 音乐下载, AlgerKong, 网易云音乐" />
|
||||
|
||||
<!-- 作者信息 -->
|
||||
<meta name="author" content="AlgerKong" />
|
||||
<meta name="author-url" content="https://github.com/algerkong" />
|
||||
|
||||
|
||||
<!-- PWA 相关 -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
@@ -22,12 +23,12 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="网抑云音乐" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
|
||||
<!-- 资源预加载 -->
|
||||
<link rel="preload" href="/icon/iconfont.css" as="style" />
|
||||
<link rel="preload" href="/css/animate.css" as="style" />
|
||||
<link rel="preload" href="/css/base.css" as="style" />
|
||||
|
||||
|
||||
<!-- 样式表 -->
|
||||
<link rel="stylesheet" href="/icon/iconfont.css" />
|
||||
<link rel="stylesheet" href="/css/animate.css" />
|
||||
@@ -45,37 +46,12 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<div style="display: none;">
|
||||
Total Page View <span id="vercount_value_page_pv">Loading</span>
|
||||
Total Visits <span id="vercount_value_site_pv">Loading</span>
|
||||
Site Total Visitors <span id="vercount_value_site_uv">Loading</span>
|
||||
</div>
|
||||
<!-- 收款码图片预加载 -->
|
||||
<link rel="preload" as="image" href="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" />
|
||||
<link rel="preload" as="image" href="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" />
|
||||
<div style="display: none;">
|
||||
Total Page View <span id="vercount_value_page_pv">Loading</span>
|
||||
Total Visits <span id="vercount_value_site_pv">Loading</span>
|
||||
Site Total Visitors <span id="vercount_value_site_uv">Loading</span>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
<!-- 结构化数据 -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "网抑云音乐",
|
||||
"applicationCategory": "MultimediaApplication",
|
||||
"operatingSystem": "Web, Windows, MacOS",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "AlgerKong",
|
||||
"url": "https://github.com/algerkong"
|
||||
},
|
||||
"description": "一款免费的在线音乐播放器,支持在线播放、歌词显示、音乐下载等功能。",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "CNY"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "alger-music",
|
||||
"version": "2.1.0",
|
||||
"version": "2.4.0",
|
||||
"description": "这是一个用于音乐播放的应用程序。",
|
||||
"author": "Alger <algerkc@qq.com>",
|
||||
"main": "app.js",
|
||||
|
||||
14
src/App.vue
14
src/App.vue
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div class="app-container" :class="{ mobile: isMobile }">
|
||||
<div class="app-container" :class="{ mobile: isMobile, noElectron: !isElectron }">
|
||||
<n-config-provider :theme="darkTheme">
|
||||
<n-dialog-provider>
|
||||
<router-view></router-view>
|
||||
<n-message-provider>
|
||||
<router-view></router-view>
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
</div>
|
||||
@@ -12,12 +14,20 @@
|
||||
import { darkTheme } from 'naive-ui';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import { isElectron } from '@/hooks/MusicHook';
|
||||
import homeRouter from '@/router/home';
|
||||
import store from '@/store';
|
||||
|
||||
import { isMobile } from './utils';
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('initializeSettings');
|
||||
if (isMobile.value) {
|
||||
store.commit(
|
||||
'setMenus',
|
||||
homeRouter.filter((item) => item.meta.isMobile),
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,15 +2,27 @@ import { IData } from '@/type';
|
||||
import { IMvItem, IMvUrlData } from '@/type/mv';
|
||||
import request from '@/utils/request';
|
||||
|
||||
interface MvParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
area?: string;
|
||||
}
|
||||
|
||||
// 获取 mv 排行
|
||||
export const getTopMv = (limit = 30, offset = 0) => {
|
||||
export const getTopMv = (params: MvParams) => {
|
||||
return request({
|
||||
url: '/mv/all',
|
||||
method: 'get',
|
||||
params: {
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
// 获取所有mv
|
||||
export const getAllMv = (params: MvParams) => {
|
||||
return request({
|
||||
url: '/mv/all',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
BIN
src/assets/alipay.png
Normal file
BIN
src/assets/alipay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/wechat.png
Normal file
BIN
src/assets/wechat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -13,16 +13,22 @@
|
||||
</template>
|
||||
|
||||
<div class="p-6 bg-black rounded-lg shadow-lg">
|
||||
<div class="flex gap-6">
|
||||
<div class="flex gap-10">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<n-image :src="alipayQR" alt="支付宝收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||
<n-image :src="alipayQR" alt="支付宝收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
|
||||
<span class="text-sm text-gray-100">支付宝</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
|
||||
<span class="text-sm text-gray-100">微信支付</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-100 text-center cursor-pointer hover:text-green-500" @click="copyQQ">
|
||||
QQ群:789288579
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
@@ -31,6 +37,12 @@
|
||||
<script setup>
|
||||
import { NButton, NImage, NPopover } from 'naive-ui';
|
||||
|
||||
const message = useMessage();
|
||||
const copyQQ = () => {
|
||||
navigator.clipboard.writeText('789288579');
|
||||
message.success('已复制到剪贴板');
|
||||
};
|
||||
|
||||
defineProps({
|
||||
alipayQR: {
|
||||
type: String,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<n-drawer
|
||||
:show="show"
|
||||
:height="isMobile ? '100vh' : '80vh'"
|
||||
:height="isMobile ? '100%' : '80%'"
|
||||
placement="bottom"
|
||||
block-scroll
|
||||
mask-closable
|
||||
:style="{ backgroundColor: 'transparent' }"
|
||||
:to="`#layout-main`"
|
||||
@mask-click="close"
|
||||
>
|
||||
<div class="music-page">
|
||||
@@ -24,7 +25,7 @@
|
||||
<div class="music-info">
|
||||
<div class="music-cover">
|
||||
<n-image
|
||||
:src="getImgUrl(listInfo?.coverImgUrl, '300y300')"
|
||||
:src="getImgUrl(cover ? listInfo?.coverImgUrl : displayedSongs[0]?.picUrl, '300y300')"
|
||||
class="cover-img"
|
||||
preview-disabled
|
||||
:class="setAnimationClass('animate__fadeIn')"
|
||||
@@ -46,21 +47,23 @@
|
||||
|
||||
<!-- 右侧歌曲列表 -->
|
||||
<div class="music-list-container">
|
||||
<div v-loading="loading" class="music-list">
|
||||
<div class="music-list">
|
||||
<n-scrollbar @scroll="handleScroll">
|
||||
<div v-loading="loading || !songList.length" class="music-list-content">
|
||||
<div
|
||||
v-for="(item, index) in displayedSongs"
|
||||
:key="item.id"
|
||||
class="double-item"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
:style="getItemAnimationDelay(index)"
|
||||
>
|
||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||
<n-spin :show="loadingList || loading">
|
||||
<div class="music-list-content">
|
||||
<div
|
||||
v-for="(item, index) in displayedSongs"
|
||||
:key="item.id"
|
||||
class="double-item"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
:style="getItemAnimationDelay(index)"
|
||||
>
|
||||
<song-item :item="formatDetail(item)" @play="handlePlay" />
|
||||
</div>
|
||||
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
|
||||
<play-bottom />
|
||||
</div>
|
||||
<div v-if="isLoadingMore" class="loading-more">加载更多...</div>
|
||||
<play-bottom />
|
||||
</div>
|
||||
</n-spin>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
<play-bottom />
|
||||
@@ -81,22 +84,31 @@ import PlayBottom from './common/PlayBottom.vue';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
name: string;
|
||||
songList: any[];
|
||||
loading?: boolean;
|
||||
listInfo?: {
|
||||
trackIds: { id: number }[];
|
||||
[key: string]: any;
|
||||
};
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
name: string;
|
||||
songList: any[];
|
||||
loading?: boolean;
|
||||
listInfo?: {
|
||||
trackIds: { id: number }[];
|
||||
[key: string]: any;
|
||||
};
|
||||
cover?: boolean;
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
cover: true,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:show', 'update:loading']);
|
||||
|
||||
const page = ref(0);
|
||||
const pageSize = 20;
|
||||
const isLoadingMore = ref(false);
|
||||
const displayedSongs = ref<any[]>([]);
|
||||
const loadingList = ref(false);
|
||||
|
||||
// 计算总数
|
||||
const total = computed(() => {
|
||||
@@ -165,6 +177,7 @@ const loadMoreSongs = async () => {
|
||||
console.error('加载歌曲失败:', error);
|
||||
} finally {
|
||||
isLoadingMore.value = false;
|
||||
loadingList.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,6 +197,16 @@ const handleScroll = (e: Event) => {
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newVal) => {
|
||||
loadingList.value = newVal;
|
||||
if (!props.cover) {
|
||||
loadingList.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 songList 变化,重置分页状态
|
||||
watch(
|
||||
() => props.songList,
|
||||
@@ -193,6 +216,7 @@ watch(
|
||||
if (newSongs.length > pageSize) {
|
||||
page.value = 1;
|
||||
}
|
||||
loadingList.value = false;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -253,6 +277,10 @@ watch(
|
||||
&-list {
|
||||
@apply flex-grow min-h-0;
|
||||
|
||||
&-content {
|
||||
@apply min-h-[calc(80vh-60px)];
|
||||
}
|
||||
|
||||
:deep(.n-virtual-list__scroll) {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-drawer :show="show" height="100vh" placement="bottom" :z-index="999999999">
|
||||
<n-drawer :show="show" height="100%" placement="bottom" :z-index="999999999" :to="`#layout-main`">
|
||||
<div class="mv-detail">
|
||||
<div ref="videoContainerRef" class="video-container" :class="{ 'cursor-hidden': !showCursor }">
|
||||
<video
|
||||
@@ -553,8 +553,8 @@ const isMobile = computed(() => store.state.isMobile);
|
||||
|
||||
// 添加横屏模式支持
|
||||
@media screen and (orientation: landscape) {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
@@ -617,8 +617,8 @@ const isMobile = computed(() => store.state.isMobile);
|
||||
&:-moz-full-screen,
|
||||
&:-ms-fullscreen {
|
||||
background: black;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// 确保全屏时标题栏正确显示
|
||||
.mv-detail-title {
|
||||
|
||||
@@ -20,7 +20,14 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" />
|
||||
<MusicList
|
||||
v-model:show="showMusic"
|
||||
:name="albumName"
|
||||
:song-list="songList"
|
||||
:cover="false"
|
||||
:loading="loadingList"
|
||||
:list-info="albumInfo"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,15 +48,28 @@ const loadAlbumList = async () => {
|
||||
const showMusic = ref(false);
|
||||
const songList = ref([]);
|
||||
const albumName = ref('');
|
||||
|
||||
const loadingList = ref(false);
|
||||
const albumInfo = ref<any>({});
|
||||
const handleClick = async (item: any) => {
|
||||
songList.value = [];
|
||||
albumInfo.value = {};
|
||||
albumName.value = item.name;
|
||||
loadingList.value = true;
|
||||
showMusic.value = true;
|
||||
const res = await getAlbum(item.id);
|
||||
songList.value = res.data.songs.map((song: any) => {
|
||||
song.al.picUrl = song.al.picUrl || item.picUrl;
|
||||
return song;
|
||||
});
|
||||
albumInfo.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,
|
||||
};
|
||||
loadingList.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
v-model:show="showMusic"
|
||||
name="每日推荐列表"
|
||||
:song-list="dayRecommendData?.dailySongs"
|
||||
:cover="false"
|
||||
/>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<img src="@/assets/logo.png" alt="App Icon" />
|
||||
</div>
|
||||
<div class="app-info">
|
||||
<h2 class="app-name">Alger Music</h2>
|
||||
<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>
|
||||
@@ -15,6 +15,15 @@
|
||||
<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>
|
||||
@@ -22,6 +31,9 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import config from '@/../package.json';
|
||||
import { isMobile } from '@/utils';
|
||||
|
||||
const showModal = ref(false);
|
||||
const isElectron = ref((window as any).electron !== undefined);
|
||||
const noPrompt = ref(false);
|
||||
@@ -33,32 +45,9 @@ const closeModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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.exe',
|
||||
// };
|
||||
// // 根据操作系统选择下载链接
|
||||
// let downloadUrl = '';
|
||||
// if (isMac) {
|
||||
// downloadUrl = urls.mac;
|
||||
// } else if (isWindows) {
|
||||
// downloadUrl = urls.windows;
|
||||
// }
|
||||
const downloadUrl = 'https://github.com/algerkong/AlgerMusicPlayer/releases';
|
||||
if (downloadUrl) {
|
||||
window.open(downloadUrl, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 如果是 electron 环境,不显示安装提示
|
||||
if (isElectron.value) {
|
||||
if (isElectron.value || isMobile.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,6 +58,29 @@ onMounted(() => {
|
||||
}
|
||||
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>
|
||||
@@ -77,11 +89,11 @@ onMounted(() => {
|
||||
@apply max-w-sm;
|
||||
}
|
||||
.modal-content {
|
||||
@apply p-4;
|
||||
@apply p-4 pb-0;
|
||||
.modal-header {
|
||||
@apply flex items-center mb-6;
|
||||
.app-icon {
|
||||
@apply w-16 h-16 mr-4 rounded-2xl overflow-hidden;
|
||||
@apply w-20 h-20 mr-4 rounded-2xl overflow-hidden;
|
||||
img {
|
||||
@apply w-full h-full object-cover;
|
||||
}
|
||||
@@ -97,7 +109,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
.modal-actions {
|
||||
@apply flex gap-3;
|
||||
@apply flex gap-3 mt-4;
|
||||
.n-button {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,15 @@ export const correctionTime = ref(0.4); // 歌词矫正时间Correction time
|
||||
export const currentLrcProgress = ref(0); // 来存储当前歌词的进度
|
||||
export const playMusic = computed(() => store.state.playMusic as SongResult); // 当前播放歌曲
|
||||
export const sound = ref<Howl | null>(audioService.getCurrentSound());
|
||||
export const isLyricWindowOpen = ref(false); // 新增状态
|
||||
|
||||
document.onkeyup = (e) => {
|
||||
// 检查事件目标是否是输入框元素
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
if (store.state.play) {
|
||||
@@ -47,13 +54,18 @@ watch(
|
||||
watch(
|
||||
() => store.state.playMusic,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
nextTick(async () => {
|
||||
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
|
||||
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
|
||||
// 当歌词数据更新时,如果歌词窗口打开,则发送数据
|
||||
if (isElectron.value && isLyricWindowOpen.value && lrcArray.value.length > 0) {
|
||||
sendLyricToWin();
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -70,8 +82,13 @@ export const audioServiceOn = (audio: typeof audioService) => {
|
||||
if (newIndex !== nowIndex.value) {
|
||||
nowIndex.value = newIndex;
|
||||
currentLrcProgress.value = 0;
|
||||
// 当歌词索引更新时,发送歌词数据
|
||||
if (isElectron.value && isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
}
|
||||
}
|
||||
if (isElectron.value) {
|
||||
// 定期发送歌词数据更新
|
||||
if (isElectron.value && isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
}
|
||||
}, 50);
|
||||
@@ -81,12 +98,21 @@ export const audioServiceOn = (audio: typeof audioService) => {
|
||||
audio.onPause(() => {
|
||||
store.commit('setPlayMusic', false);
|
||||
clearInterval(interval);
|
||||
// 暂停时也发送一次状态更新
|
||||
if (isElectron.value && isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听结束
|
||||
audio.onEnd(() => {
|
||||
handleEnded();
|
||||
store.commit('nextPlay');
|
||||
if (store.state.playMode === 1) {
|
||||
// 单曲循环模式
|
||||
audio.getCurrentSound()?.play();
|
||||
} else {
|
||||
// 列表循环模式
|
||||
store.commit('nextPlay');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -201,7 +227,7 @@ export const useLyricProgress = () => {
|
||||
};
|
||||
};
|
||||
|
||||
// 设置当前播放时间
|
||||
// 设置<EFBFBD><EFBFBD><EFBFBD>前播放时间
|
||||
export const setAudioTime = (index: number) => {
|
||||
const currentSound = sound.value;
|
||||
if (!currentSound) return;
|
||||
@@ -229,72 +255,33 @@ export const getLrcTimeRange = (index: number) => ({
|
||||
watch(
|
||||
() => lrcArray.value,
|
||||
(newLrcArray) => {
|
||||
if (newLrcArray.length > 0 && isElectron.value) {
|
||||
// 重新初始化歌词数据
|
||||
initLyricWindow();
|
||||
// 发送当前状态
|
||||
if (newLrcArray.length > 0 && isElectron.value && isLyricWindowOpen.value) {
|
||||
sendLyricToWin();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听播放状态变化
|
||||
watch(isPlaying, (newIsPlaying) => {
|
||||
if (isElectron.value) {
|
||||
sendLyricToWin(newIsPlaying);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理歌曲结束
|
||||
export const handleEnded = () => {
|
||||
if (isElectron.value) {
|
||||
setTimeout(() => {
|
||||
initLyricWindow();
|
||||
sendLyricToWin();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化歌词数据
|
||||
export const initLyricWindow = () => {
|
||||
if (!isElectron.value) return;
|
||||
try {
|
||||
if (lrcArray.value.length > 0) {
|
||||
console.log('Initializing lyric window with data:', {
|
||||
lrcArray: lrcArray.value,
|
||||
lrcTimeArray: lrcTimeArray.value,
|
||||
allTime: allTime.value,
|
||||
});
|
||||
|
||||
const staticData = {
|
||||
type: 'init',
|
||||
lrcArray: lrcArray.value,
|
||||
lrcTimeArray: lrcTimeArray.value,
|
||||
allTime: allTime.value,
|
||||
};
|
||||
windowData.electronAPI.sendLyric(JSON.stringify(staticData));
|
||||
} else {
|
||||
console.log('No lyrics available for initialization');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing lyric window:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 发送歌词更新数据
|
||||
export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||
if (!isElectron.value) return;
|
||||
export const sendLyricToWin = () => {
|
||||
if (!isElectron.value || !isLyricWindowOpen.value) {
|
||||
console.log('Cannot send lyric: electron or lyric window not available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (lrcArray.value.length > 0) {
|
||||
const nowIndex = getLrcIndex(nowTime.value);
|
||||
const updateData = {
|
||||
type: 'update',
|
||||
type: 'full',
|
||||
nowIndex,
|
||||
nowTime: nowTime.value,
|
||||
startCurrentTime: lrcTimeArray.value[nowIndex],
|
||||
nextTime: lrcTimeArray.value[nowIndex + 1],
|
||||
isPlay,
|
||||
isPlay: isPlaying.value,
|
||||
lrcArray: lrcArray.value,
|
||||
lrcTimeArray: lrcTimeArray.value,
|
||||
allTime: allTime.value,
|
||||
playMusic: playMusic.value,
|
||||
};
|
||||
windowData.electronAPI.sendLyric(JSON.stringify(updateData));
|
||||
}
|
||||
@@ -305,13 +292,52 @@ export const sendLyricToWin = (isPlay: boolean = true) => {
|
||||
|
||||
export const openLyric = () => {
|
||||
if (!isElectron.value) return;
|
||||
console.log('Opening lyric window');
|
||||
windowData.electronAPI.openLyric();
|
||||
console.log('Opening lyric window with current song:', playMusic.value?.name);
|
||||
|
||||
// 延迟一下初始化,确保窗口已经创建
|
||||
setTimeout(() => {
|
||||
console.log('Initializing lyric window after delay');
|
||||
initLyricWindow();
|
||||
isLyricWindowOpen.value = !isLyricWindowOpen.value;
|
||||
if (isLyricWindowOpen.value) {
|
||||
setTimeout(() => {
|
||||
windowData.electronAPI.openLyric();
|
||||
sendLyricToWin();
|
||||
}, 500);
|
||||
sendLyricToWin();
|
||||
}, 500);
|
||||
} else {
|
||||
closeLyric();
|
||||
}
|
||||
};
|
||||
|
||||
// 添加关闭歌词窗口的方法
|
||||
export const closeLyric = () => {
|
||||
if (!isElectron.value) return;
|
||||
windowData.electron.ipcRenderer.send('close-lyric');
|
||||
};
|
||||
|
||||
// 添加播放控制命令监听
|
||||
if (isElectron.value) {
|
||||
windowData.electron.ipcRenderer.on('lyric-control-back', (command: string) => {
|
||||
console.log('Received playback control command:', command);
|
||||
switch (command) {
|
||||
case 'playpause':
|
||||
if (store.state.play) {
|
||||
store.commit('setPlayMusic', false);
|
||||
audioService.getCurrentSound()?.pause();
|
||||
} else {
|
||||
store.commit('setPlayMusic', true);
|
||||
audioService.getCurrentSound()?.play();
|
||||
}
|
||||
break;
|
||||
case 'prev':
|
||||
store.commit('prevPlay');
|
||||
break;
|
||||
case 'next':
|
||||
store.commit('nextPlay');
|
||||
break;
|
||||
case 'close':
|
||||
closeLyric();
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown command:', command);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.n-slider-handle-indicator--top {
|
||||
@apply bg-transparent text-[#ffffffdd] text-2xl px-2 py-1 shadow-none mb-0 !important;
|
||||
}
|
||||
|
||||
.text-el {
|
||||
@apply overflow-ellipsis overflow-hidden whitespace-nowrap;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="layout-page">
|
||||
<div class="layout-main" :style="{ background: backgroundColor }">
|
||||
<div id="layout-main" class="layout-main" :style="{ background: backgroundColor }">
|
||||
<title-bar v-if="isElectron" />
|
||||
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
|
||||
<!-- 侧边菜单栏 -->
|
||||
@@ -10,17 +10,15 @@
|
||||
<search-bar />
|
||||
<!-- 主页面路由 -->
|
||||
<div class="main-content" :native-scrollbar="false">
|
||||
<n-message-provider>
|
||||
<router-view
|
||||
v-slot="{ Component }"
|
||||
class="main-page"
|
||||
:class="route.meta.noScroll && !isMobile ? 'pr-3' : ''"
|
||||
>
|
||||
<keep-alive :include="keepAliveInclude">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</n-message-provider>
|
||||
<router-view
|
||||
v-slot="{ Component }"
|
||||
class="main-page"
|
||||
:class="route.meta.noScroll && !isMobile ? 'pr-3' : ''"
|
||||
>
|
||||
<keep-alive :include="keepAliveInclude">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
<play-bottom height="5rem" />
|
||||
<app-menu v-if="isMobile" class="menu" :menus="menus" />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<n-drawer
|
||||
:show="musicFull"
|
||||
height="100vh"
|
||||
height="100%"
|
||||
placement="bottom"
|
||||
:style="{ background: currentBackground || background }"
|
||||
:to="`#layout-main`"
|
||||
>
|
||||
<div id="drawer-target">
|
||||
<div class="drawer-back"></div>
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
<!-- 展开全屏 -->
|
||||
<music-full ref="MusicFullRef" v-model:music-full="musicFullVisible" :background="background" />
|
||||
<!-- 底部播放栏 -->
|
||||
|
||||
<div
|
||||
class="music-play-bar"
|
||||
:class="setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')"
|
||||
>
|
||||
<div class="music-time custom-slider">
|
||||
<n-slider v-model:value="timeSlider" :step="1" :max="allTime" :min="0" :format-tooltip="formatTooltip"></n-slider>
|
||||
</div>
|
||||
<div class="play-bar-img-wrapper" @click="setMusicFull">
|
||||
<n-image :src="getImgUrl(playMusic?.picUrl, '300y300')" class="play-bar-img" lazy preview-disabled />
|
||||
<div class="hover-arrow">
|
||||
@@ -38,37 +42,38 @@
|
||||
<div class="music-buttons-play" @click="playMusicEvent">
|
||||
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
||||
</div>
|
||||
<div class="music-buttons-next" @click="handleEnded">
|
||||
<div class="music-buttons-next" @click="handleNext">
|
||||
<i class="iconfont icon-next"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="music-time custom-slider">
|
||||
<div class="time">{{ getNowTime }}</div>
|
||||
<n-slider v-model:value="timeSlider" :step="0.05" :tooltip="false"></n-slider>
|
||||
<div class="time">{{ getAllTime }}</div>
|
||||
</div>
|
||||
<div class="audio-volume custom-slider">
|
||||
<div>
|
||||
<i class="iconfont icon-notificationfill"></i>
|
||||
</div>
|
||||
<n-slider v-model:value="volumeSlider" :step="0.01" :tooltip="false"></n-slider>
|
||||
</div>
|
||||
<div class="audio-button">
|
||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
||||
<div class="audio-volume custom-slider">
|
||||
<div class="volume-icon" @click="mute">
|
||||
<i class="iconfont" :class="getVolumeIcon"></i>
|
||||
</div>
|
||||
<div class="volume-slider">
|
||||
<n-slider v-model:value="volumeSlider" :step="0.01" :tooltip="false" vertical></n-slider>
|
||||
</div>
|
||||
</div>
|
||||
<n-tooltip trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont icon-likefill"></i>
|
||||
<i class="iconfont" :class="playModeIcon" @click="togglePlayMode"></i>
|
||||
</template>
|
||||
{{ playModeText }}
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont icon-likefill" :class="{ 'like-active': isFavorite }" @click="toggleFavorite"></i>
|
||||
</template>
|
||||
喜欢
|
||||
</n-tooltip> -->
|
||||
<!-- <n-tooltip trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont icon-Play" @click="parsingMusic"></i>
|
||||
</template>
|
||||
解析播放
|
||||
</n-tooltip> -->
|
||||
</n-tooltip>
|
||||
<n-tooltip v-if="isElectron" class="music-lyric" trigger="hover" :z-index="9999999">
|
||||
<template #trigger>
|
||||
<i class="iconfont ri-netease-cloud-music-line" @click="openLyric"></i>
|
||||
<i
|
||||
class="iconfont ri-netease-cloud-music-line"
|
||||
:class="{ 'text-green-500': isLyricWindowOpen }"
|
||||
@click="openLyricWindow"
|
||||
></i>
|
||||
</template>
|
||||
歌词
|
||||
</n-tooltip>
|
||||
@@ -112,7 +117,7 @@ import { useTemplateRef } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import SongItem from '@/components/common/SongItem.vue';
|
||||
import { allTime, isElectron, nowTime, openLyric, sound } from '@/hooks/MusicHook';
|
||||
import { allTime, isElectron, isLyricWindowOpen, nowTime, openLyric, sound } from '@/hooks/MusicHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
|
||||
|
||||
@@ -139,17 +144,33 @@ watch(
|
||||
// 使用 useThrottleFn 创建节流版本的 seek 函数
|
||||
const throttledSeek = useThrottleFn((value: number) => {
|
||||
if (!sound.value) return;
|
||||
sound.value.seek((value * allTime.value) / 100);
|
||||
sound.value.seek(value);
|
||||
nowTime.value = value;
|
||||
}, 50); // 50ms 的节流延迟
|
||||
|
||||
// 修改 timeSlider 计算属性
|
||||
const timeSlider = computed({
|
||||
get: () => (nowTime.value / allTime.value) * 100,
|
||||
get: () => nowTime.value,
|
||||
set: throttledSeek,
|
||||
});
|
||||
|
||||
const formatTooltip = (value: number) => {
|
||||
return `${secondToMinute(value)} / ${secondToMinute(allTime.value)}`;
|
||||
};
|
||||
|
||||
// 音量条
|
||||
const audioVolume = ref(localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1);
|
||||
const getVolumeIcon = computed(() => {
|
||||
// 0 静音 ri-volume-mute-line 0.5 ri-volume-down-line 1 ri-volume-up-line
|
||||
if (audioVolume.value === 0) {
|
||||
return 'ri-volume-mute-line';
|
||||
}
|
||||
if (audioVolume.value <= 0.5) {
|
||||
return 'ri-volume-down-line';
|
||||
}
|
||||
return 'ri-volume-up-line';
|
||||
});
|
||||
|
||||
const volumeSlider = computed({
|
||||
get: () => audioVolume.value * 100,
|
||||
set: (value) => {
|
||||
@@ -159,17 +180,31 @@ const volumeSlider = computed({
|
||||
audioVolume.value = value / 100;
|
||||
},
|
||||
});
|
||||
// 获取当前播放时间
|
||||
const getNowTime = computed(() => {
|
||||
return secondToMinute(nowTime.value);
|
||||
|
||||
// 静音
|
||||
const mute = () => {
|
||||
if (volumeSlider.value === 0) {
|
||||
volumeSlider.value = 30;
|
||||
} else {
|
||||
volumeSlider.value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 播放模式
|
||||
const playMode = computed(() => store.state.playMode);
|
||||
const playModeIcon = computed(() => {
|
||||
return playMode.value === 0 ? 'ri-repeat-2-line' : 'ri-repeat-one-line';
|
||||
});
|
||||
const playModeText = computed(() => {
|
||||
return playMode.value === 0 ? '列表循环' : '单曲循环';
|
||||
});
|
||||
|
||||
// 获取总时间
|
||||
const getAllTime = computed(() => {
|
||||
return secondToMinute(allTime.value);
|
||||
});
|
||||
// 切换播放模式
|
||||
const togglePlayMode = () => {
|
||||
store.commit('togglePlayMode');
|
||||
};
|
||||
|
||||
function handleEnded() {
|
||||
function handleNext() {
|
||||
store.commit('nextPlay');
|
||||
}
|
||||
|
||||
@@ -209,6 +244,23 @@ const scrollToPlayList = (val: boolean) => {
|
||||
palyListRef.value?.scrollTo({ top: store.state.playListIndex * 62 });
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const isFavorite = computed(() => {
|
||||
return store.state.favoriteList.includes(playMusic.value.id);
|
||||
});
|
||||
|
||||
const toggleFavorite = async (e: Event) => {
|
||||
e.stopPropagation();
|
||||
if (isFavorite.value) {
|
||||
store.commit('removeFromFavorite', playMusic.value.id);
|
||||
} else {
|
||||
store.commit('addToFavorite', playMusic.value.id);
|
||||
}
|
||||
};
|
||||
|
||||
const openLyricWindow = () => {
|
||||
openLyric();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -217,13 +269,13 @@ const scrollToPlayList = (val: boolean) => {
|
||||
}
|
||||
|
||||
.music-play-bar {
|
||||
@apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2;
|
||||
@apply h-20 w-full absolute bottom-0 left-0 flex items-center box-border px-6 py-2 pt-3;
|
||||
z-index: 9999;
|
||||
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
|
||||
background-color: #212121;
|
||||
animation-duration: 0.5s !important;
|
||||
.music-content {
|
||||
width: 140px;
|
||||
width: 160px;
|
||||
@apply ml-4;
|
||||
|
||||
&-title {
|
||||
@@ -246,14 +298,14 @@ const scrollToPlayList = (val: boolean) => {
|
||||
}
|
||||
|
||||
.music-buttons {
|
||||
@apply mx-6;
|
||||
@apply mx-6 flex-1 flex justify-center;
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl hover:text-green-500 transition;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@apply text-xl hover:text-white;
|
||||
@apply text-3xl hover:text-white;
|
||||
}
|
||||
|
||||
@apply flex items-center;
|
||||
@@ -263,25 +315,28 @@ const scrollToPlayList = (val: boolean) => {
|
||||
}
|
||||
|
||||
&-play {
|
||||
background: #383838;
|
||||
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition bg-opacity-40;
|
||||
}
|
||||
}
|
||||
|
||||
.music-time {
|
||||
@apply flex flex-1 items-center;
|
||||
|
||||
.time {
|
||||
@apply mx-4 mt-1;
|
||||
background-color: #ffffff20;
|
||||
@apply flex justify-center items-center w-20 h-12 rounded-full mx-4 hover:bg-[#ffffff40] transition;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-volume {
|
||||
width: 140px;
|
||||
@apply flex items-center mx-4;
|
||||
@apply flex items-center relative;
|
||||
&:hover {
|
||||
.volume-slider {
|
||||
@apply opacity-100 visible;
|
||||
}
|
||||
}
|
||||
.volume-icon {
|
||||
@apply cursor-pointer;
|
||||
|
||||
.iconfont {
|
||||
@apply text-2xl hover:text-green-500 transition cursor-pointer mr-4;
|
||||
.iconfont {
|
||||
@apply text-2xl hover:text-green-500 transition;
|
||||
}
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
@apply absolute opacity-0 invisible transition-all duration-300 bottom-[30px] left-1/2 -translate-x-1/2 h-[180px] px-2 py-4 bg-gray-800 bg-opacity-80 rounded-xl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,17 +404,31 @@ const scrollToPlayList = (val: boolean) => {
|
||||
--n-handle-size: 12px;
|
||||
--n-handle-color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
--n-rail-height: 6px;
|
||||
--n-handle-size: 14px;
|
||||
&.n-slider--vertical {
|
||||
height: 100%;
|
||||
|
||||
.n-slider-rail {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.n-slider-rail {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.n-slider-handle {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.n-slider-rail {
|
||||
@apply overflow-hidden;
|
||||
@apply overflow-hidden transition-all duration-200;
|
||||
}
|
||||
|
||||
.n-slider-handle {
|
||||
@apply transition-opacity duration-200;
|
||||
@apply transition-all duration-200;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -407,4 +476,21 @@ const scrollToPlayList = (val: boolean) => {
|
||||
.play-bar-img {
|
||||
@apply w-14 h-14 rounded-2xl;
|
||||
}
|
||||
|
||||
.like-active {
|
||||
@apply text-red-600;
|
||||
}
|
||||
|
||||
.icon-loop,
|
||||
.icon-single-loop {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.music-time .n-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,32 +13,53 @@
|
||||
<i class="iconfont icon-search"></i>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<div class="w-20 px-3 flex justify-between items-center">
|
||||
<div>{{ searchTypeOptions.find((item) => item.key === store.state.searchType)?.label }}</div>
|
||||
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
|
||||
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
|
||||
<div class="w-20 px-3 flex justify-between items-center">
|
||||
<div>{{ searchTypeOptions.find((item) => item.key === store.state.searchType)?.label }}</div>
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div class="user-box">
|
||||
<n-dropdown trigger="hover" :options="userSetOptions" @select="selectItem">
|
||||
<i class="iconfont icon-xiasanjiaoxing"></i>
|
||||
</n-dropdown>
|
||||
<n-avatar
|
||||
v-if="store.state.user"
|
||||
class="ml-2 cursor-pointer"
|
||||
circle
|
||||
size="medium"
|
||||
:src="getImgUrl(store.state.user.avatarUrl)"
|
||||
/>
|
||||
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">登录</div>
|
||||
</div>
|
||||
<coffee
|
||||
alipay-q-r="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true"
|
||||
wechat-q-r="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true"
|
||||
>
|
||||
<n-popover trigger="hover" placement="bottom" :show-arrow="false" raw>
|
||||
<template #trigger>
|
||||
<div class="user-box">
|
||||
<n-avatar
|
||||
v-if="store.state.user"
|
||||
class="ml-2 cursor-pointer"
|
||||
circle
|
||||
size="medium"
|
||||
:src="getImgUrl(store.state.user.avatarUrl)"
|
||||
@click="selectItem('user')"
|
||||
/>
|
||||
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">登录</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="user-popover">
|
||||
<div v-if="store.state.user" class="user-header" @click="selectItem('user')">
|
||||
<n-avatar circle size="small" :src="getImgUrl(store.state.user?.avatarUrl)" />
|
||||
<span class="username">{{ store.state.user?.nickname || 'Theodore' }}</span>
|
||||
</div>
|
||||
<div class="menu-items">
|
||||
<div v-if="!store.state.user" class="menu-item" @click="toLogin">
|
||||
<i class="iconfont ri-login-box-line"></i>
|
||||
<span>去登录</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="selectItem('set')">
|
||||
<i class="iconfont ri-settings-3-line"></i>
|
||||
<span>设置</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="toGithubRelease">
|
||||
<i class="iconfont ri-refresh-line"></i>
|
||||
<span>当前版本</span>
|
||||
<span class="download-btn">{{ config.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-popover>
|
||||
|
||||
<coffee :alipay-q-r="alipay" :wechat-q-r="wechat">
|
||||
<div class="github" @click="toGithub">
|
||||
<i class="ri-github-fill"></i>
|
||||
</div>
|
||||
@@ -50,8 +71,11 @@
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import config from '@/../package.json';
|
||||
import { getSearchKeyword } from '@/api/home';
|
||||
import { getUserDetail, logout } from '@/api/login';
|
||||
import alipay from '@/assets/alipay.png';
|
||||
import wechat from '@/assets/wechat.png';
|
||||
import Coffee from '@/components/Coffee.vue';
|
||||
import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
|
||||
import { getImgUrl } from '@/utils';
|
||||
@@ -141,6 +165,9 @@ const selectItem = async (key: string) => {
|
||||
case 'set':
|
||||
router.push('/set');
|
||||
break;
|
||||
case 'user':
|
||||
router.push('/user');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
@@ -148,11 +175,15 @@ const selectItem = async (key: string) => {
|
||||
const toGithub = () => {
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer', '_blank');
|
||||
};
|
||||
|
||||
const toGithubRelease = () => {
|
||||
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-box {
|
||||
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
|
||||
@apply ml-4 flex text-lg justify-center items-center rounded-full border border-gray-600 hover:border-gray-400 transition-colors duration-200;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
.search-box {
|
||||
@@ -171,4 +202,65 @@ const toGithub = () => {
|
||||
.github {
|
||||
@apply cursor-pointer text-gray-100 hover:text-gray-400 text-xl ml-4 rounded-full border border-gray-600 flex justify-center items-center px-2 h-full;
|
||||
}
|
||||
|
||||
.user-popover {
|
||||
@apply min-w-[280px] p-0 rounded-xl overflow-hidden;
|
||||
background: #2c2c2c;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.user-header {
|
||||
@apply flex items-center gap-2 p-3;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
|
||||
.username {
|
||||
@apply text-sm font-medium text-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-items {
|
||||
@apply py-1;
|
||||
|
||||
.menu-item {
|
||||
@apply flex items-center px-3 py-2 text-sm cursor-pointer;
|
||||
@apply text-gray-300;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
i {
|
||||
@apply mr-1 text-lg text-gray-400;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
@apply ml-auto text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
@apply ml-auto px-2 py-0.5 text-xs rounded;
|
||||
background: #4a4a4a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
@apply ml-auto flex items-center gap-2;
|
||||
color: #fff;
|
||||
|
||||
.zoom-btn {
|
||||
@apply px-2 py-0.5 text-sm rounded cursor-pointer;
|
||||
background: #3a3a3a;
|
||||
|
||||
&:hover {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
}
|
||||
|
||||
span:not(.zoom-btn) {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,14 +15,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useDialog } from 'naive-ui';
|
||||
|
||||
import { isElectron } from '@/hooks/MusicHook';
|
||||
|
||||
const dialog = useDialog();
|
||||
const windowData = window as any;
|
||||
|
||||
const minimize = () => {
|
||||
if (!isElectron.value) {
|
||||
return;
|
||||
}
|
||||
windowData.electronAPI.minimize();
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
if (!isElectron.value) {
|
||||
return;
|
||||
}
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定要退出吗?',
|
||||
@@ -38,6 +46,9 @@ const close = () => {
|
||||
};
|
||||
|
||||
const drag = (event: MouseEvent) => {
|
||||
if (!isElectron.value) {
|
||||
return;
|
||||
}
|
||||
windowData.electronAPI.dragStart(event);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,7 @@ const layoutRouter = [
|
||||
title: '首页',
|
||||
icon: 'icon-Home',
|
||||
keepAlive: true,
|
||||
isMobile: true,
|
||||
},
|
||||
component: () => import('@/views/home/index.vue'),
|
||||
},
|
||||
@@ -17,6 +18,7 @@ const layoutRouter = [
|
||||
noScroll: true,
|
||||
icon: 'icon-Search',
|
||||
keepAlive: true,
|
||||
isMobile: true,
|
||||
},
|
||||
component: () => import('@/views/search/index.vue'),
|
||||
},
|
||||
@@ -27,6 +29,7 @@ const layoutRouter = [
|
||||
title: '歌单',
|
||||
icon: 'icon-Paper',
|
||||
keepAlive: true,
|
||||
isMobile: true,
|
||||
},
|
||||
component: () => import('@/views/list/index.vue'),
|
||||
},
|
||||
@@ -37,27 +40,29 @@ const layoutRouter = [
|
||||
title: 'MV',
|
||||
icon: 'icon-recordfill',
|
||||
keepAlive: true,
|
||||
isMobile: true,
|
||||
},
|
||||
component: () => import('@/views/mv/index.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/history',
|
||||
// name: 'history',
|
||||
// meta: {
|
||||
// title: '历史',
|
||||
// icon: 'icon-a-TicketStar',
|
||||
// keepAlive: true,
|
||||
// },
|
||||
// component: () => import('@/views/history/index.vue'),
|
||||
// },
|
||||
{
|
||||
path: '/history',
|
||||
name: 'history',
|
||||
component: () => import('@/views/historyAndFavorite/index.vue'),
|
||||
meta: {
|
||||
title: '历史',
|
||||
title: '我的收藏和历史',
|
||||
icon: 'icon-a-TicketStar',
|
||||
keepAlive: true,
|
||||
},
|
||||
component: () => import('@/views/history/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/favorite',
|
||||
name: 'favorite',
|
||||
component: () => import('@/views/favorite/index.vue'),
|
||||
meta: {
|
||||
title: '我的收藏',
|
||||
icon: 'icon-likefill',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
@@ -67,8 +72,20 @@ const layoutRouter = [
|
||||
icon: 'icon-Profile',
|
||||
keepAlive: true,
|
||||
noScroll: true,
|
||||
isMobile: true,
|
||||
},
|
||||
component: () => import('@/views/user/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/set',
|
||||
name: 'set',
|
||||
meta: {
|
||||
title: '设置',
|
||||
icon: 'ri-settings-3-fill',
|
||||
keepAlive: true,
|
||||
noScroll: true,
|
||||
},
|
||||
component: () => import('@/views/set/index.vue'),
|
||||
},
|
||||
];
|
||||
export default layoutRouter;
|
||||
|
||||
@@ -13,6 +13,11 @@ const defaultSettings = {
|
||||
authorUrl: 'https://github.com/algerkong',
|
||||
};
|
||||
|
||||
function getLocalStorageItem<T>(key: string, defaultValue: T): T {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
}
|
||||
|
||||
interface State {
|
||||
menus: any[];
|
||||
play: boolean;
|
||||
@@ -28,6 +33,7 @@ interface State {
|
||||
searchValue: string;
|
||||
searchType: number;
|
||||
favoriteList: number[];
|
||||
playMode: number;
|
||||
}
|
||||
|
||||
const state: State = {
|
||||
@@ -36,7 +42,7 @@ const state: State = {
|
||||
isPlay: false,
|
||||
playMusic: {} as SongResult,
|
||||
playMusicUrl: '',
|
||||
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') as string) : null,
|
||||
user: getLocalStorageItem('user', null),
|
||||
playList: [],
|
||||
playListIndex: 0,
|
||||
setData: defaultSettings,
|
||||
@@ -44,7 +50,8 @@ const state: State = {
|
||||
isMobile: false,
|
||||
searchValue: '',
|
||||
searchType: 1,
|
||||
favoriteList: localStorage.getItem('favoriteList') ? JSON.parse(localStorage.getItem('favoriteList') || '[]') : [],
|
||||
favoriteList: getLocalStorageItem('favoriteList', []),
|
||||
playMode: getLocalStorageItem('playMode', 0),
|
||||
};
|
||||
|
||||
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
|
||||
@@ -91,6 +98,10 @@ const mutations = {
|
||||
state.favoriteList = state.favoriteList.filter((id) => id !== songId);
|
||||
localStorage.setItem('favoriteList', JSON.stringify(state.favoriteList));
|
||||
},
|
||||
togglePlayMode(state: State) {
|
||||
state.playMode = state.playMode === 0 ? 1 : 0;
|
||||
localStorage.setItem('playMode', JSON.stringify(state.playMode));
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
|
||||
@@ -15,12 +15,12 @@ interface SortCategories {
|
||||
|
||||
interface SortAll {
|
||||
name: string;
|
||||
resourceCount: number;
|
||||
imgId: number;
|
||||
resourceCount?: number;
|
||||
imgId?: number;
|
||||
imgUrl?: any;
|
||||
type: number;
|
||||
category: number;
|
||||
resourceType: number;
|
||||
hot: boolean;
|
||||
activity: boolean;
|
||||
type?: number;
|
||||
category?: number;
|
||||
resourceType?: number;
|
||||
hot?: boolean;
|
||||
activity?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
<template>
|
||||
<div v-if="isComponent ? favoriteSongs.length : true" class="favorite-page">
|
||||
<div class="favorite-header" :class="setAnimationClass('animate__fadeInRight')">
|
||||
<div class="favorite-header" :class="setAnimationClass('animate__fadeInLeft')">
|
||||
<h2>我的收藏</h2>
|
||||
<div class="favorite-count">共 {{ favoriteList.length }} 首</div>
|
||||
</div>
|
||||
<div class="favorite-main" :class="setAnimationClass('animate__bounceInRight')">
|
||||
<n-scrollbar class="favorite-content">
|
||||
<n-scrollbar ref="scrollbarRef" class="favorite-content" @scroll="handleScroll">
|
||||
<div v-if="favoriteList.length === 0" class="empty-tip">
|
||||
<n-empty description="还没有收藏歌曲" />
|
||||
</div>
|
||||
<div v-else class="favorite-list">
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<song-item
|
||||
v-for="(song, index) in favoriteSongs"
|
||||
:key="song.id"
|
||||
:item="song"
|
||||
:favorite="!isComponent"
|
||||
:class="setAnimationClass('animate__bounceInUp')"
|
||||
:style="getItemAnimationDelay(index)"
|
||||
@play="handlePlay"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<song-item
|
||||
v-for="(song, index) in favoriteSongs"
|
||||
:key="song.id"
|
||||
:item="song"
|
||||
:favorite="!isComponent"
|
||||
:class="setAnimationClass('animate__bounceInLeft')"
|
||||
:style="getItemAnimationDelay(index)"
|
||||
@play="handlePlay"
|
||||
/>
|
||||
<div v-if="isComponent" class="favorite-list-more text-center">
|
||||
<n-button text type="primary" @click="handleMore">查看更多</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-if="noMore" class="no-more-tip">没有更多了</div>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
<div v-if="favoriteList.length > 0 && !loading && !isComponent" class="pagination-wrapper">
|
||||
<n-pagination
|
||||
v-model:page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:item-count="favoriteList.length"
|
||||
:page-slot="5"
|
||||
size="small"
|
||||
@update:page="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -58,12 +48,14 @@ const store = useStore();
|
||||
const favoriteList = computed(() => store.state.favoriteList);
|
||||
const favoriteSongs = ref<SongResult[]>([]);
|
||||
const loading = ref(false);
|
||||
const noMore = ref(false);
|
||||
const scrollbarRef = ref();
|
||||
|
||||
// 分页相关
|
||||
// 无限滚动相关
|
||||
const pageSize = 16;
|
||||
const currentPage = ref(1);
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
isComponent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -72,7 +64,6 @@ defineProps({
|
||||
|
||||
// 获取当前页的收藏歌曲ID
|
||||
const getCurrentPageIds = () => {
|
||||
// 反转列表顺序,最新收藏的在前面
|
||||
const reversedList = [...favoriteList.value];
|
||||
const startIndex = (currentPage.value - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
@@ -86,17 +77,29 @@ const getFavoriteSongs = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.isComponent && favoriteSongs.value.length >= 16) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const currentIds = getCurrentPageIds();
|
||||
const res = await getMusicDetail(currentIds);
|
||||
if (res.data.songs) {
|
||||
favoriteSongs.value = res.data.songs.map((song: SongResult) => {
|
||||
return {
|
||||
...song,
|
||||
picUrl: song.al?.picUrl || '',
|
||||
};
|
||||
});
|
||||
const newSongs = res.data.songs.map((song: SongResult) => ({
|
||||
...song,
|
||||
picUrl: song.al?.picUrl || '',
|
||||
}));
|
||||
|
||||
// 追加新数据而不是替换
|
||||
if (currentPage.value === 1) {
|
||||
favoriteSongs.value = newSongs;
|
||||
} else {
|
||||
favoriteSongs.value = [...favoriteSongs.value, ...newSongs];
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
noMore.value = favoriteSongs.value.length >= favoriteList.value.length;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取收藏歌曲失败:', error);
|
||||
@@ -105,9 +108,15 @@ const getFavoriteSongs = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理页码变化
|
||||
const handlePageChange = () => {
|
||||
getFavoriteSongs();
|
||||
// 处理滚动事件
|
||||
const handleScroll = (e: any) => {
|
||||
const { scrollTop, scrollHeight, offsetHeight } = e.target;
|
||||
const threshold = 100; // 距离底部多少像素时加载更多
|
||||
|
||||
if (!loading.value && !noMore.value && scrollHeight - (scrollTop + offsetHeight) < threshold) {
|
||||
currentPage.value++;
|
||||
getFavoriteSongs();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -119,6 +128,7 @@ watch(
|
||||
favoriteList,
|
||||
() => {
|
||||
currentPage.value = 1;
|
||||
noMore.value = false;
|
||||
getFavoriteSongs();
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
@@ -129,8 +139,7 @@ const handlePlay = () => {
|
||||
};
|
||||
|
||||
const getItemAnimationDelay = (index: number) => {
|
||||
const currentPageIndex = index % pageSize;
|
||||
return setAnimationDelay(currentPageIndex, 30);
|
||||
return setAnimationDelay(index, 30);
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
@@ -141,13 +150,13 @@ const handleMore = () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.favorite-page {
|
||||
@apply h-full flex flex-col p-6;
|
||||
@apply h-full flex flex-col pt-2;
|
||||
|
||||
.favorite-header {
|
||||
@apply flex items-center justify-between mb-6 flex-shrink-0;
|
||||
@apply flex items-center justify-between flex-shrink-0 px-4;
|
||||
|
||||
h2 {
|
||||
@apply text-2xl font-bold;
|
||||
@apply text-xl font-bold pb-2;
|
||||
}
|
||||
|
||||
.favorite-count {
|
||||
@@ -166,7 +175,7 @@ const handleMore = () => {
|
||||
}
|
||||
|
||||
.favorite-list {
|
||||
@apply space-y-2 pb-4;
|
||||
@apply space-y-2 pb-4 px-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,20 +185,8 @@ const handleMore = () => {
|
||||
@apply flex justify-center items-center py-20;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
@apply flex justify-center py-4 flex-shrink-0;
|
||||
|
||||
:deep(.n-pagination) {
|
||||
@apply bg-gray-800 rounded-full px-4 py-1;
|
||||
|
||||
.n-pagination-item {
|
||||
@apply text-gray-300 hover:text-white;
|
||||
|
||||
&--active {
|
||||
@apply text-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-more-tip {
|
||||
@apply text-center text-gray-400 py-4 text-sm;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
<template>
|
||||
<div class="history-page">
|
||||
<div class="title">播放历史</div>
|
||||
<n-scrollbar :size="100">
|
||||
<div class="title" :class="setAnimationClass('animate__fadeInRight')">播放历史</div>
|
||||
<n-scrollbar ref="scrollbarRef" :size="100" @scroll="handleScroll">
|
||||
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||
<div
|
||||
v-for="(item, index) in musicList"
|
||||
v-for="(item, index) in displayList"
|
||||
:key="item.id"
|
||||
class="history-item"
|
||||
:class="setAnimationClass('animate__bounceIn')"
|
||||
:class="setAnimationClass('animate__bounceInRight')"
|
||||
:style="setAnimationDelay(index, 30)"
|
||||
>
|
||||
<song-item class="history-item-content" :item="item" list @play="handlePlay" />
|
||||
<song-item class="history-item-content" :item="item" @play="handlePlay" />
|
||||
<div class="history-item-count min-w-[60px]">
|
||||
{{ item.count }}
|
||||
</div>
|
||||
<div class="history-item-delete">
|
||||
<i class="iconfont icon-close" @click="delMusic(item)"></i>
|
||||
<i class="iconfont icon-close" @click="handleDelMusic(item)"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-if="noMore" class="no-more-tip">没有更多了</div>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getMusicDetail } from '@/api/music';
|
||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||
import type { SongResult } from '@/type/music';
|
||||
import { setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
defineOptions({
|
||||
@@ -35,9 +44,81 @@ defineOptions({
|
||||
|
||||
const store = useStore();
|
||||
const { delMusic, musicList } = useMusicHistory();
|
||||
const scrollbarRef = ref();
|
||||
const loading = ref(false);
|
||||
const noMore = ref(false);
|
||||
const displayList = ref<SongResult[]>([]);
|
||||
|
||||
// 无限滚动相关配置
|
||||
const pageSize = 20;
|
||||
const currentPage = ref(1);
|
||||
|
||||
// 获取当前页的音乐详情
|
||||
const getHistorySongs = async () => {
|
||||
if (musicList.value.length === 0) {
|
||||
displayList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const startIndex = (currentPage.value - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const currentPageItems = musicList.value.slice(startIndex, endIndex);
|
||||
|
||||
const currentIds = currentPageItems.map((item) => item.id);
|
||||
const res = await getMusicDetail(currentIds);
|
||||
|
||||
if (res.data.songs) {
|
||||
const newSongs = res.data.songs.map((song: SongResult) => {
|
||||
const historyItem = currentPageItems.find((item) => item.id === song.id);
|
||||
return {
|
||||
...song,
|
||||
picUrl: song.al?.picUrl || '',
|
||||
count: historyItem?.count || 0,
|
||||
};
|
||||
});
|
||||
|
||||
if (currentPage.value === 1) {
|
||||
displayList.value = newSongs;
|
||||
} else {
|
||||
displayList.value = [...displayList.value, ...newSongs];
|
||||
}
|
||||
|
||||
noMore.value = displayList.value.length >= musicList.value.length;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取历史记录失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (e: any) => {
|
||||
const { scrollTop, scrollHeight, offsetHeight } = e.target;
|
||||
const threshold = 100; // 距离底部多少像素时加载更多
|
||||
|
||||
if (!loading.value && !noMore.value && scrollHeight - (scrollTop + offsetHeight) < threshold) {
|
||||
currentPage.value++;
|
||||
getHistorySongs();
|
||||
}
|
||||
};
|
||||
|
||||
// 播放全部
|
||||
const handlePlay = () => {
|
||||
store.commit('setPlayList', musicList.value);
|
||||
store.commit('setPlayList', displayList.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getHistorySongs();
|
||||
});
|
||||
|
||||
// 重写删除方法,需要同时更新 displayList
|
||||
const handleDelMusic = async (item: SongResult) => {
|
||||
delMusic(item);
|
||||
musicList.value = musicList.value.filter((music) => music.id !== item.id);
|
||||
displayList.value = displayList.value.filter((music) => music.id !== item.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -45,11 +126,11 @@ const handlePlay = () => {
|
||||
.history-page {
|
||||
@apply h-full w-full pt-2;
|
||||
.title {
|
||||
@apply pl-4 text-xl font-bold;
|
||||
@apply pl-4 text-xl font-bold pb-2 px-4;
|
||||
}
|
||||
|
||||
.history-list-content {
|
||||
@apply px-4 mt-2 pb-28;
|
||||
@apply mt-2 pb-28 px-4;
|
||||
.history-item {
|
||||
@apply flex items-center justify-between;
|
||||
&-content {
|
||||
@@ -64,4 +145,12 @@ const handlePlay = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-wrapper {
|
||||
@apply flex justify-center items-center py-8;
|
||||
}
|
||||
|
||||
.no-more-tip {
|
||||
@apply text-center text-gray-400 py-4 text-sm;
|
||||
}
|
||||
</style>
|
||||
|
||||
13
src/views/historyAndFavorite/index.vue
Normal file
13
src/views/historyAndFavorite/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="flex gap-4 h-full p-4">
|
||||
<favorite class="flex-1 bg-[#0d0d0d] border border-[#374151] rounded-2xl overflow-hidden" />
|
||||
<history class="flex-1 bg-[#0d0d0d] border border-[#374151] rounded-2xl overflow-hidden" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Favorite from '@/views/favorite/index.vue';
|
||||
import History from '@/views/history/index.vue';
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -47,10 +47,13 @@ defineOptions({
|
||||
:deep(.favorite-page) {
|
||||
@apply p-0 mx-4 h-[300px];
|
||||
.favorite-header {
|
||||
@apply mb-0;
|
||||
@apply mb-0 px-0 !important;
|
||||
h2 {
|
||||
@apply text-lg font-bold mb-4;
|
||||
@apply text-lg font-bold;
|
||||
}
|
||||
}
|
||||
.favorite-list {
|
||||
@apply px-0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { getListByCat, getListDetail, getRecommendList } from '@/api/list';
|
||||
import { getPlaylistCategory } from '@/api/home';
|
||||
import { getListByCat, getListDetail } from '@/api/list';
|
||||
import MusicList from '@/components/MusicList.vue';
|
||||
import type { IRecommendItem } from '@/type/list';
|
||||
import type { IListDetail } from '@/type/listDetail';
|
||||
import type { IPlayListSort } from '@/type/playlist';
|
||||
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
|
||||
|
||||
defineOptions({
|
||||
@@ -85,14 +87,40 @@ const handleScroll = (e: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 添加歌单分类相关的代码
|
||||
const playlistCategory = ref<IPlayListSort>();
|
||||
const currentType = ref((route.query.type as string) || '每日推荐');
|
||||
|
||||
const getAnimationDelay = computed(() => {
|
||||
return (index: number) => setAnimationDelay(index, 30);
|
||||
});
|
||||
|
||||
// 加载歌单分类
|
||||
const loadPlaylistCategory = async () => {
|
||||
const { data } = await getPlaylistCategory();
|
||||
playlistCategory.value = {
|
||||
...data,
|
||||
sub: [
|
||||
{
|
||||
name: '每日推荐',
|
||||
category: 0,
|
||||
},
|
||||
...data.sub,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const handleClickPlaylistType = (type: string) => {
|
||||
currentType.value = type;
|
||||
listTitle.value = type;
|
||||
loading.value = true;
|
||||
loadList(type);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.type) {
|
||||
loadList(route.query.type as string);
|
||||
} else {
|
||||
getRecommendList(TOTAL_ITEMS).then((res: { data: { result: any } }) => {
|
||||
recommendList.value = res.data.result;
|
||||
});
|
||||
}
|
||||
loadPlaylistCategory(); // 添加加载歌单分类
|
||||
currentType.value = (route.query.type as string) || currentType.value;
|
||||
loadList(currentType.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -101,6 +129,8 @@ watch(
|
||||
if (newParams.type) {
|
||||
recommendList.value = [];
|
||||
listTitle.value = newParams.type || '歌单列表';
|
||||
currentType.value = newParams.type as string;
|
||||
loading.value = true;
|
||||
loadList(newParams.type as string);
|
||||
}
|
||||
},
|
||||
@@ -109,7 +139,23 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="list-page">
|
||||
<div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div>
|
||||
<!-- 修改歌单分类部分 -->
|
||||
<div class="play-list-type">
|
||||
<n-scrollbar x-scrollable>
|
||||
<div class="categories-wrapper">
|
||||
<span
|
||||
v-for="(item, index) in playlistCategory?.sub"
|
||||
:key="item.name"
|
||||
class="play-list-type-item"
|
||||
:class="[setAnimationClass('animate__bounceIn'), { active: currentType === item.name }]"
|
||||
:style="getAnimationDelay(index)"
|
||||
@click="handleClickPlaylistType(item.name)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
<!-- 歌单列表 -->
|
||||
<n-scrollbar class="recommend" :size="100" @scroll="handleScroll">
|
||||
<div v-loading="loading" class="recommend-list">
|
||||
@@ -228,4 +274,35 @@ watch(
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加歌单分类样式
|
||||
.play-list-type {
|
||||
.title {
|
||||
@apply text-lg font-bold mb-4;
|
||||
}
|
||||
|
||||
.categories-wrapper {
|
||||
@apply flex items-center py-2 pb-4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-item {
|
||||
@apply py-2 px-3 mr-3 inline-block border border-gray-700 rounded-xl cursor-pointer transition-all duration-300;
|
||||
background-color: #1a1a1a;
|
||||
|
||||
&:hover {
|
||||
@apply bg-green-600/50;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-green-600 border-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.play-list-type {
|
||||
@apply mx-0 w-full;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
<div
|
||||
class="lyric-window"
|
||||
:class="[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="drag-overlay"></div>
|
||||
<!-- 顶部控制栏 -->
|
||||
<div class="control-bar" :class="{ 'control-bar-show': showControls }">
|
||||
<div class="font-size-controls">
|
||||
@@ -16,16 +18,29 @@
|
||||
<i class="ri-add-line"></i>
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
<div>{{ staticData.playMusic.name }}</div>
|
||||
</div>
|
||||
<!-- 添加播放控制按钮 -->
|
||||
<div class="play-controls">
|
||||
<div class="control-button" @click="handlePrev">
|
||||
<i class="ri-skip-back-fill"></i>
|
||||
</div>
|
||||
<div class="control-button play-button" @click="handlePlayPause">
|
||||
<i :class="dynamicData.isPlay ? 'ri-pause-fill' : 'ri-play-fill'"></i>
|
||||
</div>
|
||||
<div class="control-button" @click="handleNext">
|
||||
<i class="ri-skip-forward-fill"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-buttons">
|
||||
<div class="control-button" @click="checkTheme">
|
||||
<i v-if="lyricSetting.theme === 'light'" class="ri-sun-line"></i>
|
||||
<i v-else class="ri-moon-line"></i>
|
||||
</div>
|
||||
<div class="control-button" @click="handleTop">
|
||||
<!-- <div class="control-button" @click="handleTop">
|
||||
<i class="ri-pushpin-line" :class="{ active: lyricSetting.isTop }"></i>
|
||||
</div>
|
||||
<div class="control-button" @click="handleLock">
|
||||
</div> -->
|
||||
<div id="lyric-lock" class="control-button" @click="handleLock">
|
||||
<i v-if="lyricSetting.isLock" class="ri-lock-line"></i>
|
||||
<i v-else class="ri-lock-unlock-line"></i>
|
||||
</div>
|
||||
@@ -61,7 +76,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="lyric-empty">暂无歌词</div>
|
||||
<div v-else class="lyric-empty">无歌词</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,31 +84,35 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { SongResult } from '@/type/music';
|
||||
|
||||
defineOptions({
|
||||
name: 'Lyric',
|
||||
});
|
||||
|
||||
const windowData = window as any;
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const containerHeight = ref(0);
|
||||
const lineHeight = ref(60);
|
||||
const currentIndex = ref(0);
|
||||
const isInitialized = ref(false);
|
||||
// 字体大小控制
|
||||
const fontSize = ref(24); // 默认字体大小
|
||||
const fontSizeStep = 2; // 每次整的步长
|
||||
const animationFrameId = ref<number | null>(null);
|
||||
const lastUpdateTime = ref(performance.now());
|
||||
|
||||
// 静态数据
|
||||
const staticData = ref<{
|
||||
lrcArray: Array<{ text: string; trText: string }>;
|
||||
lrcTimeArray: number[];
|
||||
allTime: number;
|
||||
playMusic: SongResult;
|
||||
}>({
|
||||
lrcArray: [],
|
||||
lrcTimeArray: [],
|
||||
allTime: 0,
|
||||
playMusic: {} as SongResult,
|
||||
});
|
||||
|
||||
// 动态数据
|
||||
@@ -136,14 +155,19 @@ const clearHideTimer = () => {
|
||||
|
||||
// 处理鼠标进入窗口
|
||||
const handleMouseEnter = () => {
|
||||
if (!lyricSetting.value.isLock) return;
|
||||
isHovering.value = true;
|
||||
if (lyricSetting.value.isLock) {
|
||||
isHovering.value = true;
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', true);
|
||||
} else {
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理鼠标离开窗口
|
||||
const handleMouseLeave = () => {
|
||||
if (!lyricSetting.value.isLock) return;
|
||||
isHovering.value = false;
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
||||
};
|
||||
|
||||
// 监听锁定状态变化
|
||||
@@ -169,7 +193,7 @@ onUnmounted(() => {
|
||||
|
||||
// 计算歌词滚动位置
|
||||
const wrapperStyle = computed(() => {
|
||||
if (!isInitialized.value || !containerHeight.value) {
|
||||
if (!containerHeight.value) {
|
||||
return {
|
||||
transform: 'translateY(0)',
|
||||
transition: 'none',
|
||||
@@ -180,7 +204,7 @@ const wrapperStyle = computed(() => {
|
||||
const containerCenter = containerHeight.value / 2;
|
||||
|
||||
// 计算当前行到顶部的距离(包含padding)
|
||||
const currentLineTop = currentIndex.value * lineHeight.value + containerHeight.value * 0.2; // 加上顶部padding
|
||||
const currentLineTop = currentIndex.value * lineHeight.value + containerHeight.value * 0.2 + lineHeight.value; // 加上顶部padding
|
||||
|
||||
// 计算偏移量,使当前行居中
|
||||
const targetOffset = containerCenter - currentLineTop;
|
||||
@@ -197,7 +221,7 @@ const wrapperStyle = computed(() => {
|
||||
|
||||
return {
|
||||
transform: `translateY(${finalOffset}px)`,
|
||||
transition: isInitialized.value ? 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none',
|
||||
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -265,17 +289,13 @@ onMounted(() => {
|
||||
resizeObserver.disconnect();
|
||||
});
|
||||
});
|
||||
|
||||
// 动画帧ID
|
||||
const animationFrameId = ref<number | null>(null);
|
||||
|
||||
// 实际播放时间
|
||||
const actualTime = ref(0);
|
||||
|
||||
// 计算当前行的进度
|
||||
const currentProgress = computed(() => {
|
||||
const { startCurrentTime, nextTime, isPlay } = dynamicData.value;
|
||||
if (!startCurrentTime || !nextTime || !isPlay) return 0;
|
||||
const { startCurrentTime, nextTime } = dynamicData.value;
|
||||
if (!startCurrentTime || !nextTime) return 0;
|
||||
|
||||
const duration = nextTime - startCurrentTime;
|
||||
const elapsed = actualTime.value - startCurrentTime;
|
||||
@@ -317,9 +337,8 @@ const updateProgress = () => {
|
||||
};
|
||||
|
||||
// 记录上次更新时间
|
||||
const lastUpdateTime = ref(performance.now());
|
||||
|
||||
// 监听数据更新
|
||||
// 监听据更新
|
||||
watch(
|
||||
() => dynamicData.value,
|
||||
(newData: any) => {
|
||||
@@ -351,29 +370,41 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
// 修改数据更新处理
|
||||
// 修改数据更新处
|
||||
const handleDataUpdate = (parsedData: {
|
||||
nowTime: number;
|
||||
startCurrentTime: number;
|
||||
nextTime: number;
|
||||
isPlay: boolean;
|
||||
nowIndex: number;
|
||||
lrcArray: Array<{ text: string; trText: string }>;
|
||||
lrcTimeArray: number[];
|
||||
allTime: number;
|
||||
playMusic: SongResult;
|
||||
}) => {
|
||||
// 确保数据存在且格式正确
|
||||
if (!parsedData || typeof parsedData.nowTime !== 'number') {
|
||||
if (!parsedData) {
|
||||
console.error('Invalid update data received:', parsedData);
|
||||
return;
|
||||
}
|
||||
// 更新静态数据
|
||||
staticData.value = {
|
||||
lrcArray: parsedData.lrcArray || [],
|
||||
lrcTimeArray: parsedData.lrcTimeArray || [],
|
||||
allTime: parsedData.allTime || 0,
|
||||
playMusic: parsedData.playMusic || {},
|
||||
};
|
||||
|
||||
// 更新动态数据
|
||||
dynamicData.value = {
|
||||
nowTime: parsedData.nowTime,
|
||||
startCurrentTime: parsedData.startCurrentTime,
|
||||
nextTime: parsedData.nextTime,
|
||||
nowTime: parsedData.nowTime || 0,
|
||||
startCurrentTime: parsedData.startCurrentTime || 0,
|
||||
nextTime: parsedData.nextTime || 0,
|
||||
isPlay: parsedData.isPlay,
|
||||
};
|
||||
|
||||
// 更新索引
|
||||
if (typeof parsedData.nowIndex === 'number' && parsedData.nowIndex !== currentIndex.value) {
|
||||
if (typeof parsedData.nowIndex === 'number') {
|
||||
currentIndex.value = parsedData.nowIndex;
|
||||
}
|
||||
};
|
||||
@@ -394,33 +425,7 @@ onMounted(() => {
|
||||
windowData.electron.ipcRenderer.on('receive-lyric', (data: string) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(data);
|
||||
if (parsedData.type === 'init') {
|
||||
// 初始化重置状态
|
||||
currentIndex.value = 0;
|
||||
isInitialized.value = false;
|
||||
|
||||
// 清理可能存在的动画
|
||||
if (animationFrameId.value) {
|
||||
cancelAnimationFrame(animationFrameId.value);
|
||||
animationFrameId.value = null;
|
||||
}
|
||||
|
||||
// 保据格式正确
|
||||
if (Array.isArray(parsedData.lrcArray)) {
|
||||
staticData.value = {
|
||||
lrcArray: parsedData.lrcArray,
|
||||
lrcTimeArray: parsedData.lrcTimeArray || [],
|
||||
allTime: parsedData.allTime || 0,
|
||||
};
|
||||
} else {
|
||||
console.error('Invalid lyric array format:', parsedData);
|
||||
}
|
||||
nextTick(() => {
|
||||
isInitialized.value = true;
|
||||
});
|
||||
} else if (parsedData.type === 'update') {
|
||||
handleDataUpdate(parsedData);
|
||||
}
|
||||
handleDataUpdate(parsedData);
|
||||
} catch (error) {
|
||||
console.error('Error parsing lyric data:', error);
|
||||
}
|
||||
@@ -446,6 +451,7 @@ const handleTop = () => {
|
||||
|
||||
const handleLock = () => {
|
||||
lyricSetting.value.isLock = !lyricSetting.value.isLock;
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', lyricSetting.value.isLock);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -459,6 +465,87 @@ watch(
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 添<><E6B7BB>拖动相关变量
|
||||
const isDragging = ref(false);
|
||||
const startPosition = ref({ x: 0, y: 0 });
|
||||
|
||||
// 处理鼠标按下事件
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
// 如果点击的是控制按钮区域或窗口被锁定,不处理拖动
|
||||
if (
|
||||
lyricSetting.value.isLock ||
|
||||
(e.target as HTMLElement).closest('.control-buttons') ||
|
||||
(e.target as HTMLElement).closest('.font-size-controls')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只响应鼠标左键
|
||||
if (e.button !== 0) return;
|
||||
|
||||
isDragging.value = true;
|
||||
startPosition.value = { x: e.screenX, y: e.screenY };
|
||||
|
||||
// 添加全局鼠标事件监听
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return;
|
||||
|
||||
const deltaX = e.screenX - startPosition.value.x;
|
||||
const deltaY = e.screenY - startPosition.value.y;
|
||||
|
||||
// 发送移动事件到主进程
|
||||
windowData.electron.ipcRenderer.send('lyric-drag-move', { deltaX, deltaY });
|
||||
startPosition.value = { x: e.screenX, y: e.screenY };
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isDragging.value) return;
|
||||
isDragging.value = false;
|
||||
|
||||
// 移除事件监听
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// 添加全局事件监听
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
isDragging.value = false;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const lyricLock = document.getElementById('lyric-lock');
|
||||
if (lyricLock) {
|
||||
lyricLock.onmouseenter = () => {
|
||||
if (lyricSetting.value.isLock) {
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', false);
|
||||
}
|
||||
};
|
||||
lyricLock.onmouseleave = () => {
|
||||
if (lyricSetting.value.isLock) {
|
||||
windowData.electron.ipcRenderer.send('set-ignore-mouse', true);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 添加播放控制相关的函数
|
||||
const handlePlayPause = () => {
|
||||
windowData.electron.ipcRenderer.send('control-back', 'playpause');
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
windowData.electron.ipcRenderer.send('control-back', 'prev');
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
windowData.electron.ipcRenderer.send('control-back', 'next');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -474,67 +561,79 @@ body {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
.control-bar {
|
||||
&-show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
--bg-color: transparent;
|
||||
--text-color: #ffffff;
|
||||
--text-secondary: rgba(255, 255, 255, 0.6);
|
||||
--highlight-color: #1db954;
|
||||
--control-bg: rgba(0, 0, 0, 0.3);
|
||||
--control-bg: rgba(124, 124, 124, 0.3);
|
||||
}
|
||||
|
||||
&.light {
|
||||
--bg-color: transparent;
|
||||
--text-color: #333333;
|
||||
--text-secondary: rgba(51, 51, 51, 0.6);
|
||||
--highlight-color: #1db954;
|
||||
--control-bg: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&.lyric_lock {
|
||||
.control-bar {
|
||||
background: var(--control-bg);
|
||||
|
||||
&-show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
background: var(--control-bg);
|
||||
backdrop-filter: blur(8px);
|
||||
height: 80px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
padding: 0 20px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
visibility 0.2s ease;
|
||||
-webkit-app-region: drag;
|
||||
z-index: 100;
|
||||
|
||||
&-show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
.font-size-controls {
|
||||
-webkit-app-region: no-drag;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.font-size-controls {
|
||||
margin-right: auto; // 将字体控制放在侧
|
||||
padding-right: 20px;
|
||||
.play-controls {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.n-button {
|
||||
.play-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
i {
|
||||
font-size: 16px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,23 +650,21 @@ body {
|
||||
}
|
||||
|
||||
.control-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
border-radius: 8px;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&:hover {
|
||||
background: var(--control-bg);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.active {
|
||||
@@ -578,11 +675,12 @@ body {
|
||||
|
||||
.lyric-container {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
top: 80px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.lyric-scroll {
|
||||
@@ -616,8 +714,7 @@ body {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.lyric-line-passed,
|
||||
&.lyric-line-next {
|
||||
&.lyric-line-passed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
@@ -663,4 +760,38 @@ body {
|
||||
.lyric-line-current {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
.control-buttons {
|
||||
.control-button {
|
||||
&:not(:has(.ri-lock-line)):not(:has(.ri-lock-unlock-line)) {
|
||||
.lyric_lock & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lyric_lock & .font-size-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lyric_lock & .play-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lyric_lock {
|
||||
background: transparent;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#lyric-lock {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 72px;
|
||||
background: var(--control-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
<div class="mv-list-title">
|
||||
<h2>推荐MV</h2>
|
||||
</div>
|
||||
<div class="play-list-type">
|
||||
<n-scrollbar x-scrollable>
|
||||
<div class="categories-wrapper">
|
||||
<span
|
||||
v-for="(category, index) in categories"
|
||||
:key="category.value"
|
||||
class="play-list-type-item"
|
||||
:class="[setAnimationClass('animate__bounceIn'), { active: selectedCategory === category.value }]"
|
||||
:style="getAnimationDelay(index)"
|
||||
@click="selectedCategory = category.value"
|
||||
>
|
||||
{{ category.label }}
|
||||
</span>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
<n-scrollbar :size="100" @scroll="handleScroll">
|
||||
<div v-loading="initLoading" class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
|
||||
<div
|
||||
@@ -10,7 +26,7 @@
|
||||
:key="item.id"
|
||||
class="mv-item"
|
||||
:class="setAnimationClass('animate__bounceIn')"
|
||||
:style="getItemAnimationDelay(index)"
|
||||
:style="getAnimationDelay(index)"
|
||||
>
|
||||
<div class="mv-item-img" @click="handleShowMv(item, index)">
|
||||
<n-image class="mv-item-img-img" :src="getImgUrl(item.cover, '320y180')" lazy preview-disabled />
|
||||
@@ -38,10 +54,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
import { getTopMv } from '@/api/mv';
|
||||
import { getAllMv, getTopMv } from '@/api/mv';
|
||||
import MvPlayer from '@/components/MvPlayer.vue';
|
||||
import { audioService } from '@/services/audioService';
|
||||
import { IMvItem } from '@/type/mv';
|
||||
@@ -62,10 +78,26 @@ const offset = ref(0);
|
||||
const limit = ref(42);
|
||||
const hasMore = ref(true);
|
||||
|
||||
const getItemAnimationDelay = (index: number) => {
|
||||
const currentPageIndex = index % limit.value;
|
||||
return setAnimationDelay(currentPageIndex, 30);
|
||||
};
|
||||
const categories = [
|
||||
{ label: '全部', value: '全部' },
|
||||
{ label: '内地', value: '内地' },
|
||||
{ label: '港台', value: '港台' },
|
||||
{ label: '欧美', value: '欧美' },
|
||||
{ label: '日本', value: '日本' },
|
||||
{ label: '韩国', value: '韩国' },
|
||||
];
|
||||
const selectedCategory = ref('全部');
|
||||
|
||||
watch(selectedCategory, async () => {
|
||||
offset.value = 0;
|
||||
mvList.value = [];
|
||||
hasMore.value = true;
|
||||
await loadMvList();
|
||||
});
|
||||
|
||||
const getAnimationDelay = computed(() => {
|
||||
return (index: number) => setAnimationDelay(index, 30);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMvList();
|
||||
@@ -116,26 +148,26 @@ const playNextMv = async (setLoading: (value: boolean) => void) => {
|
||||
};
|
||||
|
||||
const loadMvList = async () => {
|
||||
if (!hasMore.value || loadingMore.value) return;
|
||||
|
||||
if (offset.value === 0) {
|
||||
initLoading.value = true;
|
||||
} else {
|
||||
loadingMore.value = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getTopMv(limit.value, offset.value);
|
||||
if (!hasMore.value || loadingMore.value) return;
|
||||
if (offset.value === 0) {
|
||||
mvList.value = res.data.data;
|
||||
initLoading.value = true;
|
||||
} else {
|
||||
mvList.value.push(...res.data.data);
|
||||
loadingMore.value = true;
|
||||
}
|
||||
|
||||
hasMore.value = res.data.data.length === limit.value;
|
||||
const params = {
|
||||
limit: limit.value,
|
||||
offset: offset.value,
|
||||
area: selectedCategory.value === '全部' ? '' : selectedCategory.value,
|
||||
};
|
||||
|
||||
const res = selectedCategory.value === '全部' ? await getTopMv(params) : await getAllMv(params);
|
||||
|
||||
const { data } = res.data;
|
||||
mvList.value.push(...data);
|
||||
hasMore.value = data.length === limit.value;
|
||||
offset.value += limit.value;
|
||||
} catch (error) {
|
||||
console.error('加载MV失败:', error);
|
||||
} finally {
|
||||
initLoading.value = false;
|
||||
loadingMore.value = false;
|
||||
@@ -157,12 +189,37 @@ const isPrevDisabled = computed(() => currentIndex.value === 0);
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mv-list {
|
||||
@apply relative h-full w-full;
|
||||
@apply h-full flex-1 flex flex-col overflow-hidden;
|
||||
|
||||
&-title {
|
||||
@apply text-xl font-bold pb-2;
|
||||
}
|
||||
|
||||
// 添加歌单分类样式
|
||||
.play-list-type {
|
||||
.title {
|
||||
@apply text-lg font-bold mb-4;
|
||||
}
|
||||
|
||||
.categories-wrapper {
|
||||
@apply flex items-center py-2 pb-4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-item {
|
||||
@apply py-2 px-3 mr-3 inline-block border border-gray-700 rounded-xl cursor-pointer transition-all duration-300;
|
||||
background-color: #1a1a1a;
|
||||
|
||||
&:hover {
|
||||
@apply bg-green-600/50;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-green-600 border-green-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
@apply grid gap-4 pb-28 mt-2 pr-4;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<n-scrollbar>
|
||||
<div class="set-page">
|
||||
<div class="set-page" :class="setAnimationClass('animate__bounceInLeft')">
|
||||
<div v-if="isElectron" class="set-item">
|
||||
<div>
|
||||
<div class="set-item-title">代理</div>
|
||||
@@ -59,16 +59,20 @@
|
||||
isElectron ? '保存并重启' : '保存'
|
||||
}}</n-button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-black rounded-lg shadow-lg mt-20">
|
||||
<div class="mt-10">
|
||||
<p class="text-sm text-gray-100 text-center cursor-pointer hover:text-green-500" @click="copyQQ">
|
||||
QQ群:789288579
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6 bg-black rounded-lg shadow-lg">
|
||||
<div class="text-gray-100 text-base text-center">支持作者</div>
|
||||
<div class="flex gap-60">
|
||||
<div class="flex flex-col items-center gap-2 cursor-pointer hover:scale-[2] transition-all z-10 bg-black">
|
||||
<n-image :src="alipayQR" alt="支付宝收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||
<div class="flex flex-col items-center gap-2 cursor-none hover:scale-[2] transition-all z-10 bg-black">
|
||||
<n-image :src="alipay" alt="支付宝收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||
<span class="text-sm text-gray-100">支付宝</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-2 cursor-pointer hover:scale-[2] transition-all z-10 bg-black">
|
||||
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||
<div class="flex flex-col items-center gap-2 cursor-none hover:scale-[2] transition-all z-10 bg-black">
|
||||
<n-image :src="wechat" alt="微信收款码" class="w-32 h-32 rounded-lg" preview-disabled />
|
||||
<span class="text-sm text-gray-100">微信支付</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,14 +86,19 @@ import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import config from '@/../package.json';
|
||||
import alipay from '@/assets/alipay.png';
|
||||
import wechat from '@/assets/wechat.png';
|
||||
import store from '@/store';
|
||||
import { setAnimationClass } from '@/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'Setting',
|
||||
});
|
||||
|
||||
const alipayQR = 'https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true';
|
||||
const wechatQR = 'https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true';
|
||||
const message = useMessage();
|
||||
const copyQQ = () => {
|
||||
navigator.clipboard.writeText('789288579');
|
||||
message.success('已复制到剪贴板');
|
||||
};
|
||||
|
||||
const isElectron = ref((window as any).electronAPI !== undefined);
|
||||
const router = useRouter();
|
||||
@@ -109,7 +118,7 @@ const handleSave = () => {
|
||||
if (isElectron.value) {
|
||||
(window as any).electronAPI.restart();
|
||||
}
|
||||
router.back();
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
const openAuthor = () => {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
content: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
highlight: 'var(--highlight-color)',
|
||||
text: 'var(--text-color)',
|
||||
secondary: 'var(--text-secondary)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user