🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能
@@ -1,10 +0,0 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
minimize: () => void;
|
||||
maximize: () => void;
|
||||
close: () => void;
|
||||
dragStart: () => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"isProxy": false,
|
||||
"noAnimate": false,
|
||||
"animationSpeed": 1,
|
||||
"author": "Alger",
|
||||
"authorUrl": "https://github.com/algerkong",
|
||||
"musicApiPort": 30488
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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>;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -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 } });
|
||||
};
|
||||
@@ -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 |
@@ -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 |
@@ -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';
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -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')
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -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; // 上下padding各20vh
|
||||
const contentHeight =
|
||||
staticData.value.lrcArray.length * lineHeight.value + containerHeight.value * 0.4; // 上下padding各20vh
|
||||
|
||||
// 计算最小和最大偏移量
|
||||
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;
|
||||
@@ -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;
|
||||