🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能

This commit is contained in:
alger
2025-01-01 02:25:18 +08:00
parent f8d421c9b1
commit 17d20fa299
260 changed files with 78557 additions and 1693 deletions
-10
View File
@@ -1,10 +0,0 @@
declare global {
interface Window {
electronAPI: {
minimize: () => void;
maximize: () => void;
close: () => void;
dragStart: () => void;
};
}
}
+205
View File
@@ -0,0 +1,205 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils';
import { app, BrowserWindow, globalShortcut, ipcMain, Menu, nativeImage, shell, Tray } from 'electron';
import Store from 'electron-store';
import { join } from 'path';
import set from './set.json';
// 导入所有图标
const iconPath = join(__dirname, '../../resources');
const icon = nativeImage.createFromPath(
process.platform === 'darwin'
? join(iconPath, 'icon.icns')
: process.platform === 'win32'
? join(iconPath, 'favicon.ico')
: join(iconPath, 'icon.png')
);
import { loadLyricWindow } from './lyric';
import { startMusicApi } from './server';
let mainWindow: BrowserWindow;
function createWindow(): void {
startMusicApi();
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1200,
height: 780,
show: false,
frame: false,
autoHideMenuBar: true,
icon,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true
}
});
mainWindow.setMinimumSize(1200, 780);
mainWindow.on('ready-to-show', () => {
mainWindow.show();
});
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: 'deny' };
});
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
mainWindow.webContents.openDevTools({ mode: 'detach' });
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
} else {
mainWindow.webContents.openDevTools({ mode: 'detach' });
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
// 创建托盘图标
const trayIcon = nativeImage.createFromPath(join(iconPath, 'icon_16x16.png')).resize({ width: 16, height: 16 });
const tray = new Tray(trayIcon);
// 创建一个上下文菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '显示',
click: () => {
mainWindow.show();
},
},
{
label: '退出',
click: () => {
mainWindow.destroy();
app.quit();
},
},
]);
// 设置系统托盘图标的上下文菜单
tray.setContextMenu(contextMenu);
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
tray.on('click', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
loadLyricWindow(ipcMain, mainWindow);
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.alger.music');
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window);
});
// IPC test
ipcMain.on('ping', () => console.log('pong'));
createWindow();
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('ready', () => {
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
ipcMain.on('minimize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.minimize();
}
});
ipcMain.on('maximize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
if (win.isMaximized()) {
win.unmaximize();
} else {
win.maximize();
}
}
});
ipcMain.on('close-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.destroy();
app.quit();
}
});
ipcMain.on('drag-start', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.webContents.beginFrameSubscription((frameBuffer) => {
event.reply('frame-buffer', frameBuffer);
});
}
});
ipcMain.on('mini-tray', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.hide();
}
});
// 重启
ipcMain.on('restart', () => {
app.relaunch();
app.exit(0);
});
const store = new Store({
name: 'config', // 配置文件名
defaults: {
set: set
}
});
// 定义ipcRenderer监听事件
ipcMain.on('set-store-value', (_, key, value) => {
store.set(key, value);
});
ipcMain.on('get-store-value', (_, key) => {
const value = store.get(key);
_.returnValue = value || '';
});
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
+176
View File
@@ -0,0 +1,176 @@
import { BrowserWindow, IpcMain, screen } from 'electron';
import Store from 'electron-store';
import path, { join } from 'path';
const store = new Store();
let lyricWindow: BrowserWindow | null = null;
const createWin = () => {
console.log('Creating lyric window');
// 获取保存的窗口位置
const windowBounds =
(store.get('lyricWindowBounds') as {
x?: number;
y?: number;
width?: number;
height?: number;
}) || {};
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: width || 800,
height: height || 200,
x: validPosition ? x : undefined,
y: validPosition ? y : undefined,
frame: false,
show: false,
transparent: true,
hasShadow: false,
alwaysOnTop: true,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true
}
});
// 监听窗口关闭事件
lyricWindow.on('closed', () => {
if (lyricWindow) {
lyricWindow.destroy();
lyricWindow = null;
}
});
return lyricWindow;
};
export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {
const showLyricWindow = () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
if (lyricWindow.isMinimized()) {
lyricWindow.restore();
}
lyricWindow.focus();
lyricWindow.show();
return true;
}
return false;
};
ipcMain.on('open-lyric', () => {
console.log('Received open-lyric request');
if (showLyricWindow()) {
return;
}
console.log('Creating new lyric window');
const win = createWin();
if (!win) {
console.error('Failed to create lyric window');
return;
}
if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools({ mode: 'detach' });
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`);
} else {
const distPath = path.resolve(__dirname, '../renderer');
win.loadURL(`file://${distPath}/index.html#/lyric`);
}
win.setMinimumSize(600, 200);
win.setSkipTaskbar(true);
win.once('ready-to-show', () => {
console.log('Lyric window ready to show');
win.show();
});
});
ipcMain.on('send-lyric', (_, data) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
try {
lyricWindow.webContents.send('receive-lyric', data);
} catch (error) {
console.error('Error processing lyric data:', error);
}
}
});
ipcMain.on('top-lyric', (_, data) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setAlwaysOnTop(data);
}
});
ipcMain.on('close-lyric', () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.webContents.send('lyric-window-close');
mainWin.webContents.send('lyric-control-back', 'close');
lyricWindow.destroy();
lyricWindow = null;
}
});
// 处理鼠标事件
ipcMain.on('mouseenter-lyric', () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(true);
}
});
ipcMain.on('mouseleave-lyric', () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(false);
}
});
// 处理拖动移动
ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {
if (!lyricWindow || lyricWindow.isDestroyed()) 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', (_, shouldIgnore) => {
if (!lyricWindow || lyricWindow.isDestroyed()) return;
lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });
});
// 添加播放控制处理
ipcMain.on('control-back', (_, command) => {
console.log('command', command);
if (mainWin && !mainWin.isDestroyed()) {
console.log('Sending control-back command:', command);
mainWin.webContents.send('lyric-control-back', command);
}
});
};
+32
View File
@@ -0,0 +1,32 @@
import { ipcMain } from 'electron';
import Store from 'electron-store';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { unblockMusic } from './unblockMusic';
const store = new Store();
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
}
// 处理解锁音乐请求
ipcMain.handle('unblock-music', async (_, id) => {
return unblockMusic(id);
});
import server from 'netease-cloud-music-api-alger/server';
async function startMusicApi(): Promise<void> {
console.log('MUSIC API STARTED');
const port = (store.get('set') as any).musicApiPort || 30488;
await server.serveNcmApi({
port
});
}
export { startMusicApi };
+8
View File
@@ -0,0 +1,8 @@
{
"isProxy": false,
"noAnimate": false,
"animationSpeed": 1,
"author": "Alger",
"authorUrl": "https://github.com/algerkong",
"musicApiPort": 30488
}
+23
View File
@@ -0,0 +1,23 @@
import match from '@unblockneteasemusic/server';
const unblockMusic = async (id: any): Promise<any> => {
return new Promise((resolve, reject) => {
match(parseInt(id, 10), ['qq', 'migu', 'kugou', 'joox'])
.then((data) => {
resolve({
data: {
data,
params: {
id,
type: 'song'
}
}
});
})
.catch((err) => {
reject(err);
});
});
};
export { unblockMusic };
+18
View File
@@ -0,0 +1,18 @@
import { ElectronAPI } from '@electron-toolkit/preload';
declare global {
interface Window {
electron: ElectronAPI;
api: {
sendLyric: (data: string) => void;
openLyric: () => void;
minimize: () => void;
maximize: () => void;
close: () => void;
dragStart: (data: string) => void;
miniTray: () => void;
restart: () => void;
unblockMusic: (id: number) => Promise<any>;
};
}
}
+32
View File
@@ -0,0 +1,32 @@
import { electronAPI } from '@electron-toolkit/preload';
import { contextBridge, ipcRenderer } from 'electron';
// Custom APIs for renderer
const api = {
minimize: () => ipcRenderer.send('minimize-window'),
maximize: () => ipcRenderer.send('maximize-window'),
close: () => ipcRenderer.send('close-window'),
dragStart: (data) => ipcRenderer.send('drag-start', data),
miniTray: () => ipcRenderer.send('mini-tray'),
restart: () => ipcRenderer.send('restart'),
openLyric: () => ipcRenderer.send('open-lyric'),
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id)
};
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI);
contextBridge.exposeInMainWorld('api', api);
} catch (error) {
console.error(error);
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI;
// @ts-ignore (define in dts)
window.api = api;
}
+3 -3
View File
@@ -13,8 +13,8 @@
<script setup lang="ts">
import { darkTheme, lightTheme } from 'naive-ui';
import { onMounted } from 'vue';
import { isElectron } from '@/utils';
import { isElectron } from '@/hooks/MusicHook';
import homeRouter from '@/router/home';
import store from '@/store';
@@ -30,11 +30,11 @@ onMounted(() => {
if (isMobile.value) {
store.commit(
'setMenus',
homeRouter.filter((item) => item.meta.isMobile),
homeRouter.filter((item) => item.meta.isMobile)
);
console.log(
'qqq ',
homeRouter.filter((item) => item.meta.isMobile),
homeRouter.filter((item) => item.meta.isMobile)
);
}
});
+1 -1
View File
@@ -22,7 +22,7 @@ export function getListByTag(params: IListByTagParams) {
// 根据cat 获取歌单列表
export function getListByCat(params: IListByCatParams) {
return request.get('/top/playlist', {
params,
params
});
}
@@ -41,6 +41,6 @@ export function logout() {
export function loginByCellphone(phone: string, password: string) {
return request.post('/login/cellphone', {
phone,
password,
password
});
}
@@ -1,5 +1,6 @@
import { ILyric } from '@/type/lyric';
import { IPlayMusicUrl } from '@/type/music';
import { isElectron } from '@/utils';
import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
// 根据音乐Id获取音乐播放URl
@@ -18,5 +19,8 @@ export const getMusicLrc = (id: number) => {
};
export const getParsingMusicUrl = (id: number) => {
if (isElectron) {
return window.api.unblockMusic(id);
}
return requestMusic.get<any>('/music', { params: { id } });
};
+7 -7
View File
@@ -1,5 +1,5 @@
import { IData } from '@/type';
import { IMvItem, IMvUrlData } from '@/type/mv';
import { IMvUrlData } from '@/type/mv';
import request from '@/utils/request';
interface MvParams {
@@ -13,7 +13,7 @@ export const getTopMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params,
params
});
};
@@ -22,7 +22,7 @@ export const getAllMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params,
params
});
};
@@ -30,8 +30,8 @@ export const getAllMv = (params: MvParams) => {
export const getMvDetail = (mvid: string) => {
return request.get('/mv/detail', {
params: {
mvid,
},
mvid
}
});
};
@@ -39,7 +39,7 @@ export const getMvDetail = (mvid: string) => {
export const getMvUrl = (id: Number) => {
return request.get<IData<IMvUrlData>>('/mv/url', {
params: {
id,
},
id
}
});
};
@@ -7,6 +7,6 @@ interface IParams {
// 搜索内容
export const getSearch = (params: IParams) => {
return request.get<any>('/cloudsearch', {
params,
params
});
};

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
body {
/* background-color: #000; */
}
.n-popover:has(.music-play) {
border-radius: 1.5rem !important;
}

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+283
View File
@@ -0,0 +1,283 @@
@font-face {
font-family: 'iconfont'; /* Project id 2685283 */
src:
url('iconfont.woff2?t=1703643214551') format('woff2'),
url('iconfont.woff?t=1703643214551') format('woff'),
url('iconfont.ttf?t=1703643214551') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-list:before {
content: '\e603';
}
.icon-maxsize:before {
content: '\e692';
}
.icon-close:before {
content: '\e616';
}
.icon-minisize:before {
content: '\e602';
}
.icon-shuaxin:before {
content: '\e627';
}
.icon-icon_error:before {
content: '\e615';
}
.icon-a-3User:before {
content: '\e601';
}
.icon-Chat:before {
content: '\e605';
}
.icon-Category:before {
content: '\e606';
}
.icon-Document:before {
content: '\e607';
}
.icon-Heart:before {
content: '\e608';
}
.icon-Hide:before {
content: '\e609';
}
.icon-Home:before {
content: '\e60a';
}
.icon-a-Image2:before {
content: '\e60b';
}
.icon-Profile:before {
content: '\e60c';
}
.icon-Search:before {
content: '\e60d';
}
.icon-Paper:before {
content: '\e60e';
}
.icon-Play:before {
content: '\e60f';
}
.icon-Setting:before {
content: '\e610';
}
.icon-a-TicketStar:before {
content: '\e611';
}
.icon-a-VolumeOff:before {
content: '\e612';
}
.icon-a-VolumeUp:before {
content: '\e613';
}
.icon-a-VolumeDown:before {
content: '\e614';
}
.icon-stop:before {
content: '\e600';
}
.icon-next:before {
content: '\e6a9';
}
.icon-prev:before {
content: '\e6ac';
}
.icon-play:before {
content: '\e6aa';
}
.icon-xiasanjiaoxing:before {
content: '\e642';
}
.icon-videofill:before {
content: '\e7c7';
}
.icon-favorfill:before {
content: '\e64b';
}
.icon-favor:before {
content: '\e64c';
}
.icon-loading:before {
content: '\e64f';
}
.icon-search:before {
content: '\e65c';
}
.icon-likefill:before {
content: '\e668';
}
.icon-like:before {
content: '\e669';
}
.icon-notificationfill:before {
content: '\e66a';
}
.icon-notification:before {
content: '\e66b';
}
.icon-evaluate:before {
content: '\e672';
}
.icon-homefill:before {
content: '\e6bb';
}
.icon-link:before {
content: '\e6bf';
}
.icon-roundaddfill:before {
content: '\e6d8';
}
.icon-roundadd:before {
content: '\e6d9';
}
.icon-add:before {
content: '\e6da';
}
.icon-appreciatefill:before {
content: '\e6e3';
}
.icon-forwardfill:before {
content: '\e6ea';
}
.icon-voicefill:before {
content: '\e6f0';
}
.icon-wefill:before {
content: '\e6f4';
}
.icon-keyboard:before {
content: '\e71b';
}
.icon-picfill:before {
content: '\e72c';
}
.icon-markfill:before {
content: '\e730';
}
.icon-presentfill:before {
content: '\e732';
}
.icon-peoplefill:before {
content: '\e735';
}
.icon-read:before {
content: '\e742';
}
.icon-backwardfill:before {
content: '\e74d';
}
.icon-playfill:before {
content: '\e74f';
}
.icon-all:before {
content: '\e755';
}
.icon-hotfill:before {
content: '\e757';
}
.icon-recordfill:before {
content: '\e7a4';
}
.icon-full:before {
content: '\e7bc';
}
.icon-favor_fill_light:before {
content: '\e7ec';
}
.icon-round_favor_fill:before {
content: '\e80a';
}
.icon-round_location_fill:before {
content: '\e80b';
}
.icon-round_like_fill:before {
content: '\e80c';
}
.icon-round_people_fill:before {
content: '\e80d';
}
.icon-round_skin_fill:before {
content: '\e80e';
}
.icon-broadcast_fill:before {
content: '\e81d';
}
.icon-card_fill:before {
content: '\e81f';
}
File diff suppressed because one or more lines are too long
+478
View File
@@ -0,0 +1,478 @@
{
"id": "2685283",
"name": "music",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "1111849",
"name": "list",
"font_class": "list",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "1306794",
"name": "maxsize",
"font_class": "maxsize",
"unicode": "e692",
"unicode_decimal": 59026
},
{
"icon_id": "4437591",
"name": "close",
"font_class": "close",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "5383753",
"name": "minisize",
"font_class": "minisize",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "13075017",
"name": "刷新",
"font_class": "shuaxin",
"unicode": "e627",
"unicode_decimal": 58919
},
{
"icon_id": "24457556",
"name": "icon_error",
"font_class": "icon_error",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "24492642",
"name": "3 User",
"font_class": "a-3User",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "24492643",
"name": "Chat",
"font_class": "Chat",
"unicode": "e605",
"unicode_decimal": 58885
},
{
"icon_id": "24492646",
"name": "Category",
"font_class": "Category",
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "24492661",
"name": "Document",
"font_class": "Document",
"unicode": "e607",
"unicode_decimal": 58887
},
{
"icon_id": "24492662",
"name": "Heart",
"font_class": "Heart",
"unicode": "e608",
"unicode_decimal": 58888
},
{
"icon_id": "24492665",
"name": "Hide",
"font_class": "Hide",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "24492667",
"name": "Home",
"font_class": "Home",
"unicode": "e60a",
"unicode_decimal": 58890
},
{
"icon_id": "24492678",
"name": "Image 2",
"font_class": "a-Image2",
"unicode": "e60b",
"unicode_decimal": 58891
},
{
"icon_id": "24492684",
"name": "Profile",
"font_class": "Profile",
"unicode": "e60c",
"unicode_decimal": 58892
},
{
"icon_id": "24492685",
"name": "Search",
"font_class": "Search",
"unicode": "e60d",
"unicode_decimal": 58893
},
{
"icon_id": "24492687",
"name": "Paper",
"font_class": "Paper",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "24492690",
"name": "Play",
"font_class": "Play",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "24492698",
"name": "Setting",
"font_class": "Setting",
"unicode": "e610",
"unicode_decimal": 58896
},
{
"icon_id": "24492708",
"name": "Ticket Star",
"font_class": "a-TicketStar",
"unicode": "e611",
"unicode_decimal": 58897
},
{
"icon_id": "24492712",
"name": "Volume Off",
"font_class": "a-VolumeOff",
"unicode": "e612",
"unicode_decimal": 58898
},
{
"icon_id": "24492713",
"name": "Volume Up",
"font_class": "a-VolumeUp",
"unicode": "e613",
"unicode_decimal": 58899
},
{
"icon_id": "24492714",
"name": "Volume Down",
"font_class": "a-VolumeDown",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "18875422",
"name": "暂停 停止 灰色",
"font_class": "stop",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "15262786",
"name": "1_music82",
"font_class": "next",
"unicode": "e6a9",
"unicode_decimal": 59049
},
{
"icon_id": "15262807",
"name": "1_music83",
"font_class": "prev",
"unicode": "e6ac",
"unicode_decimal": 59052
},
{
"icon_id": "15262830",
"name": "1_music81",
"font_class": "play",
"unicode": "e6aa",
"unicode_decimal": 59050
},
{
"icon_id": "15367",
"name": "下三角形",
"font_class": "xiasanjiaoxing",
"unicode": "e642",
"unicode_decimal": 58946
},
{
"icon_id": "1096518",
"name": "video_fill",
"font_class": "videofill",
"unicode": "e7c7",
"unicode_decimal": 59335
},
{
"icon_id": "29930",
"name": "favor_fill",
"font_class": "favorfill",
"unicode": "e64b",
"unicode_decimal": 58955
},
{
"icon_id": "29931",
"name": "favor",
"font_class": "favor",
"unicode": "e64c",
"unicode_decimal": 58956
},
{
"icon_id": "29934",
"name": "loading",
"font_class": "loading",
"unicode": "e64f",
"unicode_decimal": 58959
},
{
"icon_id": "29947",
"name": "search",
"font_class": "search",
"unicode": "e65c",
"unicode_decimal": 58972
},
{
"icon_id": "30417",
"name": "like_fill",
"font_class": "likefill",
"unicode": "e668",
"unicode_decimal": 58984
},
{
"icon_id": "30418",
"name": "like",
"font_class": "like",
"unicode": "e669",
"unicode_decimal": 58985
},
{
"icon_id": "30419",
"name": "notification_fill",
"font_class": "notificationfill",
"unicode": "e66a",
"unicode_decimal": 58986
},
{
"icon_id": "30420",
"name": "notification",
"font_class": "notification",
"unicode": "e66b",
"unicode_decimal": 58987
},
{
"icon_id": "30434",
"name": "evaluate",
"font_class": "evaluate",
"unicode": "e672",
"unicode_decimal": 58994
},
{
"icon_id": "33519",
"name": "home_fill",
"font_class": "homefill",
"unicode": "e6bb",
"unicode_decimal": 59067
},
{
"icon_id": "34922",
"name": "link",
"font_class": "link",
"unicode": "e6bf",
"unicode_decimal": 59071
},
{
"icon_id": "38744",
"name": "round_add_fill",
"font_class": "roundaddfill",
"unicode": "e6d8",
"unicode_decimal": 59096
},
{
"icon_id": "38746",
"name": "round_add",
"font_class": "roundadd",
"unicode": "e6d9",
"unicode_decimal": 59097
},
{
"icon_id": "38747",
"name": "add",
"font_class": "add",
"unicode": "e6da",
"unicode_decimal": 59098
},
{
"icon_id": "43903",
"name": "appreciate_fill",
"font_class": "appreciatefill",
"unicode": "e6e3",
"unicode_decimal": 59107
},
{
"icon_id": "52506",
"name": "forward_fill",
"font_class": "forwardfill",
"unicode": "e6ea",
"unicode_decimal": 59114
},
{
"icon_id": "55448",
"name": "voice_fill",
"font_class": "voicefill",
"unicode": "e6f0",
"unicode_decimal": 59120
},
{
"icon_id": "61146",
"name": "we_fill",
"font_class": "wefill",
"unicode": "e6f4",
"unicode_decimal": 59124
},
{
"icon_id": "90847",
"name": "keyboard",
"font_class": "keyboard",
"unicode": "e71b",
"unicode_decimal": 59163
},
{
"icon_id": "127305",
"name": "pic_fill",
"font_class": "picfill",
"unicode": "e72c",
"unicode_decimal": 59180
},
{
"icon_id": "143738",
"name": "mark_fill",
"font_class": "markfill",
"unicode": "e730",
"unicode_decimal": 59184
},
{
"icon_id": "143740",
"name": "present_fill",
"font_class": "presentfill",
"unicode": "e732",
"unicode_decimal": 59186
},
{
"icon_id": "158873",
"name": "people_fill",
"font_class": "peoplefill",
"unicode": "e735",
"unicode_decimal": 59189
},
{
"icon_id": "176313",
"name": "read",
"font_class": "read",
"unicode": "e742",
"unicode_decimal": 59202
},
{
"icon_id": "212324",
"name": "backward_fill",
"font_class": "backwardfill",
"unicode": "e74d",
"unicode_decimal": 59213
},
{
"icon_id": "212328",
"name": "play_fill",
"font_class": "playfill",
"unicode": "e74f",
"unicode_decimal": 59215
},
{
"icon_id": "240126",
"name": "all",
"font_class": "all",
"unicode": "e755",
"unicode_decimal": 59221
},
{
"icon_id": "240128",
"name": "hot_fill",
"font_class": "hotfill",
"unicode": "e757",
"unicode_decimal": 59223
},
{
"icon_id": "747747",
"name": "record_fill",
"font_class": "recordfill",
"unicode": "e7a4",
"unicode_decimal": 59300
},
{
"icon_id": "1005712",
"name": "full",
"font_class": "full",
"unicode": "e7bc",
"unicode_decimal": 59324
},
{
"icon_id": "1512759",
"name": "favor_fill_light",
"font_class": "favor_fill_light",
"unicode": "e7ec",
"unicode_decimal": 59372
},
{
"icon_id": "4110741",
"name": "round_favor_fill",
"font_class": "round_favor_fill",
"unicode": "e80a",
"unicode_decimal": 59402
},
{
"icon_id": "4110743",
"name": "round_location_fill",
"font_class": "round_location_fill",
"unicode": "e80b",
"unicode_decimal": 59403
},
{
"icon_id": "4110745",
"name": "round_like_fill",
"font_class": "round_like_fill",
"unicode": "e80c",
"unicode_decimal": 59404
},
{
"icon_id": "4110746",
"name": "round_people_fill",
"font_class": "round_people_fill",
"unicode": "e80d",
"unicode_decimal": 59405
},
{
"icon_id": "4110750",
"name": "round_skin_fill",
"font_class": "round_skin_fill",
"unicode": "e80e",
"unicode_decimal": 59406
},
{
"icon_id": "11778953",
"name": "broadcast_fill",
"font_class": "broadcast_fill",
"unicode": "e81d",
"unicode_decimal": 59421
},
{
"icon_id": "12625085",
"name": "card_fill",
"font_class": "card_fill",
"unicode": "e81f",
"unicode_decimal": 59423
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+75
View File
@@ -0,0 +1,75 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useDialog: typeof import('naive-ui')['useDialog']
const useId: typeof import('vue')['useId']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useModel: typeof import('vue')['useModel']
const useNotification: typeof import('naive-ui')['useNotification']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+37
View File
@@ -0,0 +1,37 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDropdown: typeof import('naive-ui')['NDropdown']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSlider: typeof import('naive-ui')['NSlider']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTag: typeof import('naive-ui')['NTag']
NTooltip: typeof import('naive-ui')['NTooltip']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}
@@ -15,11 +15,21 @@
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
<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 cursor-none" preview-disabled />
<n-image
:src="alipayQR"
alt="支付宝收款码"
class="w-32 h-32 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">支付宝</span>
</div>
<div class="flex flex-col items-center gap-2">
<n-image :src="wechatQR" alt="微信收款码" class="w-32 h-32 rounded-lg cursor-none" preview-disabled />
<n-image
:src="wechatQR"
alt="微信收款码"
class="w-32 h-32 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">微信支付</span>
</div>
</div>
@@ -49,11 +59,11 @@ const copyQQ = () => {
defineProps({
alipayQR: {
type: String,
required: true,
required: true
},
wechatQR: {
type: String,
required: true,
},
required: true
}
});
</script>
@@ -98,8 +98,8 @@ const props = withDefaults(
}>(),
{
loading: false,
cover: true,
},
cover: true
}
);
const emit = defineEmits(['update:show', 'update:loading']);
@@ -122,7 +122,7 @@ const formatDetail = computed(() => (detail: any) => {
const song = {
artists: detail.ar,
name: detail.al.name,
id: detail.al.id,
id: detail.al.id
};
detail.song = song;
@@ -138,9 +138,9 @@ const handlePlay = () => {
...item,
picUrl: item.al.picUrl,
song: {
artists: item.ar,
},
})),
artists: item.ar
}
}))
);
};
@@ -204,7 +204,7 @@ watch(
if (!props.cover) {
loadingList.value = false;
}
},
}
);
// songList
@@ -218,7 +218,7 @@ watch(
}
loadingList.value = false;
},
{ immediate: true },
{ immediate: true }
);
</script>
@@ -1,7 +1,11 @@
<template>
<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 }">
<div
ref="videoContainerRef"
class="video-container"
:class="{ 'cursor-hidden': !showCursor }"
>
<video
ref="videoRef"
:src="mvUrl"
@@ -86,7 +90,9 @@
下一个
</n-tooltip>
<div class="time-display">{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</div>
<div class="time-display">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</div>
</div>
<div class="right-controls">
@@ -96,14 +102,22 @@
<n-button quaternary circle @click="toggleMute">
<template #icon>
<n-icon size="24">
<i :class="volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'"></i>
<i
:class="volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'"
></i>
</n-icon>
</template>
</n-button>
</template>
{{ volume === 0 ? '取消静音' : '静音' }}
</n-tooltip>
<n-slider v-model:value="volume" :min="0" :max="100" :tooltip="false" class="volume-slider" />
<n-slider
v-model:value="volume"
:min="0"
:max="100"
:tooltip="false"
class="volume-slider"
/>
</div>
<n-tooltip v-if="!props.noList" placement="top">
@@ -111,7 +125,11 @@
<n-button quaternary circle class="play-mode-btn" @click="togglePlayMode">
<template #icon>
<n-icon size="24">
<i :class="playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'"></i>
<i
:class="
playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'
"
></i>
</n-icon>
</template>
</n-button>
@@ -124,7 +142,9 @@
<n-button quaternary circle @click="toggleFullscreen">
<template #icon>
<n-icon size="24">
<i :class="isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"></i>
<i
:class="isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'"
></i>
</n-icon>
</template>
</n-button>
@@ -181,7 +201,7 @@ import { IMvItem } from '@/type/mv';
type PlayMode = 'single' | 'auto';
const PLAY_MODE = {
Single: 'single' as PlayMode,
Auto: 'auto' as PlayMode,
Auto: 'auto' as PlayMode
} as const;
const props = withDefaults(
@@ -193,8 +213,8 @@ const props = withDefaults(
{
show: false,
currentMv: undefined,
noList: false,
},
noList: false
}
);
const emit = defineEmits<{
@@ -310,7 +330,7 @@ watch(
if (newMv) {
await loadMvUrl(newMv);
}
},
}
);
const autoPlayBlocked = ref(false);
@@ -383,11 +403,21 @@ const checkFullscreenAPI = () => {
(videoContainerRef.value as any)?.webkitRequestFullscreen ||
(videoContainerRef.value as any)?.mozRequestFullScreen ||
(videoContainerRef.value as any)?.msRequestFullscreen,
exitFullscreen: doc.exitFullscreen || doc.webkitExitFullscreen || doc.mozCancelFullScreen || doc.msExitFullscreen,
exitFullscreen:
doc.exitFullscreen ||
doc.webkitExitFullscreen ||
doc.mozCancelFullScreen ||
doc.msExitFullscreen,
fullscreenElement:
doc.fullscreenElement || doc.webkitFullscreenElement || doc.mozFullScreenElement || doc.msFullscreenElement,
doc.fullscreenElement ||
doc.webkitFullscreenElement ||
doc.mozFullScreenElement ||
doc.msFullscreenElement,
fullscreenEnabled:
doc.fullscreenEnabled || doc.webkitFullscreenEnabled || doc.mozFullScreenEnabled || doc.msFullscreenEnabled,
doc.fullscreenEnabled ||
doc.webkitFullscreenEnabled ||
doc.mozFullScreenEnabled ||
doc.msFullscreenEnabled
};
};
@@ -13,7 +13,7 @@
? 'animate__bounceIn'
: !isShowAllPlaylistCategory
? 'animate__backOutLeft'
: 'animate__bounceIn',
: 'animate__bounceIn'
) +
' ' +
'type-item-' +
@@ -27,7 +27,11 @@
<div
class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
:style="
setAnimationDelay(
!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30
)
"
@click="handleToggleShowAllPlaylistCategory"
>
{{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
@@ -63,8 +67,8 @@ const getAnimationDelay = computed(() => {
watch(isShowAllPlaylistCategory, (newVal) => {
if (!newVal) {
const elements = playlistCategory.value?.sub.map((item, index) =>
document.querySelector(`.type-item-${index}`),
const elements = playlistCategory.value?.sub.map((_, index) =>
document.querySelector(`.type-item-${index}`)
) as HTMLElement[];
elements
.slice(20)
@@ -75,7 +79,7 @@ watch(isShowAllPlaylistCategory, (newVal) => {
() => {
(element as HTMLElement).style.position = 'absolute';
},
index * DELAY_TIME + 400,
index * DELAY_TIME + 400
);
}
});
@@ -90,7 +94,7 @@ watch(isShowAllPlaylistCategory, (newVal) => {
}
});
},
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME,
(playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME
);
} else {
document.querySelectorAll('.play-list-type-item').forEach((element) => {
@@ -112,8 +116,8 @@ const handleClickPlaylistType = (type: string) => {
router.push({
path: '/list',
query: {
type,
},
type
}
});
};
@@ -38,6 +38,7 @@ import { getNewAlbum } from '@/api/home';
import { getAlbum } from '@/api/list';
import type { IAlbumNew } from '@/type/album';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
import MusicList from '@/components/MusicList.vue';
const albumData = ref<IAlbumNew>();
const loadAlbumList = async () => {
@@ -65,9 +66,9 @@ const handleClick = async (item: any) => {
...res.data.album,
creator: {
avatarUrl: res.data.album.artist.img1v1Url,
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`,
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`
},
description: res.data.album.description,
description: res.data.album.description
};
loadingList.value = false;
};
@@ -10,7 +10,9 @@
:style="setAnimationDelay(0, 100)"
>
<div
:style="setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))"
:style="
setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))
"
class="recommend-singer-item-bg"
></div>
<div
@@ -20,7 +22,11 @@
<div class="font-bold text-xl">每日推荐</div>
<div class="mt-2">
<p v-for="item in dayRecommendData?.dailySongs.slice(0, 5)" :key="item.id" class="text-el">
<p
v-for="item in dayRecommendData?.dailySongs.slice(0, 5)"
:key="item.id"
class="text-el"
>
{{ item.name }}
<br />
</p>
@@ -34,8 +40,13 @@
:class="setAnimationClass('animate__backInRight')"
:style="setAnimationDelay(index + 1, 100)"
>
<div :style="setBackgroundImg(getImgUrl(item.picUrl, '500y500'))" class="recommend-singer-item-bg"></div>
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">{{ item.musicSize }}</div>
<div
:style="setBackgroundImg(getImgUrl(item.picUrl, '500y500'))"
class="recommend-singer-item-bg"
></div>
<div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">
{{ item.musicSize }}
</div>
<div class="recommend-singer-item-info z-10">
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
<i class="iconfont icon-playfill text-xl"></i>
@@ -68,6 +79,7 @@ import router from '@/router';
import { IDayRecommend } from '@/type/day_recommend';
import type { IHotSinger } from '@/type/singer';
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
import MusicList from '@/components/MusicList.vue';
const store = useStore();
@@ -88,13 +100,13 @@ const loadData = async () => {
//
try {
const {
data: { data: dayRecommend },
data: { data: dayRecommend }
} = await getDayRecommend();
//
if (dayRecommend) {
singerData.artists = singerData.artists.slice(0, 4);
}
dayRecommendData.value = dayRecommend;
dayRecommendData.value = dayRecommend as unknown as IDayRecommend;
} catch (error) {
console.error('error', error);
}
@@ -109,8 +121,8 @@ const toSearchSinger = (keyword: string) => {
router.push({
path: '/search',
query: {
keyword,
},
keyword
}
});
};
@@ -9,7 +9,10 @@
>
<!-- 推荐音乐列表 -->
<template v-for="(item, index) in recommendMusic?.result" :key="item.id">
<div :class="setAnimationClass('animate__bounceInUp')" :style="setAnimationDelay(index, 100)">
<div
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 100)"
>
<song-item :item="item" @play="handlePlay" />
</div>
</template>
@@ -1,5 +1,11 @@
<template>
<n-modal v-model:show="showModal" preset="dialog" :show-icon="false" :mask-closable="true" class="install-app-modal">
<n-modal
v-model:show="showModal"
preset="dialog"
:show-icon="false"
:mask-closable="true"
class="install-app-modal"
>
<div class="modal-content">
<div class="modal-header">
<div class="app-icon">
@@ -18,7 +24,10 @@
<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"
<a
class="text-green-500"
target="_blank"
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
>GitHub</a
>
下载最新版本
@@ -31,11 +40,11 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import config from '@/../package.json';
import { isMobile } from '@/utils';
import { isElectron, isMobile } from '@/utils';
import config from '../../../../package.json';
const showModal = ref(false);
const isElectron = ref((window as any).electron !== undefined);
const noPrompt = ref(false);
const closeModal = () => {
@@ -47,7 +56,7 @@ const closeModal = () => {
onMounted(() => {
// electron
if (isElectron.value || isMobile.value) {
if (isElectron || isMobile.value) {
return;
}
@@ -64,10 +73,13 @@ const handleInstall = async (): Promise<void> => {
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 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'));
!isX64 &&
(userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
const getDownloadUrl = (os: string, arch: string): string => {
const version = config.version as string;
@@ -4,12 +4,12 @@ import { setAnimationClass } from '@/utils';
const props = defineProps({
showPop: {
type: Boolean,
default: false,
default: false
},
showClose: {
type: Boolean,
default: true,
},
default: true
}
});
const musicFullClass = computed(() => {
@@ -10,8 +10,8 @@ const isPlay = computed(() => store.state.isPlay as boolean);
defineProps({
height: {
type: String,
default: undefined,
},
default: undefined
}
});
</script>
@@ -1,7 +1,11 @@
<template>
<div class="search-item" :class="item.type" @click="handleClick">
<div class="search-item-img">
<n-image :src="getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '100y100')" lazy preview-disabled />
<n-image
:src="getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '100y100')"
lazy
preview-disabled
/>
<div v-if="item.type === 'mv'" class="play">
<i class="iconfont icon icon-play"></i>
</div>
@@ -19,7 +23,12 @@
:list-info="listInfo"
:cover="false"
/>
<mv-player v-if="item.type === 'mv'" v-model:show="showPop" :current-mv="getCurrentMv()" no-list />
<mv-player
v-if="item.type === 'mv'"
v-model:show="showPop"
:current-mv="getCurrentMv()"
no-list
/>
</div>
</template>
@@ -31,6 +40,7 @@ import MvPlayer from '@/components/MvPlayer.vue';
import { audioService } from '@/services/audioService';
import { IMvItem } from '@/type/mv';
import { getImgUrl } from '@/utils';
import MusicList from '../MusicList.vue';
const props = defineProps<{
item: {
@@ -50,7 +60,7 @@ const listInfo = ref<any>(null);
const getCurrentMv = () => {
return {
id: props.item.id,
name: props.item.name,
name: props.item.name
} as unknown as IMvItem;
};
@@ -69,9 +79,9 @@ const handleClick = async () => {
...res.data.album,
creator: {
avatarUrl: res.data.album.artist.img1v1Url,
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`,
nickname: `${res.data.album.artist.name} - ${res.data.album.company}`
},
description: res.data.album.description,
description: res.data.album.description
};
}
@@ -7,17 +7,20 @@
class="song-item-img"
preview-disabled
:img-props="{
crossorigin: 'anonymous',
crossorigin: 'anonymous'
}"
@load="imageLoad"
/>
<div class="song-item-content">
<div v-if="list" class="song-item-content-wrapper">
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
<n-ellipsis class="song-item-content-title text-ellipsis" line-clamp="1">{{
item.name
}}</n-ellipsis>
<div class="song-item-content-divider">-</div>
<n-ellipsis class="song-item-content-name text-ellipsis" line-clamp="1">
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
>{{ artists.name
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
>
</n-ellipsis>
</div>
@@ -27,8 +30,11 @@
</div>
<div class="song-item-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1">
<span v-for="(artists, artistsindex) in item.ar || item.song.artists" :key="artistsindex"
>{{ artists.name }}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
<span
v-for="(artists, artistsindex) in item.ar || item.song.artists"
:key="artistsindex"
>{{ artists.name
}}{{ artistsindex < (item.ar || item.song.artists).length - 1 ? ' / ' : '' }}</span
>
</n-ellipsis>
</div>
@@ -36,7 +42,11 @@
</div>
<div class="song-item-operating" :class="{ 'song-item-operating-list': list }">
<div v-if="favorite" class="song-item-operating-like">
<i class="iconfont icon-likefill" :class="{ 'like-active': isFavorite }" @click.stop="toggleFavorite"></i>
<i
class="iconfont icon-likefill"
:class="{ 'like-active': isFavorite }"
@click.stop="toggleFavorite"
></i>
</div>
<div
class="song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated"
@@ -69,8 +79,8 @@ const props = withDefaults(
{
mini: false,
list: false,
favorite: true,
},
favorite: true
}
);
const store = useStore();
@@ -79,7 +89,9 @@ const play = computed(() => store.state.play as boolean);
const playMusic = computed(() => store.state.playMusic);
const playLoading = computed(() => playMusic.value.id === props.item.id && playMusic.value.playLoading);
const playLoading = computed(
() => playMusic.value.id === props.item.id && playMusic.value.playLoading
);
//
const isPlaying = computed(() => {
@@ -95,7 +107,7 @@ const imageLoad = async () => {
return;
}
const { backgroundColor } = await getImageBackground(
(songImageRef.value as any).imageRef as unknown as HTMLImageElement,
(songImageRef.value as any).imageRef as unknown as HTMLImageElement
);
// eslint-disable-next-line vue/no-mutating-props
props.item.backgroundColor = backgroundColor;
@@ -0,0 +1,243 @@
<template>
<n-modal
v-model:show="showModal"
preset="dialog"
:show-icon="false"
:mask-closable="true"
class="update-app-modal"
style="width: 800px; max-width: 90vw"
>
<div class="modal-content">
<div class="modal-header">
<div class="app-icon">
<img src="@/assets/logo.png" alt="App Icon" />
</div>
<div class="app-info">
<h2 class="app-name">发现新版本 {{ updateInfo.latestVersion }}</h2>
<p class="app-desc mb-2">当前版本 {{ updateInfo.currentVersion }}</p>
<n-checkbox v-model:checked="noPrompt">不再提示</n-checkbox>
</div>
</div>
<div class="update-info">
<div class="update-title">更新内容</div>
<n-scrollbar style="max-height: 300px">
<div class="update-body" v-html="parsedReleaseNotes"></div>
</n-scrollbar>
</div>
<div class="modal-actions">
<n-button class="cancel-btn" @click="closeModal">暂不更新</n-button>
<n-button type="primary" class="update-btn" @click="handleUpdate">立即更新</n-button>
</div>
<div class="modal-desc mt-4 text-center">
<p class="text-xs text-gray-400">
下载遇到问题
<a
class="text-green-500"
target="_blank"
href="https://github.com/algerkong/AlgerMusicPlayer/releases"
>GitHub</a
>
下载最新版本
</p>
</div>
</div>
</n-modal>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue';
import { marked } from 'marked';
import { checkUpdate } from '@/utils';
import config from '../../../../package.json';
// 配置 marked
marked.setOptions({
breaks: true, // 支持 GitHub 风格的换行
gfm: true // 启用 GitHub 风格的 Markdown
});
interface ReleaseInfo {
tag_name: string;
body?: string;
html_url: string;
assets: Array<{
browser_download_url: string;
name: string;
}>;
}
const showModal = ref(false);
const noPrompt = ref(false);
const updateInfo = ref({
hasUpdate: false,
latestVersion: '',
currentVersion: config.version,
releaseInfo: null as ReleaseInfo | null
});
// 解析 Markdown
const parsedReleaseNotes = computed(() => {
if (!updateInfo.value.releaseInfo?.body) return '';
try {
return marked.parse(updateInfo.value.releaseInfo.body);
} catch (error) {
console.error('Error parsing markdown:', error);
return updateInfo.value.releaseInfo.body;
}
});
const closeModal = () => {
showModal.value = false;
if (noPrompt.value) {
localStorage.setItem('updatePromptDismissed', 'true');
}
};
const checkForUpdates = async () => {
try {
const result = await checkUpdate();
updateInfo.value = result;
// 如果有更新且用户没有选择不再提示,则显示弹窗
if (result.hasUpdate && localStorage.getItem('updatePromptDismissed') !== 'true') {
showModal.value = true;
}
} catch (error) {
console.error('检查更新失败:', error);
}
};
const handleUpdate = async () => {
const { userAgent } = navigator;
const isMac: boolean = userAgent.includes('Mac');
const isWindows: boolean = userAgent.includes('Win');
const isARM: boolean =
userAgent.includes('ARM') || userAgent.includes('arm') || userAgent.includes('OS X');
const isX64: boolean =
userAgent.includes('x86_64') || userAgent.includes('Win64') || userAgent.includes('WOW64');
const isX86: boolean =
!isX64 &&
(userAgent.includes('i686') || userAgent.includes('i386') || userAgent.includes('Win32'));
const getDownloadUrl = (os: string, arch: string): string => {
const version = updateInfo.value.latestVersion;
const setup = os !== 'mac' ? 'Setup_' : '';
return `https://gh.llkk.cc/https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusic_${version}_${setup}${arch}.${os === 'mac' ? 'dmg' : 'exe'}`;
};
const osType: string | null = isMac ? 'mac' : isWindows ? 'windows' : null;
const archType: string | null = isARM ? 'arm64' : isX64 ? 'x64' : isX86 ? 'x86' : null;
const downloadUrl: string | null = osType && archType ? getDownloadUrl(osType, archType) : null;
window.open(downloadUrl || 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
closeModal();
};
onMounted(() => {
checkForUpdates();
});
</script>
<style lang="scss" scoped>
.update-app-modal {
:deep(.n-modal) {
@apply max-w-4xl;
}
.modal-content {
@apply p-6 pb-4;
.modal-header {
@apply flex items-center mb-6;
.app-icon {
@apply w-24 h-24 mr-6 rounded-2xl overflow-hidden;
img {
@apply w-full h-full object-cover;
}
}
.app-info {
@apply flex-1;
.app-name {
@apply text-2xl font-bold mb-2;
}
.app-desc {
@apply text-base text-gray-400;
}
}
}
.update-info {
@apply mb-6 rounded-lg bg-gray-50 dark:bg-gray-800;
.update-title {
@apply text-base font-medium p-4 pb-2;
}
.update-body {
@apply p-4 pt-2 text-gray-600 dark:text-gray-300;
:deep(h1) {
@apply text-xl font-bold mb-3;
}
:deep(h2) {
@apply text-lg font-bold mb-3;
}
:deep(h3) {
@apply text-base font-bold mb-2;
}
:deep(p) {
@apply mb-3 leading-relaxed;
}
:deep(ul) {
@apply list-disc list-inside mb-3;
}
:deep(ol) {
@apply list-decimal list-inside mb-3;
}
:deep(li) {
@apply mb-2 leading-relaxed;
}
:deep(code) {
@apply px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200;
}
:deep(pre) {
@apply p-3 rounded bg-gray-100 dark:bg-gray-700 overflow-x-auto mb-3;
code {
@apply bg-transparent p-0;
}
}
:deep(blockquote) {
@apply pl-4 border-l-4 border-gray-200 dark:border-gray-600 mb-3;
}
:deep(a) {
@apply text-green-500 hover:text-green-600 dark:hover:text-green-400;
}
:deep(hr) {
@apply my-4 border-gray-200 dark:border-gray-600;
}
:deep(table) {
@apply w-full mb-3;
th, td {
@apply px-3 py-2 border border-gray-200 dark:border-gray-600;
}
th {
@apply bg-gray-100 dark:bg-gray-700;
}
}
}
}
.modal-actions {
@apply flex gap-4 mt-6;
.n-button {
@apply flex-1 text-base py-2;
}
.cancel-btn {
@apply bg-gray-800 text-gray-300 border-none;
&:hover {
@apply bg-gray-700;
}
}
.update-btn {
@apply bg-green-600 border-none;
&:hover {
@apply bg-green-500;
}
}
}
}
}
</style>
@@ -13,22 +13,22 @@ export const USER_SET_OPTIONS = [
// },
{
label: '退出登录',
key: 'logout',
key: 'logout'
},
{
label: '设置',
key: 'set',
},
key: 'set'
}
];
export const SEARCH_TYPES = [
{
label: '单曲',
key: 1,
key: 1
},
{
label: '专辑',
key: 10,
key: 10
},
// {
// label: '歌手',
@@ -36,7 +36,7 @@ export const SEARCH_TYPES = [
// },
{
label: '歌单',
key: 1000,
key: 1000
},
// {
// label: '用户',
@@ -44,8 +44,8 @@ export const SEARCH_TYPES = [
// },
{
label: 'MV',
key: 1004,
},
key: 1004
}
// {
// label: '歌词',
// key: 1006,
@@ -1,7 +1,7 @@
import { vLoading } from './loading/index';
const directives = {
loading: vLoading,
loading: vLoading
};
export default directives;
@@ -6,23 +6,23 @@ const vnode: VNode = createVNode(Loading) as VNode;
export const vLoading = {
// 在绑定元素的父组件 及他自己的所有子节点都挂载完成后调用
mounted: (el: HTMLElement, binding: any) => {
mounted: (el: HTMLElement) => {
render(vnode, el);
},
// 在绑定元素的父组件 及他自己的所有子节点都更新后调用
updated: (el: HTMLElement, binding: any) => {
if (binding.value) {
vnode?.component?.exposed.show();
vnode?.component?.exposed?.show();
} else {
vnode?.component?.exposed.hide();
vnode?.component?.exposed?.hide();
}
// 动态添加删除自定义class: loading-parent
formatterClass(el, binding);
},
// 绑定元素的父组件卸载后调用
unmounted: () => {
vnode?.component?.exposed.hide();
},
vnode?.component?.exposed?.hide();
}
};
function formatterClass(el: HTMLElement, binding: any) {
@@ -18,26 +18,26 @@ defineProps({
type: String,
default() {
return '加载中...';
},
}
},
maskBackground: {
type: String,
default() {
return 'rgba(0, 0, 0, 0.05)';
},
}
},
loadingColor: {
type: String,
default() {
return 'rgba(255, 255, 255, 1)';
},
}
},
textColor: {
type: String,
default() {
return 'rgba(255, 255, 255, 1)';
},
},
}
}
});
const isShow = ref(false);
@@ -50,7 +50,7 @@ const hide = () => {
defineExpose({
show,
hide,
isShow,
isShow
});
</script>
<style lang="scss" scoped>
@@ -6,7 +6,11 @@ const useIndexedDB = () => {
const db = ref<IDBDatabase | null>(null); // 数据库引用
// 打开数据库并创建表
const initDB = (dbName: string, version: number, stores: { name: string; keyPath?: string }[]) => {
const initDB = (
dbName: string,
version: number,
stores: { name: string; keyPath?: string }[]
) => {
return new Promise<void>((resolve, reject) => {
const request = indexedDB.open(dbName, version); // 打开数据库请求
@@ -17,7 +21,7 @@ const useIndexedDB = () => {
// 确保对象存储(表)创建
db.createObjectStore(store.name, {
keyPath: store.keyPath || 'id',
autoIncrement: true,
autoIncrement: true
});
}
});
@@ -176,7 +180,7 @@ const useIndexedDB = () => {
getData,
deleteData,
getAllData,
getDataWithPagination,
getDataWithPagination
};
};
@@ -27,13 +27,13 @@ export const useMusicHistory = () => {
() => musicHistory.value,
() => {
musicList.value = musicHistory.value;
},
}
);
return {
musicHistory,
musicList,
addMusic,
delMusic,
delMusic
};
};
@@ -4,11 +4,10 @@ import { audioService } from '@/services/audioService';
import store from '@/store';
import type { ILyricText, SongResult } from '@/type/music';
import { getTextColors } from '@/utils/linearColor';
import { isElectron } from '@/utils';
const windowData = window as any;
export const isElectron = computed(() => !!windowData.electronAPI);
export const lrcArray = ref<ILyricText[]>([]); // 歌词数组
export const lrcTimeArray = ref<number[]>([]); // 歌词时间数组
export const nowTime = ref(0); // 当前播放时间
@@ -50,7 +49,7 @@ watch(
sound.value = audioService.getCurrentSound();
audioServiceOn(audioService);
}
},
}
);
watch(
@@ -60,15 +59,15 @@ watch(
lrcArray.value = playMusic.value.lyric?.lrcArray || [];
lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || [];
// 当歌词数据更新时,如果歌词窗口打开,则发送数据
if (isElectron.value && isLyricWindowOpen.value && lrcArray.value.length > 0) {
if (isElectron && isLyricWindowOpen.value && lrcArray.value.length > 0) {
sendLyricToWin();
}
});
},
{
deep: true,
immediate: true,
},
immediate: true
}
);
export const audioServiceOn = (audio: typeof audioService) => {
@@ -85,12 +84,12 @@ export const audioServiceOn = (audio: typeof audioService) => {
nowIndex.value = newIndex;
currentLrcProgress.value = 0;
// 当歌词索引更新时,发送歌词数据
if (isElectron.value && isLyricWindowOpen.value) {
if (isElectron && isLyricWindowOpen.value) {
sendLyricToWin();
}
}
// 定期发送歌词数据更新
if (isElectron.value && isLyricWindowOpen.value) {
if (isElectron && isLyricWindowOpen.value) {
sendLyricToWin();
}
}, 50);
@@ -101,7 +100,7 @@ export const audioServiceOn = (audio: typeof audioService) => {
store.commit('setPlayMusic', false);
clearInterval(interval);
// 暂停时也发送一次状态更新
if (isElectron.value && isLyricWindowOpen.value) {
if (isElectron && isLyricWindowOpen.value) {
sendLyricToWin();
}
});
@@ -185,7 +184,7 @@ export const getLrcStyle = (index: number) => {
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent',
transition: 'background-image 0.1s linear',
transition: 'background-image 0.1s linear'
};
}
return {};
@@ -241,7 +240,7 @@ export const useLyricProgress = () => {
return {
currentLrcProgress,
getLrcStyle,
getLrcStyle
};
};
@@ -259,29 +258,29 @@ export const getCurrentLrc = () => {
const index = getLrcIndex(nowTime.value);
return {
currentLrc: lrcArray.value[index],
nextLrc: lrcArray.value[index + 1],
nextLrc: lrcArray.value[index + 1]
};
};
// 获取一句歌词播放时间几秒到几秒
export const getLrcTimeRange = (index: number) => ({
currentTime: lrcTimeArray.value[index],
nextTime: lrcTimeArray.value[index + 1],
nextTime: lrcTimeArray.value[index + 1]
});
// 监听歌词数组变化,当切换歌曲时重新初始化歌词窗口
watch(
() => lrcArray.value,
(newLrcArray) => {
if (newLrcArray.length > 0 && isElectron.value && isLyricWindowOpen.value) {
if (newLrcArray.length > 0 && isElectron && isLyricWindowOpen.value) {
sendLyricToWin();
}
},
}
);
// 发送歌词更新数据
export const sendLyricToWin = () => {
if (!isElectron.value || !isLyricWindowOpen.value) {
if (!isElectron || !isLyricWindowOpen.value) {
console.log('Cannot send lyric: electron or lyric window not available');
return;
}
@@ -299,9 +298,9 @@ export const sendLyricToWin = () => {
lrcArray: lrcArray.value,
lrcTimeArray: lrcTimeArray.value,
allTime: allTime.value,
playMusic: playMusic.value,
playMusic: playMusic.value
};
windowData.electronAPI.sendLyric(JSON.stringify(updateData));
window.api.sendLyric(JSON.stringify(updateData));
}
} catch (error) {
console.error('Error sending lyric update:', error);
@@ -309,13 +308,13 @@ export const sendLyricToWin = () => {
};
export const openLyric = () => {
if (!isElectron.value) return;
if (!isElectron) return;
console.log('Opening lyric window with current song:', playMusic.value?.name);
isLyricWindowOpen.value = !isLyricWindowOpen.value;
if (isLyricWindowOpen.value) {
setTimeout(() => {
windowData.electronAPI.openLyric();
window.api.openLyric();
sendLyricToWin();
}, 500);
sendLyricToWin();
@@ -326,14 +325,13 @@ export const openLyric = () => {
// 添加关闭歌词窗口的方法
export const closeLyric = () => {
if (!isElectron.value) return;
if (!isElectron) 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);
if (isElectron) {
windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => {
switch (command) {
case 'playpause':
if (store.state.play) {
@@ -16,6 +16,7 @@ const getSongUrl = async (id: number) => {
try {
if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id);
console.log('res', res);
url = res.data.data.url;
}
} catch (error) {
@@ -60,13 +61,16 @@ export const useMusicListHook = () => {
src: [nextSongUrl],
html5: true,
preload: true,
autoplay: false,
autoplay: false
});
return sound;
};
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
const songs = state.playList.slice(Math.max(0, startIndex), Math.min(endIndex, state.playList.length));
const songs = state.playList.slice(
Math.max(0, startIndex),
Math.min(endIndex, state.playList.length)
);
const detailedSongs = await Promise.all(
songs.map(async (song: SongResult) => {
@@ -75,7 +79,7 @@ export const useMusicListHook = () => {
return await getSongDetail(song);
}
return song;
}),
})
);
// 加载下一首的歌词
const nextSong = detailedSongs[0];
@@ -153,13 +157,13 @@ export const useMusicListHook = () => {
});
return {
lrcTimeArray: times,
lrcArray: lyrics,
lrcArray: lyrics
};
} catch (err) {
console.error('Error loading lyrics:', err);
return {
lrcTimeArray: [],
lrcArray: [],
lrcArray: []
};
}
};
@@ -186,6 +190,6 @@ export const useMusicListHook = () => {
nextPlay,
prevPlay,
play,
pause,
pause
};
};
-2
View File
@@ -18,7 +18,6 @@
@apply overflow-ellipsis overflow-hidden whitespace-nowrap;
}
.theme-dark {
--bg-color: #000;
--text-color: #fff;
@@ -57,4 +56,3 @@
--text-color-300: #3d3d3d;
--primary-color: #22c55e;
}
+58
View File
@@ -0,0 +1,58 @@
<!doctype html>
<html lang="zh-CN">
<head>
<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="keywords"
content="AlgerMusic, AlgerMusicPlayer, 网抑云, 音乐播放器, 在线音乐, 免费音乐, 歌词显示, 音乐下载, AlgerKong, 网易云音乐"
/>
<!-- 作者信息 -->
<meta name="author" content="AlgerKong" />
<meta name="author-url" content="https://github.com/algerkong" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="网抑云音乐" />
<!-- 资源预加载 -->
<link rel="preload" href="./assets/icon/iconfont.css" as="style" />
<link rel="preload" href="./assets/css/animate.css" as="style" />
<link rel="preload" href="./assets/css/base.css" as="style" />
<!-- 样式表 -->
<link rel="stylesheet" href="./assets/icon/iconfont.css" />
<link rel="stylesheet" href="./assets/css/animate.css" />
<link rel="stylesheet" href="./assets/css/base.css" />
<script defer src="https://cn.vercount.one/js"></script>
<!-- 动画配置 -->
<style>
:root {
--animate-delay: 0.5s;
}
</style>
</head>
<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>
<script type="module" src="./main.ts"></script>
</body>
</html>
@@ -28,19 +28,20 @@
<play-bar v-if="isPlay" />
</div>
<install-app-modal></install-app-modal>
<update-modal />
</div>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent } from 'vue';
import { computed, defineAsyncComponent, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';
import InstallAppModal from '@/components/common/InstallAppModal.vue';
import PlayBottom from '@/components/common/PlayBottom.vue';
import { isElectron } from '@/hooks/MusicHook';
import homeRouter from '@/router/home';
import { isMobile } from '@/utils';
import { isElectron, isMobile } from '@/utils';
import UpdateModal from '@/components/common/UpdateModal.vue';
const keepAliveInclude = computed(() =>
homeRouter
@@ -49,7 +50,7 @@ const keepAliveInclude = computed(() =>
})
.map((item) => {
return item.name.charAt(0).toUpperCase() + item.name.slice(1);
}),
})
);
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
@@ -62,6 +63,11 @@ const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean);
const { menus } = store.state;
const route = useRoute();
onMounted(() => {
store.dispatch('initializeSettings');
store.dispatch('initializeTheme');
});
</script>
<style lang="scss" scoped>
@@ -4,16 +4,23 @@
<div class="app-menu" :class="{ 'app-menu-expanded': isText }">
<div class="app-menu-header">
<div class="app-menu-logo" @click="isText = !isText">
<img src="/icon.png" class="w-9 h-9" alt="logo" />
<img :src="icon" class="w-9 h-9" alt="logo" />
</div>
</div>
<div class="app-menu-list">
<div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
<router-link class="app-menu-item-link" :to="item.path">
<i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></i>
<span v-if="isText" class="app-menu-item-text ml-3" :class="isChecked(index) ? 'text-green-500' : ''">{{
item.meta.title
}}</span>
<i
class="iconfont app-menu-item-icon"
:style="iconStyle(index)"
:class="item.meta.icon"
></i>
<span
v-if="isText"
class="app-menu-item-text ml-3"
:class="isChecked(index) ? 'text-green-500' : ''"
>{{ item.meta.title }}</span
>
</router-link>
</div>
</div>
@@ -24,23 +31,25 @@
<script lang="ts" setup>
import { useRoute } from 'vue-router';
import icon from '@/assets/icon.png';
const props = defineProps({
size: {
type: String,
default: '26px',
default: '26px'
},
color: {
type: String,
default: '#aaa',
default: '#aaa'
},
selectColor: {
type: String,
default: '#10B981',
default: '#10B981'
},
menus: {
type: Array as any,
default: () => [],
},
default: () => []
}
});
const route = useRoute();
@@ -49,7 +58,7 @@ watch(
() => route.path,
async (newParams) => {
path.value = newParams;
},
}
);
const isChecked = (index: number) => {
@@ -59,7 +68,7 @@ const isChecked = (index: number) => {
const iconStyle = (index: number) => {
const style = {
fontSize: props.size,
color: isChecked(index) ? props.selectColor : props.color,
color: isChecked(index) ? props.selectColor : props.color
};
return style;
};
@@ -69,7 +78,7 @@ const isText = ref(false);
<style lang="scss" scoped>
.app-menu {
@apply flex-col items-center justify-center bg-light dark:bg-black transition-all duration-300 w-[100px] px-1;
@apply flex-col items-center justify-center transition-all duration-300 w-[100px] px-1;
}
.app-menu-expanded {
@@ -8,13 +8,23 @@
>
<div id="drawer-target">
<div class="drawer-back"></div>
<div class="music-img" :style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }">
<n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '500y500')" class="img" lazy preview-disabled />
<div
class="music-img"
:style="{ color: textColors.theme === 'dark' ? '#000000' : '#ffffff' }"
>
<n-image
ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '500y500')"
class="img"
lazy
preview-disabled
/>
<div>
<div class="music-content-name">{{ playMusic.name }}</div>
<div class="music-content-singer">
<span v-for="(item, index) in playMusic.ar || playMusic.song.artists" :key="index">
{{ item.name }}{{ index < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
{{ item.name
}}{{ index < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
</span>
</div>
</div>
@@ -61,7 +71,14 @@
import { useDebounceFn } from '@vueuse/core';
import { onBeforeUnmount, ref, watch } from 'vue';
import { lrcArray, nowIndex, playMusic, setAudioTime, textColors, useLyricProgress } from '@/hooks/MusicHook';
import {
lrcArray,
nowIndex,
playMusic,
setAudioTime,
textColors,
useLyricProgress
} from '@/hooks/MusicHook';
import { getImgUrl } from '@/utils';
import { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';
@@ -76,12 +93,12 @@ const isDark = ref(false);
const props = defineProps({
musicFull: {
type: Boolean,
default: false,
default: false
},
background: {
type: String,
default: '',
},
default: ''
}
});
//
@@ -120,7 +137,7 @@ watch(
lrcScroll('instant');
});
}
},
}
);
//
@@ -129,7 +146,10 @@ watch(
(newBg) => {
if (!newBg) {
textColors.value = getTextColors();
document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(false));
document.documentElement.style.setProperty(
'--hover-bg-color',
getHoverBackgroundColor(false)
);
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
return;
@@ -149,11 +169,14 @@ watch(
textColors.value = getTextColors(newBg);
isDark.value = textColors.value.active === '#000000';
document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(isDark.value));
document.documentElement.style.setProperty(
'--hover-bg-color',
getHoverBackgroundColor(isDark.value)
);
document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);
document.documentElement.style.setProperty('--text-color-active', textColors.value.active);
},
{ immediate: true },
{ immediate: true }
);
// useLyricProgress 使
@@ -173,13 +196,13 @@ const getLrcStyle = (index: number) => {
.replace(/#ffffff8a/g, `${colors.primary}`),
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
color: 'transparent',
color: 'transparent'
};
}
// 使
return {
color: colors.primary,
color: colors.primary
};
};
@@ -191,7 +214,7 @@ onBeforeUnmount(() => {
});
defineExpose({
lrcScroll,
lrcScroll
});
</script>
@@ -5,7 +5,9 @@
<div
class="music-play-bar"
:class="setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')"
:class="
setAnimationClass('animate__bounceInUp') + ' ' + (musicFullVisible ? 'play-bar-opcity' : '')
"
:style="{
color: musicFullVisible
? textColors.theme === 'dark'
@@ -13,18 +15,32 @@
: '#ffffff'
: store.state.theme === 'dark'
? '#ffffff'
: '#000000',
: '#000000'
}"
>
<div class="music-time custom-slider">
<n-slider v-model:value="timeSlider" :step="1" :max="allTime" :min="0" :format-tooltip="formatTooltip"></n-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, '500y500')" class="play-bar-img" lazy preview-disabled />
<n-image
:src="getImgUrl(playMusic?.picUrl, '500y500')"
class="play-bar-img"
lazy
preview-disabled
/>
<div class="hover-arrow">
<div class="hover-content">
<!-- <i class="ri-arrow-up-s-line text-3xl" :class="{ 'ri-arrow-down-s-line': musicFullVisible }"></i> -->
<i class="text-3xl" :class="musicFullVisible ? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line'"></i>
<i
class="text-3xl"
:class="musicFullVisible ? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line'"
></i>
<span class="hover-text">{{ musicFullVisible ? '收起' : '展开' }}歌词</span>
</div>
</div>
@@ -37,9 +53,13 @@
</div>
<div class="music-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1">
<span v-for="(artists, artistsindex) in playMusic.ar || playMusic.song.artists" :key="artistsindex"
<span
v-for="(artists, artistsindex) in playMusic.ar || playMusic.song.artists"
:key="artistsindex"
>{{ artists.name
}}{{ artistsindex < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}</span
}}{{
artistsindex < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : ''
}}</span
>
</n-ellipsis>
</div>
@@ -72,7 +92,11 @@
</n-tooltip>
<n-tooltip v-if="!isMobile" trigger="hover" :z-index="9999999">
<template #trigger>
<i class="iconfont icon-likefill" :class="{ 'like-active': isFavorite }" @click="toggleFavorite"></i>
<i
class="iconfont icon-likefill"
:class="{ 'like-active': isFavorite }"
@click="toggleFavorite"
></i>
</template>
喜欢
</n-tooltip>
@@ -126,9 +150,16 @@ import { useTemplateRef } from 'vue';
import { useStore } from 'vuex';
import SongItem from '@/components/common/SongItem.vue';
import { allTime, isElectron, isLyricWindowOpen, nowTime, openLyric, sound, textColors } from '@/hooks/MusicHook';
import {
allTime,
isLyricWindowOpen,
nowTime,
openLyric,
sound,
textColors
} from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music';
import { getImgUrl, isMobile, secondToMinute, setAnimationClass } from '@/utils';
import { getImgUrl, isMobile, secondToMinute, setAnimationClass, isElectron } from '@/utils';
import MusicFull from './MusicFull.vue';
@@ -147,7 +178,7 @@ watch(
async () => {
background.value = playMusic.value.backgroundColor as string;
},
{ immediate: true, deep: true },
{ immediate: true, deep: true }
);
// 使 useThrottleFn seek
@@ -160,7 +191,7 @@ const throttledSeek = useThrottleFn((value: number) => {
// timeSlider
const timeSlider = computed({
get: () => nowTime.value,
set: throttledSeek,
set: throttledSeek
});
const formatTooltip = (value: number) => {
@@ -168,7 +199,9 @@ const formatTooltip = (value: number) => {
};
//
const audioVolume = ref(localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1);
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) {
@@ -187,7 +220,7 @@ const volumeSlider = computed({
localStorage.setItem('volume', (value / 100).toString());
sound.value.volume(value / 100);
audioVolume.value = value / 100;
},
}
});
//
@@ -15,7 +15,9 @@
<template #suffix>
<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>
<div>
{{ searchTypeOptions.find((item) => item.key === store.state.searchType)?.label }}
</div>
<i class="iconfont icon-xiasanjiaoxing"></i>
</div>
</n-dropdown>
@@ -63,10 +65,19 @@
</template>
</n-switch>
</div>
<div class="menu-item" @click="restartApp">
<i class="iconfont ri-restart-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 class="version-info">
<span class="version-number">{{ updateInfo.currentVersion }}</span>
<n-tag v-if="updateInfo.hasUpdate" type="success" size="small" class="ml-1">
New {{ updateInfo.latestVersion }}
</n-tag>
</div>
</div>
</div>
</div>
@@ -81,18 +92,19 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, watchEffect } from 'vue';
import { onMounted, ref, watchEffect, computed } from 'vue';
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';
import { getImgUrl, checkUpdate } from '@/utils';
import config from '../../../../package.json';
const router = useRouter();
const store = useStore();
@@ -125,6 +137,10 @@ watchEffect(() => {
}
});
const restartApp = () => {
window.electron.ipcRenderer.send('restart');
};
const toLogin = () => {
router.push('/login');
};
@@ -133,11 +149,12 @@ const toLogin = () => {
onMounted(() => {
loadHotSearchKeyword();
loadPage();
checkForUpdates();
});
const isDarkTheme = computed({
get: () => store.state.theme === 'dark',
set: () => store.commit('toggleTheme'),
set: () => store.commit('toggleTheme')
});
//
@@ -157,8 +174,8 @@ const search = () => {
router.push({
path: '/search',
query: {
keyword: value,
},
keyword: value
}
});
};
@@ -195,8 +212,28 @@ const toGithub = () => {
window.open('https://github.com/algerkong/AlgerMusicPlayer', '_blank');
};
const updateInfo = ref({
hasUpdate: false,
latestVersion: '',
currentVersion: config.version,
releaseInfo: null
});
const checkForUpdates = async () => {
try {
const result = await checkUpdate();
updateInfo.value = result;
} catch (error) {
console.error('检查更新失败:', error);
}
};
const toGithubRelease = () => {
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
if (updateInfo.value.hasUpdate) {
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
} else {
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
}
};
</script>
@@ -268,9 +305,13 @@ const toGithubRelease = () => {
@apply mr-1 text-lg text-gray-500 dark:text-gray-400;
}
.download-btn {
@apply ml-auto text-xs px-2 py-0.5 rounded;
@apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;
.version-info {
@apply ml-auto flex items-center;
.version-number {
@apply text-xs px-2 py-0.5 rounded;
@apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;
}
}
}
}
@@ -15,20 +15,19 @@
<script setup lang="ts">
import { useDialog } from 'naive-ui';
import { isElectron } from '@/hooks/MusicHook';
import { isElectron } from '@/utils';
const dialog = useDialog();
const windowData = window as any;
const minimize = () => {
if (!isElectron.value) {
if (!isElectron) {
return;
}
windowData.electronAPI.minimize();
window.api.minimize();
};
const close = () => {
if (!isElectron.value) {
if (!isElectron) {
return;
}
dialog.warning({
@@ -37,19 +36,19 @@ const close = () => {
positiveText: '最小化',
negativeText: '关闭',
onPositiveClick: () => {
windowData.electronAPI.miniTray();
window.api.minimize();
},
onNegativeClick: () => {
windowData.electronAPI.close();
},
window.api.close();
}
});
};
const drag = (event: MouseEvent) => {
if (!isElectron.value) {
if (!isElectron) {
return;
}
windowData.electronAPI.dragStart(event);
window.api.dragStart(event as unknown as string);
};
</script>
@@ -8,16 +8,16 @@
defineProps({
lrcList: {
type: Array,
default: () => [],
default: () => []
},
lrcIndex: {
type: Number,
default: 0,
default: 0
},
lrcTime: {
type: Number,
default: 0,
},
default: 0
}
});
</script>
@@ -6,9 +6,9 @@ const layoutRouter = [
title: '首页',
icon: 'icon-Home',
keepAlive: true,
isMobile: true,
isMobile: true
},
component: () => import('@/views/home/index.vue'),
component: () => import('@/views/home/index.vue')
},
{
path: '/search',
@@ -18,9 +18,9 @@ const layoutRouter = [
noScroll: true,
icon: 'icon-Search',
keepAlive: true,
isMobile: true,
isMobile: true
},
component: () => import('@/views/search/index.vue'),
component: () => import('@/views/search/index.vue')
},
{
path: '/list',
@@ -29,9 +29,9 @@ const layoutRouter = [
title: '歌单',
icon: 'icon-Paper',
keepAlive: true,
isMobile: true,
isMobile: true
},
component: () => import('@/views/list/index.vue'),
component: () => import('@/views/list/index.vue')
},
{
path: '/mv',
@@ -40,9 +40,9 @@ const layoutRouter = [
title: 'MV',
icon: 'icon-recordfill',
keepAlive: true,
isMobile: true,
isMobile: true
},
component: () => import('@/views/mv/index.vue'),
component: () => import('@/views/mv/index.vue')
},
// {
// path: '/history',
@@ -61,8 +61,8 @@ const layoutRouter = [
meta: {
title: '收藏历史',
icon: 'icon-a-TicketStar',
keepAlive: true,
},
keepAlive: true
}
},
{
path: '/user',
@@ -72,9 +72,9 @@ const layoutRouter = [
icon: 'icon-Profile',
keepAlive: true,
noScroll: true,
isMobile: true,
isMobile: true
},
component: () => import('@/views/user/index.vue'),
component: () => import('@/views/user/index.vue')
},
{
path: '/set',
@@ -83,9 +83,9 @@ const layoutRouter = [
title: '设置',
icon: 'ri-settings-3-fill',
keepAlive: true,
noScroll: true,
noScroll: true
},
component: () => import('@/views/set/index.vue'),
},
component: () => import('@/views/set/index.vue')
}
];
export default layoutRouter;
@@ -9,9 +9,9 @@ const loginRouter = {
mate: {
keepAlive: true,
title: '登录',
icon: 'icon-Home',
icon: 'icon-Home'
},
component: () => import('@/views/login/index.vue'),
component: () => import('@/views/login/index.vue')
};
const setRouter = {
@@ -20,24 +20,24 @@ const setRouter = {
mate: {
keepAlive: true,
title: '设置',
icon: 'icon-Home',
icon: 'icon-Home'
},
component: () => import('@/views/set/index.vue'),
component: () => import('@/views/set/index.vue')
};
const routes = [
{
path: '/',
component: AppLayout,
children: [...homeRouter, loginRouter, setRouter],
children: [...homeRouter, loginRouter, setRouter]
},
{
path: '/lyric',
component: () => import('@/views/lyric/index.vue'),
},
component: () => import('@/views/lyric/index.vue')
}
];
export default createRouter({
routes,
history: createWebHashHistory(),
history: createWebHashHistory()
});
@@ -12,7 +12,9 @@ class AudioService {
src: [url],
html5: true,
autoplay: true,
volume: localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1,
volume: localStorage.getItem('volume')
? parseFloat(localStorage.getItem('volume') as string)
: 1
});
return this.currentSound;
@@ -4,6 +4,7 @@ import { useMusicListHook } from '@/hooks/MusicListHook';
import homeRouter from '@/router/home';
import type { SongResult } from '@/type/music';
import { applyTheme, getCurrentTheme, ThemeType } from '@/utils/theme';
import { isElectron } from '@/utils';
// 默认设置
const defaultSettings = {
@@ -11,7 +12,7 @@ const defaultSettings = {
noAnimate: false,
animationSpeed: 1,
author: 'Alger',
authorUrl: 'https://github.com/algerkong',
authorUrl: 'https://github.com/algerkong'
};
function getLocalStorageItem<T>(key: string, defaultValue: T): T {
@@ -54,7 +55,7 @@ const state: State = {
searchType: 1,
favoriteList: getLocalStorageItem('favoriteList', []),
playMode: getLocalStorageItem('playMode', 0),
theme: getCurrentTheme(),
theme: getCurrentTheme()
};
const { handlePlayMusic, nextPlay, prevPlay } = useMusicListHook();
@@ -84,9 +85,12 @@ const mutations = {
},
setSetData(state: State, setData: any) {
state.setData = setData;
const isElectron = (window as any).electronAPI !== undefined;
if (isElectron) {
(window as any).electron.ipcRenderer.setStoreValue('set', JSON.parse(JSON.stringify(setData)));
// (window as any).electron.ipcRenderer.setStoreValue(
// 'set',
// JSON.parse(JSON.stringify(setData))
// );
window.electron.ipcRenderer.send('set-store-value', 'set', JSON.parse(JSON.stringify(setData)));
} else {
localStorage.setItem('appSettings', JSON.stringify(setData));
}
@@ -108,22 +112,21 @@ const mutations = {
toggleTheme(state: State) {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
applyTheme(state.theme);
},
}
};
const actions = {
initializeSettings({ commit }: { commit: any }) {
const isElectron = (window as any).electronAPI !== undefined;
if (isElectron) {
const setData = (window as any).electron.ipcRenderer.getStoreValue('set');
// const setData = (window as any).electron.ipcRenderer.getStoreValue('set');
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
commit('setSetData', setData || defaultSettings);
} else {
const savedSettings = localStorage.getItem('appSettings');
if (savedSettings) {
commit('setSetData', {
...defaultSettings,
...JSON.parse(savedSettings),
...JSON.parse(savedSettings)
});
} else {
commit('setSetData', defaultSettings);
@@ -132,13 +135,13 @@ const actions = {
},
initializeTheme({ state }: { state: State }) {
applyTheme(state.theme);
},
}
};
const store = createStore({
state,
mutations,
actions,
actions
});
export default store;
+22
View File
@@ -0,0 +1,22 @@
export interface IElectronAPI {
minimize: () => void;
maximize: () => void;
close: () => void;
dragStart: (data: string) => void;
miniTray: () => void;
restart: () => void;
openLyric: () => void;
sendLyric: (data: string) => void;
unblockMusic: (id: number) => Promise<string>;
store: {
get: (key: string) => Promise<any>;
set: (key: string, value: any) => Promise<boolean>;
delete: (key: string) => Promise<boolean>;
};
}
declare global {
interface Window {
api: IElectronAPI;
}
}
@@ -1,4 +1,6 @@
import { computed } from 'vue';
import axios from 'axios';
import config from '../../../package.json';
import store from '@/store';
@@ -42,7 +44,7 @@ export const secondToMinute = (s: number) => {
// 格式化数字 千,万, 百万, 千万,亿
const units = [
{ value: 1e8, symbol: '亿' },
{ value: 1e4, symbol: '万' },
{ value: 1e4, symbol: '万' }
];
export const formatNumber = (num: string | number) => {
@@ -60,7 +62,8 @@ export const getIsMc = () => {
if (!windowData.electron) {
return false;
}
if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) {
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');
if (setData.isProxy) {
return true;
}
return false;
@@ -86,7 +89,7 @@ export const getImgUrl = (url: string | undefined, size: string = '') => {
export const isMobile = computed(() => {
const flag = navigator.userAgent.match(
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i,
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
);
store.state.isMobile = !!flag;
@@ -95,3 +98,47 @@ export const isMobile = computed(() => {
if (flag) document.documentElement.classList.add('mobile');
return !!flag;
});
export const isElectron = (window as any).electron !== undefined;
/**
*
* @returns {Promise<{hasUpdate: boolean, latestVersion: string, currentVersion: string}>}
*/
export const checkUpdate = async () => {
try {
const response = await axios.get('https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest');
const latestVersion = response.data.tag_name.replace('v', '');
const currentVersion = config.version;
console.log(latestVersion, currentVersion);
// 版本号比较
const latest = latestVersion.split('.').map(Number);
const current = currentVersion.split('.').map(Number);
let hasUpdate = false;
for (let i = 0; i < 3; i++) {
if (latest[i] > current[i]) {
hasUpdate = true;
break;
} else if (latest[i] < current[i]) {
break;
}
}
return {
hasUpdate,
latestVersion,
currentVersion,
releaseInfo: response.data
};
} catch (error) {
console.error('检查更新失败:', error);
return {
hasUpdate: false,
latestVersion: '',
currentVersion: config.version,
releaseInfo: null
};
}
};
@@ -8,13 +8,13 @@ export const getImageLinearBackground = async (imageSrc: string): Promise<IColor
const primaryColor = await getImagePrimaryColor(imageSrc);
return {
backgroundColor: generateGradientBackground(primaryColor),
primaryColor,
primaryColor
};
} catch (error) {
console.error('error', error);
return {
backgroundColor: '',
primaryColor: '',
primaryColor: ''
};
}
};
@@ -24,13 +24,13 @@ export const getImageBackground = async (img: HTMLImageElement): Promise<IColor>
const primaryColor = await getImageColor(img);
return {
backgroundColor: generateGradientBackground(primaryColor),
primaryColor,
primaryColor
};
} catch (error) {
console.error('error', error);
return {
backgroundColor: '',
primaryColor: '',
primaryColor: ''
};
}
};
@@ -207,7 +207,10 @@ export const interpolateRGB = (start: number, end: number, progress: number) =>
return Math.round(start + (end - start) * progress);
};
export const createGradientString = (colors: { r: number; g: number; b: number }[], percentages = [0, 50, 100]) => {
export const createGradientString = (
colors: { r: number; g: number; b: number }[],
percentages = [0, 50, 100]
) => {
return `linear-gradient(to bottom, ${colors
.map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`)
.join(', ')})`;
@@ -217,6 +220,7 @@ export const getTextColors = (gradient: string = ''): ITextColors => {
const defaultColors = {
primary: 'rgba(255, 255, 255, 0.54)',
active: '#ffffff',
theme: 'light'
};
if (!gradient) return defaultColors;
@@ -231,7 +235,7 @@ export const getTextColors = (gradient: string = ''): ITextColors => {
return {
primary: isDark ? 'rgba(0, 0, 0, 0.54)' : 'rgba(255, 255, 255, 0.54)',
active: isDark ? '#000000' : '#ffffff',
theme: isDark ? 'dark' : 'light',
theme: isDark ? 'dark' : 'light'
};
};
@@ -243,7 +247,7 @@ export const animateGradient = (
oldGradient: string,
newGradient: string,
onUpdate: (gradient: string) => void,
duration = 1000,
duration = 1000
) => {
const startColors = parseGradient(oldGradient);
const endColors = parseGradient(newGradient);
@@ -258,7 +262,7 @@ export const animateGradient = (
const currentColors = startColors.map((startColor, i) => ({
r: interpolateRGB(startColor.r, endColors[i].r, progress),
g: interpolateRGB(startColor.g, endColors[i].g, progress),
b: interpolateRGB(startColor.b, endColors[i].b, progress),
b: interpolateRGB(startColor.b, endColors[i].b, progress)
}));
onUpdate(createGradientString(currentColors));
+77
View File
@@ -0,0 +1,77 @@
import axios, { InternalAxiosRequestConfig } from 'axios';
const setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set')
// 扩展请求配置接口
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
retryCount?: number;
}
const baseURL = window.electron ? `http://127.0.0.1:${setData.musicApiPort}` : import.meta.env.VITE_API;
const request = axios.create({
baseURL,
timeout: 5000
});
// 最大重试次数
const MAX_RETRIES = 3;
// 重试延迟(毫秒)
const RETRY_DELAY = 500;
// 请求拦截器
request.interceptors.request.use(
(config: CustomAxiosRequestConfig) => {
// 初始化重试次数
config.retryCount = 0;
// 在请求发送之前做一些处理
// 在get请求params中添加timestamp
if (config.method === 'get') {
config.params = {
...config.params,
timestamp: Date.now()
};
const token = localStorage.getItem('token');
if (token) {
config.params.cookie = token;
}
}
return config;
},
(error) => {
// 当请求异常时做一些处理
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const config = error.config as CustomAxiosRequestConfig;
// 如果没有配置重试次数,则初始化为0
if (!config || !config.retryCount) {
config.retryCount = 0;
}
// 检查是否还可以重试
if (config.retryCount < MAX_RETRIES) {
config.retryCount++;
// 延迟重试
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
// 重新发起请求
return request(config);
}
return Promise.reject(error);
}
);
export default request;
@@ -3,7 +3,7 @@ import axios from 'axios';
const baseURL = `${import.meta.env.VITE_API_MUSIC}`;
const request = axios.create({
baseURL,
timeout: 10000,
timeout: 10000
});
// 请求拦截器
@@ -14,7 +14,7 @@ request.interceptors.request.use(
(error) => {
// 当请求异常时做一些处理
return Promise.reject(error);
},
}
);
export default request;
@@ -58,8 +58,8 @@ const currentPage = ref(1);
const props = defineProps({
isComponent: {
type: Boolean,
default: false,
},
default: false
}
});
// ID
@@ -88,7 +88,7 @@ const getFavoriteSongs = async () => {
if (res.data.songs) {
const newSongs = res.data.songs.map((song: SongResult) => ({
...song,
picUrl: song.al?.picUrl || '',
picUrl: song.al?.picUrl || ''
}));
//
@@ -131,7 +131,7 @@ watch(
noMore.value = false;
getFavoriteSongs();
},
{ deep: true, immediate: true },
{ deep: true, immediate: true }
);
const handlePlay = () => {
@@ -37,9 +37,10 @@ import { getMusicDetail } from '@/api/music';
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import type { SongResult } from '@/type/music';
import { setAnimationClass, setAnimationDelay } from '@/utils';
import SongItem from '@/components/common/SongItem.vue';
defineOptions({
name: 'History',
name: 'History'
});
const store = useStore();
@@ -75,7 +76,7 @@ const getHistorySongs = async () => {
return {
...song,
picUrl: song.al?.picUrl || '',
count: historyItem?.count || 0,
count: historyItem?.count || 0
};
});
@@ -19,11 +19,15 @@
</template>
<script lang="ts" setup>
import PlaylistType from '@/components/PlaylistType.vue';
import RecommendAlbum from '@/components/RecommendAlbum.vue';
import RecommendSinger from '@/components/RecommendSinger.vue';
import RecommendSonglist from '@/components/RecommendSonglist.vue';
import { isMobile } from '@/utils';
import FavoriteList from '@/views/favorite/index.vue';
defineOptions({
name: 'Home',
name: 'Home'
});
</script>
@@ -74,7 +74,7 @@ import type { IPlayListSort } from '@/type/playlist';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
defineOptions({
name: 'List',
name: 'List'
});
const TOTAL_ITEMS = 42; //
@@ -124,7 +124,7 @@ const loadList = async (type: string, isLoadMore = false) => {
const params = {
cat: type === '每日推荐' ? '' : type,
limit: TOTAL_ITEMS,
offset: page.value * TOTAL_ITEMS,
offset: page.value * TOTAL_ITEMS
};
const { data } = await getListByCat(params);
if (isLoadMore) {
@@ -167,10 +167,10 @@ const loadPlaylistCategory = async () => {
sub: [
{
name: '每日推荐',
category: 0,
category: 0
},
...data.sub,
],
...data.sub
]
};
};
@@ -207,7 +207,7 @@ watch(
loading.value = true;
loadList(newParams.type as string);
}
},
}
);
</script>
@@ -8,7 +8,7 @@ import { checkQr, createQr, getQrKey, getUserDetail, loginByCellphone } from '@/
import { setAnimationClass } from '@/utils';
defineOptions({
name: 'Login',
name: 'Login'
});
const message = useMessage();
@@ -63,7 +63,7 @@
:class="{
'lyric-line-current': index === currentIndex,
'lyric-line-passed': index < currentIndex,
'lyric-line-next': index === currentIndex + 1,
'lyric-line-next': index === currentIndex + 1
}"
>
<div class="lyric-text" :style="{ fontSize: `${fontSize}px` }">
@@ -71,7 +71,11 @@
{{ line.text || '' }}
</span>
</div>
<div v-if="line.trText" class="lyric-translation" :style="{ fontSize: `${fontSize * 0.6}px` }">
<div
v-if="line.trText"
class="lyric-translation"
:style="{ fontSize: `${fontSize * 0.6}px` }"
>
{{ line.trText }}
</div>
</div>
@@ -89,7 +93,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { SongResult } from '@/type/music';
defineOptions({
name: 'Lyric',
name: 'Lyric'
});
const windowData = window as any;
const containerRef = ref<HTMLElement | null>(null);
@@ -112,7 +116,7 @@ const staticData = ref<{
lrcArray: [],
lrcTimeArray: [],
allTime: 0,
playMusic: {} as SongResult,
playMusic: {} as SongResult
});
//
@@ -120,7 +124,7 @@ const dynamicData = ref({
nowTime: 0,
startCurrentTime: 0,
nextTime: 0,
isPlay: true,
isPlay: true
});
const lyricSetting = ref({
@@ -129,8 +133,8 @@ const lyricSetting = ref({
: {
isTop: false,
theme: 'dark',
isLock: false,
}),
isLock: false
})
});
let hideControlsTimer: number | null = null;
@@ -177,7 +181,7 @@ watch(
if (newLock) {
isHovering.value = false;
}
},
}
);
onMounted(() => {
@@ -196,7 +200,7 @@ const wrapperStyle = computed(() => {
if (!containerHeight.value) {
return {
transform: 'translateY(0)',
transition: 'none',
transition: 'none'
};
}
@@ -204,13 +208,15 @@ const wrapperStyle = computed(() => {
const containerCenter = containerHeight.value / 2;
// padding
const currentLineTop = currentIndex.value * lineHeight.value + containerHeight.value * 0.2 + lineHeight.value; // padding
const currentLineTop =
currentIndex.value * lineHeight.value + containerHeight.value * 0.2 + lineHeight.value; // padding
// 使
const targetOffset = containerCenter - currentLineTop;
// padding
const contentHeight = staticData.value.lrcArray.length * lineHeight.value + containerHeight.value * 0.4; // padding20vh
const contentHeight =
staticData.value.lrcArray.length * lineHeight.value + containerHeight.value * 0.4; // padding20vh
//
const minOffset = -(contentHeight - containerHeight.value);
@@ -221,12 +227,12 @@ const wrapperStyle = computed(() => {
return {
transform: `translateY(${finalOffset}px)`,
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
};
});
const lyricLineStyle = computed(() => ({
height: `${lineHeight.value}px`,
height: `${lineHeight.value}px`
}));
//
const updateContainerHeight = () => {
@@ -311,7 +317,7 @@ const getLyricStyle = (index: number) => {
background: `linear-gradient(to right, var(--highlight-color) ${progress}%, var(--text-color) ${progress}%)`,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
transition: 'all 0.1s linear',
transition: 'all 0.1s linear'
};
};
@@ -353,7 +359,7 @@ watch(
updateProgress();
}
},
{ deep: true },
{ deep: true }
);
//
@@ -367,7 +373,7 @@ watch(
cancelAnimationFrame(animationFrameId.value);
animationFrameId.value = null;
}
},
}
);
//
@@ -392,7 +398,7 @@ const handleDataUpdate = (parsedData: {
lrcArray: parsedData.lrcArray || [],
lrcTimeArray: parsedData.lrcTimeArray || [],
allTime: parsedData.allTime || 0,
playMusic: parsedData.playMusic || {},
playMusic: parsedData.playMusic || {}
};
//
@@ -400,7 +406,7 @@ const handleDataUpdate = (parsedData: {
nowTime: parsedData.nowTime || 0,
startCurrentTime: parsedData.startCurrentTime || 0,
nextTime: parsedData.nextTime || 0,
isPlay: parsedData.isPlay,
isPlay: parsedData.isPlay
};
//
@@ -422,7 +428,7 @@ onMounted(() => {
window.addEventListener('resize', updateContainerHeight);
//
windowData.electron.ipcRenderer.on('receive-lyric', (data: string) => {
windowData.electron.ipcRenderer.on('receive-lyric', (_, data) => {
try {
const parsedData = JSON.parse(data);
handleDataUpdate(parsedData);
@@ -444,10 +450,10 @@ const checkTheme = () => {
}
};
const handleTop = () => {
lyricSetting.value.isTop = !lyricSetting.value.isTop;
windowData.electron.ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
};
// const handleTop = () => {
// lyricSetting.value.isTop = !lyricSetting.value.isTop;
// windowData.electron.ipcRenderer.send('top-lyric', lyricSetting.value.isTop);
// };
const handleLock = () => {
lyricSetting.value.isLock = !lyricSetting.value.isLock;
@@ -463,7 +469,7 @@ watch(
(newValue: any) => {
localStorage.setItem('lyricData', JSON.stringify(newValue));
},
{ deep: true },
{ deep: true }
);
//
@@ -749,8 +755,8 @@ body {
body {
background-color: transparent !important;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
}
.lyric-content {
@@ -7,7 +7,10 @@
v-for="(category, index) in categories"
:key="category.value"
class="play-list-type-item"
:class="[setAnimationClass('animate__bounceIn'), { active: selectedCategory === category.value }]"
:class="[
setAnimationClass('animate__bounceIn'),
{ active: selectedCategory === category.value }
]"
:style="getAnimationDelay(index)"
@click="selectedCategory = category.value"
>
@@ -17,7 +20,11 @@
</n-scrollbar>
</div>
<n-scrollbar :size="100" @scroll="handleScroll">
<div v-loading="initLoading" class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
<div
v-loading="initLoading"
class="mv-list-content"
:class="setAnimationClass('animate__bounceInLeft')"
>
<div
v-for="(item, index) in mvList"
:key="item.id"
@@ -26,7 +33,12 @@
: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 />
<n-image
class="mv-item-img-img"
:src="getImgUrl(item.cover, '320y180')"
lazy
preview-disabled
/>
<div class="top">
<div class="play-count">{{ formatNumber(item.playCount) }}</div>
<i class="iconfont icon-videofill"></i>
@@ -61,7 +73,7 @@ import { IMvItem } from '@/type/mv';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
defineOptions({
name: 'Mv',
name: 'Mv'
});
const showMv = ref(false);
@@ -81,7 +93,7 @@ const categories = [
{ label: '港台', value: '港台' },
{ label: '欧美', value: '欧美' },
{ label: '日本', value: '日本' },
{ label: '韩国', value: '韩国' },
{ label: '韩国', value: '韩国' }
];
const selectedCategory = ref('全部');
@@ -157,7 +169,7 @@ const loadMvList = async () => {
const params = {
limit: limit.value,
offset: offset.value,
area: selectedCategory.value === '全部' ? '' : selectedCategory.value,
area: selectedCategory.value === '全部' ? '' : selectedCategory.value
};
const res = selectedCategory.value === '全部' ? await getTopMv(params) : await getAllMv(params);
@@ -15,7 +15,9 @@
class="hot-search-item"
@click.stop="loadSearch(item.searchWord, 1)"
>
<span class="hot-search-item-count" :class="{ 'hot-search-item-count-3': index < 3 }">{{ index + 1 }}</span>
<span class="hot-search-item-count" :class="{ 'hot-search-item-count-3': index < 3 }">{{
index + 1
}}</span>
{{ item.searchWord }}
</div>
</template>
@@ -68,9 +70,10 @@ import { getSearch } from '@/api/search';
import SongItem from '@/components/common/SongItem.vue';
import type { IHotSearch } from '@/type/search';
import { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
import SearchItem from '@/components/common/SearchItem.vue';
defineOptions({
name: 'Search',
name: 'Search'
});
const route = useRoute();
@@ -98,7 +101,7 @@ watch(
() => store.state.searchValue,
(value) => {
loadSearch(value);
},
}
);
const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
@@ -117,7 +120,7 @@ const loadSearch = async (keywords: any, type: any = null) => {
picUrl: item.cover,
playCount: item.playCount,
desc: item.artists.map((artist: any) => artist.name).join('/'),
type: 'mv',
type: 'mv'
}));
const playlists = (data.result.playlists || []).map((item: any) => ({
@@ -125,7 +128,7 @@ const loadSearch = async (keywords: any, type: any = null) => {
picUrl: item.coverImgUrl,
playCount: item.playCount,
desc: item.creator.nickname,
type: 'playlist',
type: 'playlist'
}));
// songs map
@@ -140,7 +143,7 @@ const loadSearch = async (keywords: any, type: any = null) => {
songs,
albums,
mvs,
playlists,
playlists
};
searchDetailLoading.value = false;
@@ -152,7 +155,7 @@ watch(
if (path === '/search') {
store.state.searchValue = route.query.keyword;
}
},
}
);
const handlePlay = () => {
@@ -22,12 +22,14 @@
</div>
<n-switch v-model:value="setData.isProxy" />
</div> -->
<div class="set-item">
<div class="set-item" v-if="isElectron">
<div>
<div class="set-item-title">关闭动画效果</div>
<div class="set-item-content">关闭所有页面动画效果</div>
<div class="set-item-title">音乐API端口</div>
<div class="set-item-content">
修改后需要重启应用
</div>
</div>
<n-switch v-model:value="setData.noAnimate" />
<n-input-number v-model:value="setData.musicApiPort" />
</div>
<div class="set-item">
<div>
@@ -45,7 +47,7 @@
:marks="{
0.1: '极慢',
1: '正常',
3: '极快',
3: '极快'
}"
:disabled="setData.noAnimate"
class="w-40"
@@ -56,43 +58,109 @@
<div class="set-item">
<div>
<div class="set-item-title">版本</div>
<div class="set-item-content">当前已是最新版本</div>
<div class="set-item-content">
{{ updateInfo.currentVersion }}
<template v-if="updateInfo.hasUpdate">
<n-tag type="success" class="ml-2">发现新版本 {{ updateInfo.latestVersion }}</n-tag>
</template>
</div>
</div>
<div class="flex items-center gap-2">
<n-button
:type="updateInfo.hasUpdate ? 'primary' : 'default'"
size="small"
:loading="checking"
@click="checkForUpdates(true)"
>
{{ checking ? '检查中...' : '检查更新' }}
</n-button>
<n-button
v-if="updateInfo.hasUpdate"
type="success"
size="small"
@click="openReleasePage"
>
前往更新
</n-button>
</div>
<div>{{ config.version }}</div>
</div>
<div class="set-item cursor-pointer hover:text-green-500 hover:bg-green-950 transition-all" @click="openAuthor">
<div
class="set-item cursor-pointer hover:text-green-500 hover:bg-green-950 transition-all"
@click="openAuthor"
>
<div>
<div class="set-item-title">作者</div>
<div class="set-item-content">algerkong github</div>
</div>
<div>{{ setData.author }}</div>
</div>
<div class="set-item">
<div>
<div class="set-item-title">重启</div>
<div class="set-item-content">重启应用</div>
</div>
<n-button type="primary" @click="restartApp">重启</n-button>
</div>
</div>
<PlayBottom/>
</n-scrollbar>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { useStore } from 'vuex';
import config from '@/../package.json';
import { isElectron } from '@/hooks/MusicHook';
import { isElectron, checkUpdate } from '@/utils';
import config from '../../../../package.json';
import PlayBottom from '@/components/common/PlayBottom.vue';
const store = useStore();
const setData = computed({
get: () => store.state.setData,
set: (value) => store.commit('setSetData', value),
const checking = ref(false);
const updateInfo = ref({
hasUpdate: false,
latestVersion: '',
currentVersion: config.version,
releaseInfo: null
});
const setData = computed(() => store.state.setData);
watch(() => setData.value, (newVal) => {
store.commit('setSetData', newVal)
}, { deep: true });
const isDarkTheme = computed({
get: () => store.state.theme === 'dark',
set: () => store.commit('toggleTheme'),
set: () => store.commit('toggleTheme')
});
const openAuthor = () => {
window.open(setData.value.authorUrl);
};
const restartApp = () => {
window.electron.ipcRenderer.send('restart');
};
const message = useMessage();
const checkForUpdates = async (isClick = false) => {
checking.value = true;
try {
const result = await checkUpdate();
updateInfo.value = result;
if (!result.hasUpdate && isClick) {
message.success('当前已是最新版本');
}
} finally {
checking.value = false;
}
};
const openReleasePage = () => {
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest');
};
onMounted(() => {
checkForUpdates();
});
</script>
<style lang="scss" scoped>
@@ -36,10 +36,17 @@
class="play-list-item"
@click="showPlaylist(item.id, item.name)"
>
<n-image :src="getImgUrl(item.coverImgUrl, '50y50')" class="play-list-item-img" lazy preview-disabled />
<n-image
:src="getImgUrl(item.coverImgUrl, '50y50')"
class="play-list-item-img"
lazy
preview-disabled
/>
<div class="play-list-item-info">
<div class="play-list-item-name">{{ item.name }}</div>
<div class="play-list-item-count">{{ item.trackCount }}播放{{ item.playCount }}</div>
<div class="play-list-item-count">
{{ item.trackCount }}播放{{ item.playCount }}
</div>
</div>
</div>
<play-bottom />
@@ -47,7 +54,12 @@
</div>
</div>
</div>
<div v-if="!isMobile" v-loading="infoLoading" class="right" :class="setAnimationClass('animate__fadeInRight')">
<div
v-if="!isMobile"
v-loading="infoLoading"
class="right"
:class="setAnimationClass('animate__fadeInRight')"
>
<div class="title">听歌排行</div>
<div class="record-list">
<n-scrollbar>
@@ -90,7 +102,7 @@ import type { IUserDetail } from '@/type/user';
import { getImgUrl, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';
defineOptions({
name: 'User',
name: 'User'
});
const store = useStore();
@@ -119,7 +131,7 @@ const loadPage = async () => {
recordList.value = recordData.allData.map((item: any) => ({
...item,
...item.song,
picUrl: item.song.al.picUrl,
picUrl: item.song.al.picUrl
}));
infoLoading.value = false;
};
@@ -141,7 +153,7 @@ const showPlaylist = async (id: number, name: string) => {
listLoading.value = true;
list.value = {
name,
name
} as Playlist;
const { data } = await getListDetail(id);
list.value = data.playlist;
View File
-34
View File
@@ -1,34 +0,0 @@
import axios from 'axios';
const baseURL = `${import.meta.env.VITE_API}`;
const request = axios.create({
baseURL,
timeout: 10000,
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 在请求发送之前做一些处理
// 在get请求params中添加timestamp
if (config.method === 'get') {
config.params = {
...config.params,
timestamp: Date.now(),
};
const token = localStorage.getItem('token');
if (token) {
config.params.cookie = token;
}
}
return config;
},
(error) => {
// 当请求异常时做一些处理
return Promise.reject(error);
},
);
export default request;