feat: 添加eslint 和 桌面歌词(未完成)

This commit is contained in:
alger
2024-05-16 18:54:30 +08:00
parent 5e8676a039
commit a9e5bb33e4
65 changed files with 2724 additions and 2320 deletions

13
.eslintignore Normal file
View File

@@ -0,0 +1,13 @@
snapshot*
dist
lib
es
esm
node_modules
static
cypress
script/test/cypress
_site
temp*
static/
!.prettierrc.js

133
.eslintrc Normal file
View File

@@ -0,0 +1,133 @@
{
"extends": [
"plugin:@typescript-eslint/recommended",
"eslint-config-airbnb-base",
"@vue/typescript/recommended",
"plugin:vue/vue3-recommended",
"plugin:vue-scoped-css/base",
"plugin:prettier/recommended"
],
"env": {
"browser": true,
"node": true,
"jest": true,
"es6": true
},
"globals": {
"defineProps": "readonly",
"defineEmits": "readonly"
},
"plugins": [
"vue",
"@typescript-eslint",
"simple-import-sort"
],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module",
"allowImportExportEverywhere": true,
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"import/extensions": [
".js",
".jsx",
".ts",
".tsx"
]
},
"rules": {
"no-console": "off",
"no-continue": "off",
"no-restricted-syntax": "off",
"no-plusplus": "off",
"no-param-reassign": "off",
"no-shadow": "off",
"guard-for-in": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"import/no-extraneous-dependencies": "off",
"import/prefer-default-export": "off",
"import/first": "off", // https://github.com/vuejs/vue-eslint-parser/issues/58
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"vue/first-attribute-linebreak": 0,
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-types": "off",
"class-methods-use-this": "off", // 因为AxiosCancel必须实例化而能静态化所以加的规则如果有办法解决可以取消
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
},
"overrides": [
{
"files": [
"*.vue"
],
"rules": {
"vue/component-name-in-template-casing": [
2,
"kebab-case"
],
"vue/require-default-prop": 0,
"vue/multi-word-component-names": 0,
"vue/no-reserved-props": 0,
"vue/no-v-html": 0,
"vue-scoped-css/enforce-style-type": [
"error",
{
"allows": [
"scoped"
]
}
]
}
},
{
"files": [
"*.ts",
"*.tsx"
], // https://github.com/typescript-eslint eslint-recommended
"rules": {
"constructor-super": "off", // ts(2335) & ts(2377)
"getter-return": "off", // ts(2378)
"no-const-assign": "off", // ts(2588)
"no-dupe-args": "off", // ts(2300)
"no-dupe-class-members": "off", // ts(2393) & ts(2300)
"no-dupe-keys": "off", // ts(1117)
"no-func-assign": "off", // ts(2539)
"no-import-assign": "off", // ts(2539) & ts(2540)
"no-new-symbol": "off", // ts(2588)
"no-obj-calls": "off", // ts(2349)
"no-redeclare": "off", // ts(2451)
"no-setter-return": "off", // ts(2408)
"no-this-before-super": "off", // ts(2376)
"no-undef": "off", // ts(2304)
"no-unreachable": "off", // ts(7027)
"no-unsafe-negation": "off", // ts(2365) & ts(2360) & ts(2358)
"no-var": "error", // ts transpiles let/const to var, so no need for vars any more
"prefer-const": "error", // ts provides better types with const
"prefer-rest-params": "error", // ts provides better types with rest args over arguments
"prefer-spread": "error", // ts transpiles spread to apply, so no need for manual apply
"valid-typeof": "off" // ts(2367)
}
}
]
}

39
.prettierrc.js Normal file
View File

@@ -0,0 +1,39 @@
module.exports = {
// 一行最多 120 字符..
printWidth: 120,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: true,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾需要有逗号
trailingComma: 'all',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// vue 文件中的 script 和 style 内不用缩进
vueIndentScriptAndStyle: false,
// 换行符使用 lf
endOfLine: 'lf',
};

122
app.js
View File

@@ -1,9 +1,10 @@
const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron') const { app, BrowserWindow, ipcMain, Tray, Menu, globalShortcut, nativeImage } = require('electron');
const path = require('path') const path = require('path');
const Store = require('electron-store'); const Store = require('electron-store');
const setJson = require('./electron/set.json') const setJson = require('./electron/set.json');
const { loadLyricWindow } = require('./electron/lyric');
let mainWin = null let mainWin = null;
function createWindow() { function createWindow() {
mainWin = new BrowserWindow({ mainWin = new BrowserWindow({
width: 1200, width: 1200,
@@ -11,129 +12,132 @@ function createWindow() {
frame: false, frame: false,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
// contextIsolation: false,
preload: path.join(__dirname, '/electron/preload.js'), preload: path.join(__dirname, '/electron/preload.js'),
}, },
}) });
const win = mainWin const win = mainWin;
win.setMinimumSize(1200, 780) win.setMinimumSize(1200, 780);
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools({ mode: 'detach' }) win.webContents.openDevTools({ mode: 'detach' });
win.loadURL('http://localhost:4678/') win.loadURL('http://localhost:4678/');
} else { } else {
win.loadURL(`file://${__dirname}/dist/index.html`) win.loadURL(`file://${__dirname}/dist/index.html`);
} }
const image = nativeImage.createFromPath(path.join(__dirname, 'public/icon.png')) const image = nativeImage.createFromPath(path.join(__dirname, 'public/icon.png'));
const tray = new Tray(image) const tray = new Tray(image);
// 创建一个上下文菜单 // 创建一个上下文菜单
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
label: '显示', label: '显示',
click: () => { click: () => {
win.show() win.show();
}, },
}, },
{ {
label: '退出', label: '退出',
click: () => { click: () => {
win.destroy() win.destroy();
}, },
}, },
]) ]);
// 设置系统托盘图标的上下文菜单 // 设置系统托盘图标的上下文菜单
tray.setContextMenu(contextMenu) tray.setContextMenu(contextMenu);
// 当系统托盘图标被点击时,切换窗口的显示/隐藏 // 当系统托盘图标被点击时,切换窗口的显示/隐藏
tray.on('click', () => { tray.on('click', () => {
if (win.isVisible()) { if (win.isVisible()) {
win.hide() win.hide();
} else { } else {
win.show() win.show();
} }
}) });
const set = store.get('set') const set = store.get('set');
// store.set('set', setJson) // store.set('set', setJson)
if (!set) { if (!set) {
store.set('set', setJson) store.set('set', setJson);
} }
loadLyricWindow(ipcMain);
} }
// 限制只能启动一个应用 // 限制只能启动一个应用
const gotTheLock = app.requestSingleInstanceLock() const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) { if (!gotTheLock) {
app.quit() app.quit();
} }
app.whenReady().then(createWindow) app.whenReady().then(createWindow);
app.on('ready',()=>{ app.on('ready', () => {
globalShortcut.register('CommandOrControl+Alt+Shift+M', () => { globalShortcut.register('CommandOrControl+Alt+Shift+M', () => {
if (mainWin.isVisible()) { if (mainWin.isVisible()) {
mainWin.hide() mainWin.hide();
} else { } else {
mainWin.show() mainWin.show();
} }
}) });
}) });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit() app.quit();
} }
}) });
app.on('will-quit', () => { app.on('will-quit', () => {
globalShortcut.unregisterAll() globalShortcut.unregisterAll();
}) });
ipcMain.on('minimize-window', (event) => { ipcMain.on('minimize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender) const win = BrowserWindow.fromWebContents(event.sender);
win.minimize() win.minimize();
}) });
ipcMain.on('maximize-window', (event) => { ipcMain.on('maximize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender) const win = BrowserWindow.fromWebContents(event.sender);
if (win.isMaximized()) { if (win.isMaximized()) {
win.unmaximize() win.unmaximize();
} else { } else {
win.maximize() win.maximize();
} }
}) });
ipcMain.on('close-window', (event) => { ipcMain.on('close-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender) const win = BrowserWindow.fromWebContents(event.sender);
win.destroy() win.destroy();
}) });
ipcMain.on('drag-start', (event, data) => { ipcMain.on('drag-start', (event) => {
const win = BrowserWindow.fromWebContents(event.sender) const win = BrowserWindow.fromWebContents(event.sender);
win.webContents.beginFrameSubscription((frameBuffer) => { win.webContents.beginFrameSubscription((frameBuffer) => {
event.reply('frame-buffer', frameBuffer) event.reply('frame-buffer', frameBuffer);
}) });
}) });
ipcMain.on('mini-tray', (event) => { ipcMain.on('mini-tray', (event) => {
const win = BrowserWindow.fromWebContents(event.sender) const win = BrowserWindow.fromWebContents(event.sender);
win.hide() win.hide();
}) });
// 重启 // 重启
ipcMain.on('restart', () => { ipcMain.on('restart', () => {
app.relaunch() app.relaunch();
app.exit(0) app.exit(0);
}) });
const store = new Store(); const store = new Store();
// 定义ipcRenderer监听事件 // 定义ipcRenderer监听事件
ipcMain.on('setStore', (_, key, value) => { ipcMain.on('setStore', (_, key, value) => {
store.set(key, value) store.set(key, value);
}) });
ipcMain.on('getStore', (_, key) => { ipcMain.on('getStore', (_, key) => {
let value = store.get(key) const value = store.get(key);
_.returnValue = value || "" _.returnValue = value || '';
}) });

37
electron/lyric.js Normal file
View File

@@ -0,0 +1,37 @@
const { BrowserWindow } = require('electron');
let lyricWindow = null;
const loadLyricWindow = (ipcMain) => {
lyricWindow = new BrowserWindow({
width: 800,
height: 300,
frame: false,
show: false,
transparent: true,
webPreferences: {
nodeIntegration: true,
preload: `${__dirname}/preload.js`,
contextIsolation: false,
},
});
ipcMain.on('open-lyric', () => {
if (process.env.NODE_ENV === 'development') {
lyricWindow.webContents.openDevTools({ mode: 'detach' });
lyricWindow.loadURL('http://localhost:4678/#/lyric');
} else {
lyricWindow.loadURL(`file://${__dirname}/dist/index.html/#/lyric`);
}
lyricWindow.show();
});
ipcMain.on('send-lyric', (e, data) => {
lyricWindow.webContents.send('receive-lyric', data);
});
};
module.exports = {
loadLyricWindow,
};

View File

@@ -1,4 +1,4 @@
const { contextBridge, ipcRenderer } = require('electron') const { contextBridge, ipcRenderer, app } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
minimize: () => ipcRenderer.send('minimize-window'), minimize: () => ipcRenderer.send('minimize-window'),
@@ -7,19 +7,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
dragStart: (data) => ipcRenderer.send('drag-start', data), dragStart: (data) => ipcRenderer.send('drag-start', data),
miniTray: () => ipcRenderer.send('mini-tray'), miniTray: () => ipcRenderer.send('mini-tray'),
restart: () => ipcRenderer.send('restart'), restart: () => ipcRenderer.send('restart'),
}) openLyric: () => ipcRenderer.send('open-lyric'),
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
});
const electronHandler = { const electronHandler = {
ipcRenderer: { ipcRenderer: {
setStoreValue: (key, value) => { setStoreValue: (key, value) => {
ipcRenderer.send("setStore", key, value) ipcRenderer.send('setStore', key, value);
}, },
getStoreValue(key) { getStoreValue(key) {
const resp = ipcRenderer.sendSync("getStore", key) const resp = ipcRenderer.sendSync('getStore', key);
return resp return resp;
}, },
} },
} app,
};
contextBridge.exposeInMainWorld('electron', electronHandler) contextBridge.exposeInMainWorld('electron', electronHandler);

View File

@@ -11,18 +11,21 @@
"es": "vite && electron .", "es": "vite && electron .",
"start": "set NODE_ENV=development&&electron .", "start": "set NODE_ENV=development&&electron .",
"e:b": "electron-builder --config ./electron.config.json", "e:b": "electron-builder --config ./electron.config.json",
"eb": "vite build && e:b" "eb": "vite build && e:b",
"lint": "eslint --ext .vue,.js,.jsx,.ts,.tsx ./ --max-warnings 0"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/postcss7-compat": "^2.2.4", "@tailwindcss/postcss7-compat": "^2.2.4",
"@vue/runtime-core": "^3.3.4", "@vue/runtime-core": "^3.3.4",
"@vueuse/core": "^10.7.1", "@vueuse/core": "^10.7.1",
"@vueuse/electron": "^10.9.0",
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
"axios": "^0.21.1", "axios": "^0.21.1",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"naive-ui": "^2.34.4", "naive-ui": "^2.34.4",
"postcss": "^7.0.36", "postcss": "^7.0.36",
"remixicon": "^4.2.0",
"sass": "^1.35.2", "sass": "^1.35.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4", "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
"vue": "^3.3.4", "vue": "^3.3.4",
@@ -30,12 +33,22 @@
"vuex": "^4.1.0" "vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@sicons/antd": "^0.10.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@vicons/antd": "^0.10.0", "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4", "@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^12.0.0",
"electron": "^28.0.0", "electron": "^28.0.0",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-vue": "^9.21.1",
"eslint-plugin-vue-scoped-css": "^2.7.2",
"prettier": "^3.2.5",
"typescript": "^4.3.2", "typescript": "^4.3.2",
"unplugin-auto-import": "^0.17.2", "unplugin-auto-import": "^0.17.2",
"unplugin-vue-components": "^0.26.0", "unplugin-vue-components": "^0.26.0",

View File

@@ -1,37 +1,36 @@
<template> <template>
<div class="app"> <div class="app">
<audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio> <audio id="MusicAudio" ref="audioRef" :src="playMusicUrl" :autoplay="play"></audio>
<n-config-provider :theme="darkTheme"> <n-config-provider :theme="darkTheme">
<n-dialog-provider> <n-dialog-provider>
<router-view></router-view> <router-view></router-view>
</n-dialog-provider> </n-dialog-provider>
</n-config-provider> </n-config-provider>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { darkTheme } from 'naive-ui' import { darkTheme } from 'naive-ui';
import store from '@/store'
import store from '@/store';
const audio = ref<HTMLAudioElement | null>(null)
const playMusicUrl = computed(() => store.state.playMusicUrl as string);
const playMusicUrl = computed(() => store.state.playMusicUrl as string) // 是否播放
// 是否播放 const play = computed(() => store.state.play as boolean);
const play = computed(() => store.state.play as boolean) const windowData = window as any;
const windowData = window as any onMounted(() => {
onMounted(()=>{ if (windowData.electron) {
if(windowData.electron){ const setData = windowData.electron.ipcRenderer.getStoreValue('set');
const setData = windowData.electron.ipcRenderer.getStoreValue('set'); store.commit('setSetData', setData);
store.commit('setSetData', setData) }
} });
}) </script>
</script>
<style lang="scss" scoped>
<style lang="scss" scoped > div {
div { box-sizing: border-box;
box-sizing: border-box; }
} .app {
.app { user-select: none;
user-select: none; }
} </style>
</style>

View File

@@ -1,9 +1,9 @@
import request from "@/utils/request"; import { IAlbumNew } from '@/type/album';
import { IHotSinger } from "@/type/singer"; import { IRecommendMusic } from '@/type/music';
import { ISearchKeyword, IHotSearch } from "@/type/search"; import { IPlayListSort } from '@/type/playlist';
import { IPlayListSort } from "@/type/playlist"; import { IHotSearch, ISearchKeyword } from '@/type/search';
import { IRecommendMusic } from "@/type/music"; import { IHotSinger } from '@/type/singer';
import { IAlbumNew } from "@/type/album"; import request from '@/utils/request';
interface IHotSingerParams { interface IHotSingerParams {
offset: number; offset: number;
@@ -16,30 +16,30 @@ interface IRecommendMusicParams {
// 获取热门歌手 // 获取热门歌手
export const getHotSinger = (params: IHotSingerParams) => { export const getHotSinger = (params: IHotSingerParams) => {
return request.get<IHotSinger>("/top/artists", { params }); return request.get<IHotSinger>('/top/artists', { params });
}; };
// 获取搜索推荐词 // 获取搜索推荐词
export const getSearchKeyword = () => { export const getSearchKeyword = () => {
return request.get<ISearchKeyword>("/search/default"); return request.get<ISearchKeyword>('/search/default');
}; };
// 获取热门搜索 // 获取热门搜索
export const getHotSearch = () => { export const getHotSearch = () => {
return request.get<IHotSearch>("/search/hot/detail"); return request.get<IHotSearch>('/search/hot/detail');
}; };
// 获取歌单分类 // 获取歌单分类
export const getPlaylistCategory = () => { export const getPlaylistCategory = () => {
return request.get<IPlayListSort>("/playlist/catlist"); return request.get<IPlayListSort>('/playlist/catlist');
}; };
// 获取推荐音乐 // 获取推荐音乐
export const getRecommendMusic = (params: IRecommendMusicParams) => { export const getRecommendMusic = (params: IRecommendMusicParams) => {
return request.get<IRecommendMusic>("/personalized/newsong", { params }); return request.get<IRecommendMusic>('/personalized/newsong', { params });
}; };
// 获取最新专辑推荐 // 获取最新专辑推荐
export const getNewAlbum = () => { export const getNewAlbum = () => {
return request.get<IAlbumNew>("/album/newest"); return request.get<IAlbumNew>('/album/newest');
}; };

View File

@@ -1,42 +1,42 @@
import request from "@/utils/request"; import { IList } from '@/type/list';
import { IList } from "@/type/list"; import type { IListDetail } from '@/type/listDetail';
import type { IListDetail } from "@/type/listDetail"; import request from '@/utils/request';
interface IListByTagParams { interface IListByTagParams {
tag: string; tag: string;
before: number; before: number;
limit: number; limit: number;
} }
interface IListByCatParams { interface IListByCatParams {
cat: string; cat: string;
offset: number; offset: number;
limit: number; limit: number;
} }
// 根据tag 获取歌单列表 // 根据tag 获取歌单列表
export function getListByTag(params: IListByTagParams) { export function getListByTag(params: IListByTagParams) {
return request.get<IList>("/top/playlist/highquality", { params: params }); return request.get<IList>('/top/playlist/highquality', { params });
} }
// 根据cat 获取歌单列表 // 根据cat 获取歌单列表
export function getListByCat(params: IListByCatParams) { export function getListByCat(params: IListByCatParams) {
return request.get("/top/playlist", { return request.get('/top/playlist', {
params: params, params,
}); });
} }
// 获取推荐歌单 // 获取推荐歌单
export function getRecommendList(limit: number = 30) { export function getRecommendList(limit: number = 30) {
return request.get("/personalized", { params: { limit } }); return request.get('/personalized', { params: { limit } });
} }
// 获取歌单详情 // 获取歌单详情
export function getListDetail(id: number | string) { export function getListDetail(id: number | string) {
return request.get<IListDetail>("/playlist/detail", { params: { id } }); return request.get<IListDetail>('/playlist/detail', { params: { id } });
} }
// 获取专辑内容 // 获取专辑内容
export function getAlbum(id: number | string) { export function getAlbum(id: number | string) {
return request.get("/album", { params: { id } }); return request.get('/album', { params: { id } });
} }

View File

@@ -1,46 +1,46 @@
import request from "@/utils/request"; import request from '@/utils/request';
// 创建二维码key // 创建二维码key
// /login/qr/key // /login/qr/key
export function getQrKey() { export function getQrKey() {
return request.get("/login/qr/key"); return request.get('/login/qr/key');
} }
// 创建二维码 // 创建二维码
// /login/qr/create // /login/qr/create
export function createQr(key: any) { export function createQr(key: any) {
return request.get("/login/qr/create", { params: { key: key, qrimg: true } }); return request.get('/login/qr/create', { params: { key, qrimg: true } });
} }
// 获取二维码状态 // 获取二维码状态
// /login/qr/check // /login/qr/check
export function checkQr(key: any) { export function checkQr(key: any) {
return request.get("/login/qr/check", { params: { key: key } }); return request.get('/login/qr/check', { params: { key } });
} }
// 获取登录状态 // 获取登录状态
// /login/status // /login/status
export function getLoginStatus() { export function getLoginStatus() {
return request.get("/login/status"); return request.get('/login/status');
} }
// 获取用户信息 // 获取用户信息
// /user/account // /user/account
export function getUserDetail() { export function getUserDetail() {
return request.get("/user/account"); return request.get('/user/account');
} }
// 退出登录 // 退出登录
// /logout // /logout
export function logout() { export function logout() {
return request.get("/logout"); return request.get('/logout');
} }
// 手机号登录 // 手机号登录
// /login/cellphone // /login/cellphone
export function loginByCellphone(phone: any, password: any) { export function loginByCellphone(phone: any, password: any) {
return request.post("/login/cellphone", { return request.post('/login/cellphone', {
phone: phone, phone,
password: password, password,
}); });
} }

View File

@@ -1,22 +1,22 @@
import { IPlayMusicUrl } from "@/type/music" import { ILyric } from '@/type/lyric';
import { ILyric } from "@/type/lyric" import { IPlayMusicUrl } from '@/type/music';
import request from "@/utils/request" import request from '@/utils/request';
import requestMusic from "@/utils/request_music" import requestMusic from '@/utils/request_music';
// 根据音乐Id获取音乐播放URl // 根据音乐Id获取音乐播放URl
export const getMusicUrl = (id: number) => { export const getMusicUrl = (id: number) => {
return request.get<IPlayMusicUrl>("/song/url", { params: { id: id } }) return request.get<IPlayMusicUrl>('/song/url', { params: { id } });
} };
// 获取歌曲详情 // 获取歌曲详情
export const getMusicDetail = (ids: Array<number>) => { export const getMusicDetail = (ids: Array<number>) => {
return request.get("/song/detail", { params: { ids: ids.join(",")}}) return request.get('/song/detail', { params: { ids: ids.join(',') } });
} };
// 根据音乐Id获取音乐歌词 // 根据音乐Id获取音乐歌词
export const getMusicLrc = (id: number) => { export const getMusicLrc = (id: number) => {
return request.get<ILyric>("/lyric", { params: { id: id } }) return request.get<ILyric>('/lyric', { params: { id } });
} };
export const getParsingMusicUrl = (id: number) => { export const getParsingMusicUrl = (id: number) => {
return requestMusic.get<any>("/music", { params: { id: id } }) return requestMusic.get<any>('/music', { params: { id } });
} };

View File

@@ -1,6 +1,6 @@
import { IData } from '@/type' import { IData } from '@/type';
import { IMvItem, IMvUrlData } from '@/type/mv' import { IMvItem, IMvUrlData } from '@/type/mv';
import request from '@/utils/request' import request from '@/utils/request';
// 获取 mv 排行 // 获取 mv 排行
export const getTopMv = (limit: number) => { export const getTopMv = (limit: number) => {
@@ -8,8 +8,8 @@ export const getTopMv = (limit: number) => {
params: { params: {
limit, limit,
}, },
}) });
} };
// 获取 mv 数据 // 获取 mv 数据
export const getMvDetail = (mvid: string) => { export const getMvDetail = (mvid: string) => {
@@ -17,8 +17,8 @@ export const getMvDetail = (mvid: string) => {
params: { params: {
mvid, mvid,
}, },
}) });
} };
// 获取 mv 地址 // 获取 mv 地址
export const getMvUrl = (id: Number) => { export const getMvUrl = (id: Number) => {
@@ -26,5 +26,5 @@ export const getMvUrl = (id: Number) => {
params: { params: {
id, id,
}, },
}) });
} };

View File

@@ -1,13 +1,12 @@
import request from "@/utils/request" import request from '@/utils/request';
import { ISearchDetail } from "@/type/search"
interface IParams { interface IParams {
keywords: string keywords: string;
type: number type: number;
} }
// 搜索内容 // 搜索内容
export const getSearch = (params: IParams) => { export const getSearch = (params: IParams) => {
return request.get<any>('/cloudsearch', { return request.get<any>('/cloudsearch', {
params, params,
}) });
} };

View File

@@ -1,17 +1,17 @@
import request from "@/utils/request"; import request from '@/utils/request';
// /user/detail // /user/detail
export function getUserDetail(uid: number) { export function getUserDetail(uid: number) {
return request.get("/user/detail", { params: { uid } }); return request.get('/user/detail', { params: { uid } });
} }
// /user/playlist // /user/playlist
export function getUserPlaylist(uid: number) { export function getUserPlaylist(uid: number) {
return request.get("/user/playlist", { params: { uid } }); return request.get('/user/playlist', { params: { uid } });
} }
// 播放历史 // 播放历史
// /user/record?uid=32953014&type=1 // /user/record?uid=32953014&type=1
export function getUserRecord(uid: number, type: number = 0) { export function getUserRecord(uid: number, type: number = 0) {
return request.get("/user/record", { params: { uid, type } }); return request.get('/user/record', { params: { uid, type } });
} }

View File

@@ -5,12 +5,16 @@
<div class="music-title">{{ name }}</div> <div class="music-title">{{ name }}</div>
<!-- 歌单歌曲列表 --> <!-- 歌单歌曲列表 -->
<div class="music-list"> <div class="music-list">
<n-scrollbar > <n-scrollbar>
<div v-for="(item, index) in songList" :key="item.id" :class="setAnimationClass('animate__bounceInUp')" <div
:style="setAnimationDelay(index, 100)"> v-for="(item, index) in songList"
<SongItem :item="formatDetail(item)" @play="handlePlay" /> :key="item.id"
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 100)"
>
<song-item :item="formatDetail(item)" @play="handlePlay" />
</div> </div>
<PlayBottom/> <play-bottom />
</n-scrollbar> </n-scrollbar>
</div> </div>
</div> </div>
@@ -18,40 +22,42 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useStore } from 'vuex' import { useStore } from 'vuex';
import { setAnimationClass, setAnimationDelay } from "@/utils";
import SongItem from "@/components/common/SongItem.vue"; import SongItem from '@/components/common/SongItem.vue';
import { setAnimationClass, setAnimationDelay } from '@/utils';
import PlayBottom from './common/PlayBottom.vue'; import PlayBottom from './common/PlayBottom.vue';
const store = useStore() const store = useStore();
const props = defineProps<{ const props = defineProps<{
show: boolean; show: boolean;
name: string; name: string;
songList: any[] songList: any[];
}>() }>();
const emit = defineEmits(['update:show']) const emit = defineEmits(['update:show']);
const formatDetail = computed(() => (detail: any) => { const formatDetail = computed(() => (detail: any) => {
let song = { const song = {
artists: detail.ar, artists: detail.ar,
name: detail.al.name, name: detail.al.name,
id: detail.al.id, id: detail.al.id,
} };
detail.song = song detail.song = song;
detail.picUrl = detail.al.picUrl detail.picUrl = detail.al.picUrl;
return detail return detail;
}) });
const handlePlay = (item: any) => { const handlePlay = () => {
const tracks = props.songList || [] const tracks = props.songList || [];
store.commit('setPlayList', tracks) store.commit('setPlayList', tracks);
} };
const close = () => { const close = () => {
emit('update:show', false) emit('update:show', false);
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -72,4 +78,4 @@ const close = () => {
height: calc(100% - 60px); height: calc(100% - 60px);
} }
} }
</style> </style>

View File

@@ -1,39 +1,37 @@
<template> <template>
<!-- 歌单分类列表 --> <!-- 歌单分类列表 -->
<div class="play-list-type"> <div class="play-list-type">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div> <div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
<div> <div>
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name"> <template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
<span <span
class="play-list-type-item" v-show="isShowAllPlaylistCategory || index <= 19"
:class="setAnimationClass('animate__bounceIn')" class="play-list-type-item"
:style="setAnimationDelay(index <= 19 ? index : index - 19)" :class="setAnimationClass('animate__bounceIn')"
v-show="isShowAllPlaylistCategory || index <= 19" :style="setAnimationDelay(index <= 19 ? index : index - 19)"
@click="handleClickPlaylistType(item.name)" @click="handleClickPlaylistType(item.name)"
>{{ item.name }}</span> >{{ item.name }}</span
</template> >
<div </template>
class="play-list-type-showall" <div
:class="setAnimationClass('animate__bounceIn')" class="play-list-type-showall"
:style=" :class="setAnimationClass('animate__bounceIn')"
setAnimationDelay( :style="setAnimationDelay(!isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30)"
!isShowAllPlaylistCategory @click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
? 25 >
: playlistCategory?.sub.length || 100 + 30 {{ !isShowAllPlaylistCategory ? '显示全部' : '隐藏一些' }}
) </div>
"
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
>{{ !isShowAllPlaylistCategory ? "显示全部" : "隐藏一些" }}</div>
</div>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from 'vue';
import { getPlaylistCategory } from "@/api/home"; import { useRouter } from 'vue-router';
import type { IPlayListSort } from "@/type/playlist";
import { setAnimationDelay, setAnimationClass } from "@/utils"; import { getPlaylistCategory } from '@/api/home';
import { useRoute, useRouter } from "vue-router"; import type { IPlayListSort } from '@/type/playlist';
import { setAnimationClass, setAnimationDelay } from '@/utils';
// 歌单分类 // 歌单分类
const playlistCategory = ref<IPlayListSort>(); const playlistCategory = ref<IPlayListSort>();
// 是否显示全部歌单分类 // 是否显示全部歌单分类
@@ -41,39 +39,39 @@ const isShowAllPlaylistCategory = ref<boolean>(false);
// 加载歌单分类 // 加载歌单分类
const loadPlaylistCategory = async () => { const loadPlaylistCategory = async () => {
const { data } = await getPlaylistCategory(); const { data } = await getPlaylistCategory();
playlistCategory.value = data; playlistCategory.value = data;
}; };
const router = useRouter(); const router = useRouter();
const handleClickPlaylistType = (type: any) => { const handleClickPlaylistType = (type: any) => {
router.push({ router.push({
path: "/list", path: '/list',
query: { query: {
type: type, type,
} },
}); });
}; };
// 页面初始化 // 页面初始化
onMounted(() => { onMounted(() => {
loadPlaylistCategory(); loadPlaylistCategory();
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.title { .title {
@apply text-lg font-bold mb-4; @apply text-lg font-bold mb-4;
} }
.play-list-type { .play-list-type {
width: 250px; width: 250px;
@apply mx-6; @apply mx-6;
&-item, &-item,
&-showall { &-showall {
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition; @apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
background-color: #1a1a1a; background-color: #1a1a1a;
} }
&-showall { &-showall {
@apply block text-center; @apply block text-center;
} }
} }
</style> </style>

View File

@@ -1,86 +1,86 @@
<template> <template>
<div class="recommend-album"> <div class="recommend-album">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div> <div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div>
<div class="recommend-album-list"> <div class="recommend-album-list">
<template v-for="(item,index) in albumData?.albums" :key="item.id"> <template v-for="(item, index) in albumData?.albums" :key="item.id">
<div <div
v-if="index < 6" v-if="index < 6"
class="recommend-album-list-item" class="recommend-album-list-item"
:class="setAnimationClass('animate__backInUp')" :class="setAnimationClass('animate__backInUp')"
:style="setAnimationDelay(index, 100)" :style="setAnimationDelay(index, 100)"
@click="handleClick(item)" @click="handleClick(item)"
> >
<n-image <n-image
class="recommend-album-list-item-img" class="recommend-album-list-item-img"
:src="getImgUrl( item.blurPicUrl, '200y200')" :src="getImgUrl(item.blurPicUrl, '200y200')"
lazy lazy
preview-disabled preview-disabled
/> />
<div class="recommend-album-list-item-content">{{ item.name }}</div> <div class="recommend-album-list-item-content">{{ item.name }}</div>
</div>
</template>
</div> </div>
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" /> </template>
</div> </div>
<MusicList v-model:show="showMusic" :name="albumName" :song-list="songList" />
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getNewAlbum } from "@/api/home" import { onMounted, ref } from 'vue';
import { ref, onMounted } from "vue";
import type { IAlbumNew } from "@/type/album"
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
import { getAlbum } from "@/api/list";
import { getNewAlbum } from '@/api/home';
import { getAlbum } from '@/api/list';
import type { IAlbumNew } from '@/type/album';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const albumData = ref<IAlbumNew>() const albumData = ref<IAlbumNew>();
const loadAlbumList = async () => { const loadAlbumList = async () => {
const { data } = await getNewAlbum(); const { data } = await getNewAlbum();
albumData.value = data albumData.value = data;
} };
const showMusic = ref(false) const showMusic = ref(false);
const songList = ref([]) const songList = ref([]);
const albumName = ref('') const albumName = ref('');
const handleClick = async (item:any) => { const handleClick = async (item: any) => {
albumName.value = item.name albumName.value = item.name;
showMusic.value = true showMusic.value = true;
const res = await getAlbum(item.id) const res = await getAlbum(item.id);
songList.value = res.data.songs.map((song:any)=>{ songList.value = res.data.songs.map((song: any) => {
song.al.picUrl = song.al.picUrl || item.picUrl song.al.picUrl = song.al.picUrl || item.picUrl;
return song return song;
}) });
} };
onMounted(() => { onMounted(() => {
loadAlbumList() loadAlbumList();
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.recommend-album { .recommend-album {
@apply flex-1 mx-5; @apply flex-1 mx-5;
.title { .title {
@apply text-lg font-bold mb-4; @apply text-lg font-bold mb-4;
} }
.recommend-album-list { .recommend-album-list {
@apply grid grid-cols-2 grid-rows-3 gap-2; @apply grid grid-cols-2 grid-rows-3 gap-2;
&-item { &-item {
@apply rounded-xl overflow-hidden relative; @apply rounded-xl overflow-hidden relative;
&-img { &-img {
@apply rounded-xl transition w-full h-full; @apply rounded-xl transition w-full h-full;
} }
&:hover img { &:hover img {
filter: brightness(50%); filter: brightness(50%);
} }
&-content { &-content {
@apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl bg-opacity-60 bg-black; @apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl bg-opacity-60 bg-black;
} }
&-content:hover { &-content:hover {
opacity: 1; opacity: 1;
} }
}
} }
}
} }
</style> </style>

View File

@@ -1,85 +1,79 @@
<template> <template>
<!-- 推荐歌手 --> <!-- 推荐歌手 -->
<div class="recommend-singer"> <div class="recommend-singer">
<div class="recommend-singer-list"> <div class="recommend-singer-list">
<div <div
class="recommend-singer-item relative" v-for="(item, index) in hotSingerData?.artists"
:class="setAnimationClass('animate__backInRight')" :key="item.id"
v-for="(item, index) in hotSingerData?.artists" class="recommend-singer-item relative"
:style="setAnimationDelay(index, 100)" :class="setAnimationClass('animate__backInRight')"
:key="item.id" :style="setAnimationDelay(index, 100)"
> >
<div <div :style="setBackgroundImg(getImgUrl(item.picUrl, '300y300'))" class="recommend-singer-item-bg"></div>
:style="setBackgroundImg(getImgUrl(item.picUrl,'300y300'))" <div class="recommend-singer-item-count p-2 text-base text-gray-200 z-10">{{ item.musicSize }}</div>
class="recommend-singer-item-bg" <div class="recommend-singer-item-info z-10">
></div> <div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
<div <i class="iconfont icon-playfill text-xl"></i>
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10" </div>
>{{ item.musicSize }}</div> <div class="ml-4">
<div class="recommend-singer-item-info z-10"> <div class="recommend-singer-item-info-name">{{ item.name }}</div>
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)"> <div class="recommend-singer-item-info-name">{{ item.name }}</div>
<i class="iconfont icon-playfill text-xl"></i> </div>
</div>
<div class="ml-4">
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
</div>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { setBackgroundImg, setAnimationDelay, setAnimationClass,getImgUrl } from "@/utils"; import { onMounted, ref } from 'vue';
import { onMounted, ref } from "vue";
import { getHotSinger } from "@/api/home"; import { getHotSinger } from '@/api/home';
import type { IHotSinger } from "@/type/singer"; import router from '@/router';
import router from "@/router"; import type { IHotSinger } from '@/type/singer';
import { getImgUrl, setAnimationClass, setAnimationDelay, setBackgroundImg } from '@/utils';
// 歌手信息 // 歌手信息
const hotSingerData = ref<IHotSinger>(); const hotSingerData = ref<IHotSinger>();
//加载推荐歌手 // 加载推荐歌手
const loadSingerList = async () => { const loadSingerList = async () => {
const { data } = await getHotSinger({ offset: 0, limit: 5 }); const { data } = await getHotSinger({ offset: 0, limit: 5 });
hotSingerData.value = data; hotSingerData.value = data;
}; };
// 页面初始化 // 页面初始化
onMounted(() => { onMounted(() => {
loadSingerList(); loadSingerList();
}); });
const toSearchSinger = (keyword: string) => { const toSearchSinger = (keyword: string) => {
router.push({ router.push({
path: "/search", path: '/search',
query: { query: {
keyword: keyword, keyword,
}, },
}); });
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.recommend-singer { .recommend-singer {
&-list { &-list {
@apply flex; @apply flex;
height: 280px; height: 280px;
}
&-item {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between;
&-bg {
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
filter: brightness(80%);
} }
&-item { &-info {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between; @apply flex items-center p-2;
&-bg { &-play {
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0; @apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
filter: brightness(80%); }
}
&-info {
@apply flex items-center p-2;
&-play {
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
}
}
} }
}
} }
</style> </style>

View File

@@ -1,19 +1,10 @@
<template> <template>
<div class="recommend-music"> <div class="recommend-music">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')"> <div class="title" :class="setAnimationClass('animate__fadeInLeft')">本周最热音乐</div>
本周最热音乐 <div v-show="recommendMusic?.result" class="recommend-music-list" :class="setAnimationClass('animate__bounceInUp')">
</div>
<div
class="recommend-music-list"
:class="setAnimationClass('animate__bounceInUp')"
v-show="recommendMusic?.result"
>
<!-- 推荐音乐列表 --> <!-- 推荐音乐列表 -->
<template v-for="(item, index) in recommendMusic?.result" :key="item.id"> <template v-for="(item, index) in recommendMusic?.result" :key="item.id">
<div <div :class="setAnimationClass('animate__bounceInUp')" :style="setAnimationDelay(index, 100)">
:class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 100)"
>
<song-item :item="item" @play="handlePlay" /> <song-item :item="item" @play="handlePlay" />
</div> </div>
</template> </template>
@@ -22,30 +13,32 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getRecommendMusic } from '@/api/home' import { useStore } from 'vuex';
import type { IRecommendMusic } from '@/type/music'
import { setAnimationClass, setAnimationDelay } from '@/utils' import { getRecommendMusic } from '@/api/home';
import SongItem from './common/SongItem.vue' import type { IRecommendMusic } from '@/type/music';
import { useStore } from 'vuex' import { setAnimationClass, setAnimationDelay } from '@/utils';
import SongItem from './common/SongItem.vue';
const store = useStore(); const store = useStore();
// 推荐歌曲 // 推荐歌曲
const recommendMusic = ref<IRecommendMusic>() const recommendMusic = ref<IRecommendMusic>();
// 加载推荐歌曲 // 加载推荐歌曲
const loadRecommendMusic = async () => { const loadRecommendMusic = async () => {
const { data } = await getRecommendMusic({ limit: 10 }) const { data } = await getRecommendMusic({ limit: 10 });
recommendMusic.value = data recommendMusic.value = data;
} };
// 页面初始化 // 页面初始化
onMounted(() => { onMounted(() => {
loadRecommendMusic() loadRecommendMusic();
}) });
const handlePlay = (item: any) => { const handlePlay = () => {
store.commit('setPlayList', recommendMusic.value?.result) store.commit('setPlayList', recommendMusic.value?.result);
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -1,42 +1,40 @@
<script lang="ts" setup> <script lang="ts" setup>
import { setAnimationClass, setAnimationDelay } from "@/utils"; import { setAnimationClass } from '@/utils';
const props = defineProps({ const props = defineProps({
showPop: { showPop: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
showClose: { showClose: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
}) });
const musicFullClass = computed(() => { const musicFullClass = computed(() => {
if (props.showPop) { if (props.showPop) {
return setAnimationClass('animate__fadeInUp') return setAnimationClass('animate__fadeInUp');
} else { }
return setAnimationClass('animate__fadeOutDown') return setAnimationClass('animate__fadeOutDown');
} });
}) </script>
</script> <template>
<div v-show="props.showPop" class="pop-page" :class="musicFullClass">
<template> <i v-if="props.showClose" class="iconfont icon-icon_error close"></i>
<div class="pop-page" v-show="props.showPop" :class="musicFullClass"> <img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
<i class="iconfont icon-icon_error close" v-if="props.showClose"></i> <slot></slot>
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" /> </div>
<slot></slot> </template>
</div>
</template> <style lang="scss" scoped>
.pop-page {
<style lang="scss" scoped> height: 800px;
.pop-page { @apply absolute top-4 left-0 w-full;
height: 800px; background-color: #000000f0;
@apply absolute top-4 left-0 w-full; .close {
background-color: #000000f0; @apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
.close { }
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl; }
} </style>
}
</style>

View File

@@ -1,15 +1,16 @@
<template> <template>
<div class="bottom" v-if="isPlay"></div> <div v-if="isPlay" class="bottom"></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useStore } from 'vuex'; import { useStore } from 'vuex';
const store = useStore()
const isPlay = computed(() => store.state.isPlay as boolean) const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.bottom{ .bottom {
@apply h-28; @apply h-28;
} }
</style> </style>

View File

@@ -1,15 +1,11 @@
<template> <template>
<div class="search-item" @click="handleClick"> <div class="search-item" @click="handleClick">
<div class="search-item-img"> <div class="search-item-img">
<n-image <n-image :src="getImgUrl(item.picUrl, 'album')" lazy preview-disabled />
:src="getImgUrl(item.picUrl, 'album')"
lazy
preview-disabled
/>
</div> </div>
<div class="search-item-info"> <div class="search-item-info">
<div class="search-item-name">{{ item.name }}</div> <div class="search-item-name">{{ item.name }}</div>
<div class="search-item-artist">{{ item.desc}}</div> <div class="search-item-artist">{{ item.desc }}</div>
</div> </div>
<MusicList v-model:show="showMusic" :name="item.name" :song-list="songList" /> <MusicList v-model:show="showMusic" :name="item.name" :song-list="songList" />
@@ -17,54 +13,49 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { getImgUrl } from '@/utils'
import type {Album} from '@/type/album'
import { getAlbum } from '@/api/list'; import { getAlbum } from '@/api/list';
import { getImgUrl } from '@/utils';
const props = defineProps<{ const props = defineProps<{
item: { item: {
picUrl: string picUrl: string;
name: string name: string;
desc: string desc: string;
type: string type: string;
[key: string]: any [key: string]: any;
} };
}>() }>();
const songList = ref([]) const songList = ref([]);
const showMusic = ref(false) const showMusic = ref(false);
const handleClick = async () => { const handleClick = async () => {
showMusic.value = true showMusic.value = true;
if(props.item.type === '专辑'){ if (props.item.type === '专辑') {
const res = await getAlbum(props.item.id) const res = await getAlbum(props.item.id);
songList.value = res.data.songs.map((song:any)=>{ songList.value = res.data.songs.map((song: any) => {
song.al.picUrl = song.al.picUrl || props.item.picUrl song.al.picUrl = song.al.picUrl || props.item.picUrl;
return song return song;
}) });
} }
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.search-item {
.search-item{
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition; @apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
margin: 0 10px; margin: 0 10px;
.search-item-img{ .search-item-img {
@apply w-12 h-12 mr-4 rounded-2xl overflow-hidden; @apply w-12 h-12 mr-4 rounded-2xl overflow-hidden;
} }
.search-item-info{ .search-item-info {
&-name{ &-name {
@apply text-white text-sm text-center; @apply text-white text-sm text-center;
} }
&-artist{ &-artist {
@apply text-gray-400 text-xs text-center; @apply text-gray-400 text-xs text-center;
} }
} }
} }
</style>
</style>

View File

@@ -1,27 +1,14 @@
<template> <template>
<div class="song-item" :class="{'song-mini': mini}"> <div class="song-item" :class="{ 'song-mini': mini }">
<n-image <n-image v-if="item.picUrl" :src="getImgUrl(item.picUrl, '40y40')" class="song-item-img" lazy preview-disabled />
v-if="item.picUrl "
:src="getImgUrl( item.picUrl, '40y40')"
class="song-item-img"
lazy
preview-disabled
/>
<div class="song-item-content"> <div class="song-item-content">
<div class="song-item-content-title"> <div class="song-item-content-title">
<n-ellipsis class="text-ellipsis" line-clamp="1">{{ <n-ellipsis class="text-ellipsis" line-clamp="1">{{ item.name }}</n-ellipsis>
item.name
}}</n-ellipsis>
</div> </div>
<div class="song-item-content-name"> <div class="song-item-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1"> <n-ellipsis class="text-ellipsis" line-clamp="1">
<span <span v-for="(artists, artistsindex) in item.song.artists" :key="artistsindex"
v-for="(artists, artistsindex) in item.song.artists" >{{ artists.name }}{{ artistsindex < item.song.artists.length - 1 ? ' / ' : '' }}</span
:key="artistsindex"
>{{ artists.name
}}{{
artistsindex < item.song.artists.length - 1 ? ' / ' : ''
}}</span
> >
</n-ellipsis> </n-ellipsis>
</div> </div>
@@ -43,40 +30,43 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useStore } from 'vuex' import { useStore } from 'vuex';
import type { SongResult } from '@/type/music'
import { getImgUrl } from '@/utils'
const props = withDefaults(defineProps<{ import type { SongResult } from '@/type/music';
item: SongResult import { getImgUrl } from '@/utils';
mini?: boolean
}>(), {
mini: false
})
const store = useStore() const props = withDefaults(
defineProps<{
item: SongResult;
mini?: boolean;
}>(),
{
mini: false,
},
);
const play = computed(() => store.state.play as boolean) const store = useStore();
const playMusic = computed(() => store.state.playMusic) const play = computed(() => store.state.play as boolean);
const playMusic = computed(() => store.state.playMusic);
// 判断是否为正在播放的音乐 // 判断是否为正在播放的音乐
const isPlaying = computed(() => { const isPlaying = computed(() => {
return playMusic.value.id == props.item.id return playMusic.value.id === props.item.id;
}) });
const emits = defineEmits(['play']) const emits = defineEmits(['play']);
// 播放音乐 设置音乐详情 打开音乐底栏 // 播放音乐 设置音乐详情 打开音乐底栏
const playMusicEvent = (item: any) => { const playMusicEvent = (item: any) => {
store.commit('setPlay', item) store.commit('setPlay', item);
store.commit('setIsPlay', true) store.commit('setIsPlay', true);
emits('play', item) emits('play', item);
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// 配置文字不可选中 // 配置文字不可选中
.text-ellipsis { .text-ellipsis {
width: 100%; width: 100%;
@@ -119,31 +109,31 @@ const playMusicEvent = (item: any) => {
} }
} }
.song-mini{ .song-mini {
@apply p-2 rounded-2xl; @apply p-2 rounded-2xl;
.song-item{ .song-item {
@apply p-0; @apply p-0;
&-img{ &-img {
@apply w-10 h-10 mr-2; @apply w-10 h-10 mr-2;
} }
&-content{ &-content {
@apply flex-1; @apply flex-1;
&-title{ &-title {
@apply text-sm; @apply text-sm;
} }
&-name{ &-name {
@apply text-xs; @apply text-xs;
} }
} }
&-operating{ &-operating {
@apply pl-2; @apply pl-2;
.iconfont{ .iconfont {
@apply text-base; @apply text-base;
} }
&-like{ &-like {
@apply mr-1; @apply mr-1;
} }
&-play{ &-play {
@apply w-8 h-8; @apply w-8 h-8;
} }
} }

View File

@@ -19,7 +19,7 @@ export const USER_SET_OPTIONS = [
label: '设置', label: '设置',
key: 'set', key: 'set',
}, },
] ];
export const SEARCH_TYPES = [ export const SEARCH_TYPES = [
{ {
@@ -62,4 +62,4 @@ export const SEARCH_TYPES = [
label: '综合', label: '综合',
key: 1018, key: 1018,
}, },
] ];

10
src/electron.d.ts vendored
View File

@@ -1,10 +1,10 @@
declare global { declare global {
interface Window { interface Window {
electronAPI: { electronAPI: {
minimize: () => void minimize: () => void;
maximize: () => void maximize: () => void;
close: () => void close: () => void;
dragStart: () => void dragStart: () => void;
} };
} }
} }

View File

@@ -1,39 +1,39 @@
// musicHistoryHooks // musicHistoryHooks
import { RemovableRef, useLocalStorage } from '@vueuse/core' import { useLocalStorage } from '@vueuse/core';
import type { SongResult } from '@/type/music'
import type { SongResult } from '@/type/music';
export const useMusicHistory = () => { export const useMusicHistory = () => {
const musicHistory = useLocalStorage<SongResult[]>('musicHistory', []) const musicHistory = useLocalStorage<SongResult[]>('musicHistory', []);
const addMusic = (music: SongResult) => { const addMusic = (music: SongResult) => {
const index = musicHistory.value.findIndex((item) => item.id === music.id) const index = musicHistory.value.findIndex((item) => item.id === music.id);
if (index !== -1) { if (index !== -1) {
musicHistory.value[index].count = musicHistory.value[index].count = (musicHistory.value[index].count || 0) + 1;
(musicHistory.value[index].count || 0) + 1 musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0]);
musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0])
} else { } else {
musicHistory.value.unshift({ ...music, count: 1 }) musicHistory.value.unshift({ ...music, count: 1 });
} }
} };
const delMusic = (music: any) => { const delMusic = (music: any) => {
const index = musicHistory.value.findIndex((item) => item.id === music.id) const index = musicHistory.value.findIndex((item) => item.id === music.id);
if (index !== -1) { if (index !== -1) {
musicHistory.value.splice(index, 1) musicHistory.value.splice(index, 1);
} }
} };
const musicList = ref(musicHistory.value) const musicList = ref(musicHistory.value);
watch( watch(
() => musicHistory.value, () => musicHistory.value,
() => { () => {
musicList.value = musicHistory.value musicList.value = musicHistory.value;
} },
) );
return { return {
musicHistory, musicHistory,
musicList, musicList,
addMusic, addMusic,
delMusic, delMusic,
} };
} };

View File

@@ -1,104 +1,169 @@
import { getMusicLrc } from '@/api/music' import { getMusicLrc } from '@/api/music';
import { ILyric } from '@/type/lyric' import { ILyric } from '@/type/lyric';
import { getIsMc } from '@/utils'
interface ILrcData { interface ILrcData {
text: string text: string;
trText: string trText: string;
} }
const lrcData = ref<ILyric>() export const lrcData = ref<ILyric>();
const newLrcIndex = ref<number>(0) export const newLrcIndex = ref<number>(0);
const lrcArray = ref<Array<ILrcData>>([]) export const lrcArray = ref<Array<ILrcData>>([]);
const lrcTimeArray = ref<Array<Number>>([]) export const lrcTimeArray = ref<Array<Number>>([]);
const parseTime = (timeString: string) => { export const parseTime = (timeString: string) => {
const [minutes, seconds] = timeString.split(':') const [minutes, seconds] = timeString.split(':');
return parseInt(minutes) * 60 + parseFloat(seconds) return Number(minutes) * 60 + Number(seconds);
} };
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g;
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g;
function parseLyricLine(lyricLine: string) { function parseLyricLine(lyricLine: string) {
// [00:00.00] 作词 : 长友美知惠/ const timeText = lyricLine.match(TIME_REGEX)?.[0] || '';
const timeText = lyricLine.match(TIME_REGEX)?.[0] || '' const time = parseTime(timeText);
const time = parseTime(timeText) const text = lyricLine.replace(LRC_REGEX, '').trim();
const text = lyricLine.replace(LRC_REGEX, '').trim() return { time, text };
return { time, text }
} }
interface ILyricText { interface ILyricText {
text: string text: string;
trText: string trText: string;
} }
function parseLyrics(lyricsString: string) { function parseLyrics(lyricsString: string) {
const lines = lyricsString.split('\n') const lines = lyricsString.split('\n');
const lyrics: Array<ILyricText> = [] const lyrics: Array<ILyricText> = [];
const times: number[] = [] const times: number[] = [];
lines.forEach((line) => { lines.forEach((line) => {
const { time, text } = parseLyricLine(line) const { time, text } = parseLyricLine(line);
times.push(time) times.push(time);
lyrics.push({ text, trText: '' }) lyrics.push({ text, trText: '' });
}) });
return { lyrics, times } return { lyrics, times };
} }
const loadLrc = async (playMusicId: number): Promise<void> => { export const loadLrc = async (playMusicId: number): Promise<void> => {
try { try {
const { data } = await getMusicLrc(playMusicId) const { data } = await getMusicLrc(playMusicId);
const { lyrics, times } = parseLyrics(data.lrc.lyric) const { lyrics, times } = parseLyrics(data.lrc.lyric);
lrcTimeArray.value = times let tlyric: {
lrcArray.value = lyrics [key: string]: string;
} = {};
if (data.tlyric.lyric) {
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
tlyric = tLyrics.reduce((acc: any, cur, index) => {
acc[tTimes[index]] = cur.text;
return acc;
}, {});
}
if (Object.keys(tlyric).length) {
lyrics.forEach((item, index) => {
item.trText = item.text ? tlyric[times[index].toString()] : '';
});
}
lrcTimeArray.value = times;
lrcArray.value = lyrics;
} catch (err) { } catch (err) {
console.error('err', err) console.error('err', err);
} }
} };
// 歌词矫正时间Correction time // 歌词矫正时间Correction time
const correctionTime = ref(0.4) const correctionTime = ref(0.4);
// 增加矫正时间 // 增加矫正时间
const addCorrectionTime = (time: number) => { export const addCorrectionTime = (time: number) => {
correctionTime.value += time correctionTime.value += time;
} };
// 减少矫正时间 // 减少矫正时间
const reduceCorrectionTime = (time: number) => { export const reduceCorrectionTime = (time: number) => {
correctionTime.value -= time correctionTime.value -= time;
} };
const isCurrentLrc = (index: any, time: number) => { export const isCurrentLrc = (index: any, time: number) => {
const currentTime = Number(lrcTimeArray.value[index]) const currentTime = Number(lrcTimeArray.value[index]);
const nextTime = Number(lrcTimeArray.value[index + 1]) const nextTime = Number(lrcTimeArray.value[index + 1]);
const nowTime = time + correctionTime.value const nowTime = time + correctionTime.value;
const isTrue = nowTime > currentTime && nowTime < nextTime const isTrue = nowTime > currentTime && nowTime < nextTime;
if (isTrue) { if (isTrue) {
newLrcIndex.value = index newLrcIndex.value = index;
} }
return isTrue return isTrue;
} };
const nowTime = ref(0) export const nowTime = ref(0);
const allTime = ref(0) export const allTime = ref(0);
export const nowIndex = ref(0);
export const getLrcIndex = (time: number) => {
for (let i = 0; i < lrcTimeArray.value.length; i++) {
if (isCurrentLrc(i, time)) {
nowIndex.value = i || nowIndex.value;
return i;
}
}
return nowIndex.value;
};
// 设置当前播放时间 // 设置当前播放时间
const setAudioTime = (index: any, audio: HTMLAudioElement) => { export const setAudioTime = (index: any, audio: HTMLAudioElement) => {
audio.currentTime = lrcTimeArray.value[index] as number audio.currentTime = lrcTimeArray.value[index] as number;
audio.play() audio.play();
} };
export { // 计算这个歌词的播放时间
lrcData, const getLrcTime = (index: any) => {
lrcArray, return Number(lrcTimeArray.value[index]);
lrcTimeArray, };
newLrcIndex,
loadLrc, // 获取当前播放的歌词
isCurrentLrc, export const getCurrentLrc = () => {
addCorrectionTime, const index = getLrcIndex(nowTime.value);
reduceCorrectionTime, const currentLrc = lrcArray.value[index];
setAudioTime, const nextLrc = lrcArray.value[index + 1];
nowTime, return { currentLrc, nextLrc };
allTime, };
}
// 获取一句歌词播放时间是 几秒到几秒
export const getLrcTimeRange = (index: any) => {
const currentTime = Number(lrcTimeArray.value[index]);
const nextTime = Number(lrcTimeArray.value[index + 1]);
return { currentTime, nextTime };
};
export const sendLyricToWin = (isPlay: boolean) => {
try {
// 设置lyricWinData 获取 当前播放的两句歌词 和歌词时间
let lyricWinData = null;
if (lrcArray.value.length > 0) {
const nowIndex = getLrcIndex(nowTime.value);
const { currentLrc, nextLrc } = getCurrentLrc();
const { currentTime, nextTime } = getLrcTimeRange(nowIndex);
lyricWinData = {
currentLrc,
nextLrc,
currentTime,
nextTime,
nowIndex,
lrcTimeArray: lrcTimeArray.value,
lrcArray: lrcArray.value,
nowTime: nowTime.value,
allTime: allTime.value,
startCurrentTime: getLrcTime(nowIndex),
isPlay,
};
const windowData = window as any;
windowData.electronAPI.sendLyric(JSON.stringify(lyricWinData));
}
} catch (error) {
console.error('error', error);
}
};
export const openLyric = () => {
const windowData = window as any;
windowData.electronAPI.openLyric();
};

View File

@@ -8,4 +8,4 @@
.n-image img { .n-image img {
background-color: #111111; background-color: #111111;
width: 100%; width: 100%;
} }

View File

@@ -1,144 +1,147 @@
<template> <template>
<div class="layout-page"> <div class="layout-page">
<div class="layout-main"> <div class="layout-main">
<title-bar v-if="isElectron" /> <title-bar v-if="isElectron" />
<div class="layout-main-page" :class="isElectron ? '' : 'pt-6'"> <div class="layout-main-page" :class="isElectron ? '' : 'pt-6'">
<!-- 侧边菜单栏 --> <!-- 侧边菜单栏 -->
<app-menu class="menu" :menus="menus" /> <app-menu class="menu" :menus="menus" />
<div class="main"> <div class="main">
<!-- 搜索栏 --> <!-- 搜索栏 -->
<search-bar /> <search-bar />
<!-- 主页面路由 --> <!-- 主页面路由 -->
<div class="main-content bg-black pb-" :native-scrollbar="false" :class="isPlay ? 'pb-20' : ''"> <div class="main-content bg-black pb-" :native-scrollbar="false" :class="isPlay ? 'pb-20' : ''">
<n-message-provider> <n-message-provider>
<router-view class="main-page" v-slot="{ Component }" :class="route.meta.noScroll? 'pr-3' : ''"> <router-view v-slot="{ Component }" class="main-page" :class="route.meta.noScroll ? 'pr-3' : ''">
<template v-if="route.meta.noScroll"> <template v-if="route.meta.noScroll">
<keep-alive v-if="!route.meta.noKeepAlive"> <keep-alive v-if="!route.meta.noKeepAlive">
<component :is="Component" /> <component :is="Component" />
</keep-alive> </keep-alive>
<component v-else :is="Component"/> <component :is="Component" v-else />
</template> </template>
<template v-else> <template v-else>
<n-scrollbar> <n-scrollbar>
<keep-alive v-if="!route.meta.noKeepAlive"> <keep-alive v-if="!route.meta.noKeepAlive">
<component :is="Component" /> <component :is="Component" />
</keep-alive> </keep-alive>
<component v-else :is="Component"/> <component :is="Component" v-else />
</n-scrollbar> </n-scrollbar>
</template> </template>
</router-view> </router-view>
</n-message-provider> </n-message-provider>
</div> </div>
</div>
</div>
<!-- 底部音乐播放 -->
<play-bar v-if="isPlay" />
</div> </div>
</div>
<!-- 底部音乐播放 -->
<play-bar v-if="isPlay" />
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue')); const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue')); const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue')); const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue')); const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
const store = useStore(); const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean) const isPlay = computed(() => store.state.isPlay as boolean);
const menus = store.state.menus; const { menus } = store.state;
const play = computed(() => store.state.play as boolean) const play = computed(() => store.state.play as boolean);
const route = useRoute() const route = useRoute();
const audio = { const audio = {
value: document.querySelector('#MusicAudio') as HTMLAudioElement value: document.querySelector('#MusicAudio') as HTMLAudioElement,
} };
const windowData = window as any const windowData = window as any;
const isElectron = computed(() => { const isElectron = computed(() => {
return !!windowData.electronAPI return !!windowData.electronAPI;
}) });
onMounted(() => { onMounted(() => {
// 监听音乐是否播放 // 监听音乐是否播放
watch( watch(
() => play.value, () => play.value,
value => { (value) => {
if (value && audio.value) { if (value && audio.value) {
audioPlay() audioPlay();
} else { } else {
audioPause() audioPause();
} }
} },
) );
document.onkeyup = (e) => { document.onkeyup = (e) => {
switch (e.code) { switch (e.code) {
case 'Space': case 'Space':
playMusicEvent() playMusicEvent();
break;
default:
} }
} };
// 按下键盘按钮监听 // 按下键盘按钮监听
document.onkeydown = (e) => { document.onkeydown = (e) => {
switch (e.code) { switch (e.code) {
case 'Space': case 'Space':
return false return false;
default:
} }
} };
}) });
const audioPlay = () => { const audioPlay = () => {
if (audio.value) { if (audio.value) {
audio.value.play() audio.value.play();
} }
} };
const audioPause = () => { const audioPause = () => {
if (audio.value) { if (audio.value) {
audio.value.pause() audio.value.pause();
} }
} };
const playMusicEvent = async () => { const playMusicEvent = async () => {
if (play.value) { if (play.value) {
store.commit('setPlayMusic', false) store.commit('setPlayMusic', false);
} else { } else {
store.commit('setPlayMusic', true) store.commit('setPlayMusic', true);
} }
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.layout-page { .layout-page {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@apply flex justify-center items-center overflow-hidden; @apply flex justify-center items-center overflow-hidden;
} }
.layout-main { .layout-main {
@apply bg-black text-white shadow-xl flex flex-col relative; @apply bg-black text-white shadow-xl flex flex-col relative;
height: 100%;
width: 100%;
overflow: hidden;
&-page {
@apply flex flex-1 overflow-hidden;
}
.menu {
width: 90px;
}
.main {
@apply flex-1 box-border flex flex-col;
height: 100%; height: 100%;
width: 100%; &-content {
overflow: hidden; @apply box-border flex-1 overflow-hidden;
&-page{
@apply flex flex-1 overflow-hidden;
}
.menu {
width: 90px;
}
.main {
@apply flex-1 box-border flex flex-col;
height: 100%;
&-content {
@apply box-border flex-1 overflow-hidden;
}
}
:deep(.n-scrollbar-content){
@apply pr-3;
} }
}
:deep(.n-scrollbar-content) {
@apply pr-3;
}
} }
</style> </style>

View File

@@ -8,13 +8,9 @@
</div> </div>
</div> </div>
<div class="app-menu-list"> <div class="app-menu-list">
<div class="app-menu-item" v-for="(item,index) in menus"> <div v-for="(item, index) in menus" :key="item.path" class="app-menu-item">
<router-link class="app-menu-item-link" :to="item.path"> <router-link class="app-menu-item-link" :to="item.path">
<i <i class="iconfont app-menu-item-icon" :style="iconStyle(index)" :class="item.meta.icon"></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">{{ item.meta.title }}</span> <span v-if="isText" class="app-menu-item-text ml-3">{{ item.meta.title }}</span>
</router-link> </router-link>
</div> </div>
@@ -24,44 +20,47 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router';
const props = defineProps({ const props = defineProps({
isText: { isText: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
size: { size: {
type: String, type: String,
default: '26px' default: '26px',
}, },
color: { color: {
type: String, type: String,
default: '#aaa' default: '#aaa',
}, },
selectColor: { selectColor: {
type: String, type: String,
default: '#10B981' default: '#10B981',
}, },
menus: { menus: {
type: Array as any, type: Array as any,
default: [] default: () => [],
} },
}) });
const route = useRoute(); const route = useRoute();
const path = ref(route.path); const path = ref(route.path);
watch(() => route.path, async newParams => { watch(
path.value = newParams () => route.path,
}) async (newParams) => {
path.value = newParams;
},
);
const iconStyle = (index: any) => { const iconStyle = (index: any) => {
let style = { const style = {
fontSize: props.size, fontSize: props.size,
color: path.value === props.menus[index].path ? props.selectColor : props.color color: path.value === props.menus[index].path ? props.selectColor : props.color,
} };
return style return style;
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -83,4 +82,4 @@ const iconStyle = (index: any) => {
transform: scale(1.05); transform: scale(1.05);
transition: 0.2s ease-in-out; transition: 0.2s ease-in-out;
} }
</style> </style>

View File

@@ -1,33 +1,25 @@
<template> <template>
<n-drawer <n-drawer :show="musicFull" height="100vh" placement="bottom" :drawer-style="{ backgroundColor: 'transparent' }">
:show="musicFull"
height="100vh"
placement="bottom"
:drawer-style="{ backgroundColor: 'transparent' }"
>
<div id="drawer-target"> <div id="drawer-target">
<div class="drawer-back" :class="{'paused': !isPlaying}" :style="{backgroundImage:`url(${getImgUrl(playMusic?.picUrl, '300y300')})`}"></div> <div
class="drawer-back"
:class="{ paused: !isPlaying }"
:style="{ backgroundImage: `url(${getImgUrl(playMusic?.picUrl, '300y300')})` }"
></div>
<div class="music-img"> <div class="music-img">
<n-image <n-image ref="PicImgRef" :src="getImgUrl(playMusic?.picUrl, '300y300')" class="img" lazy preview-disabled />
ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '300y300')"
class="img"
lazy
preview-disabled
/>
</div> </div>
<div class="music-content"> <div class="music-content">
<div class="music-content-name">{{ playMusic.song.name }}</div> <div class="music-content-name">{{ playMusic.song.name }}</div>
<div class="music-content-singer"> <div class="music-content-singer">
<span v-for="(item, index) in playMusic.song.artists" :key="index"> <span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name {{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span> </span>
</div> </div>
<n-layout <n-layout
ref="lrcSider"
class="music-lrc" class="music-lrc"
style="height: 55vh" style="height: 55vh"
ref="lrcSider"
:native-scrollbar="false" :native-scrollbar="false"
@mouseover="mouseOverLayout" @mouseover="mouseOverLayout"
@mouseleave="mouseLeaveLayout" @mouseleave="mouseLeaveLayout"
@@ -38,7 +30,8 @@
:class="{ 'now-text': isCurrentLrc(index, nowTime) }" :class="{ 'now-text': isCurrentLrc(index, nowTime) }"
@click="setAudioTime(index, audio)" @click="setAudioTime(index, audio)"
> >
{{ item.text }} <div>{{ item.text }}</div>
<div class="music-lrc-text-tr">{{ item.trText }}</div>
</div> </div>
</template> </template>
</n-layout> </n-layout>
@@ -53,20 +46,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { SongResult } from '@/type/music' import { useStore } from 'vuex';
import { getImgUrl } from '@/utils'
import { useStore } from 'vuex'
import { import {
addCorrectionTime,
isCurrentLrc,
lrcArray, lrcArray,
newLrcIndex, newLrcIndex,
isCurrentLrc, nowTime,
addCorrectionTime,
reduceCorrectionTime, reduceCorrectionTime,
setAudioTime, setAudioTime,
nowTime, } from '@/hooks/MusicHook';
} from '@/hooks/MusicHook' import type { SongResult } from '@/type/music';
import { getImgUrl } from '@/utils';
const store = useStore() const store = useStore();
const props = defineProps({ const props = defineProps({
musicFull: { musicFull: {
@@ -77,39 +71,36 @@ const props = defineProps({
type: HTMLAudioElement, type: HTMLAudioElement,
default: null, default: null,
}, },
}) });
const emit = defineEmits(['update:musicFull'])
// 播放的音乐信息 // 播放的音乐信息
const playMusic = computed(() => store.state.playMusic as SongResult) const playMusic = computed(() => store.state.playMusic as SongResult);
const isPlaying = computed(() => store.state.play as boolean) const isPlaying = computed(() => store.state.play as boolean);
// 获取歌词滚动dom // 获取歌词滚动dom
const lrcSider = ref<any>(null) const lrcSider = ref<any>(null);
const isMouse = ref(false) const isMouse = ref(false);
// 歌词滚动方法 // 歌词滚动方法
const lrcScroll = () => { const lrcScroll = () => {
if (props.musicFull && !isMouse.value) { if (props.musicFull && !isMouse.value) {
let top = newLrcIndex.value * 50 - 225 const top = newLrcIndex.value * 60 - 225;
lrcSider.value.scrollTo({ top: top, behavior: 'smooth' }) lrcSider.value.scrollTo({ top, behavior: 'smooth' });
} }
} };
const mouseOverLayout = () => { const mouseOverLayout = () => {
isMouse.value = true isMouse.value = true;
} };
const mouseLeaveLayout = () => { const mouseLeaveLayout = () => {
setTimeout(() => { setTimeout(() => {
isMouse.value = false isMouse.value = false;
}, 3000) }, 3000);
} };
defineExpose({ defineExpose({
lrcScroll, lrcScroll,
}) });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@keyframes round { @keyframes round {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
@@ -118,7 +109,7 @@ defineExpose({
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.drawer-back{ .drawer-back {
@apply absolute bg-cover bg-center opacity-70; @apply absolute bg-cover bg-center opacity-70;
filter: blur(80px) brightness(80%); filter: blur(80px) brightness(80%);
z-index: -1; z-index: -1;
@@ -161,28 +152,30 @@ defineExpose({
} }
} }
.music-content-time{ .music-content-time {
display: none; display: none;
@apply flex justify-center items-center; @apply flex justify-center items-center;
} }
.music-lrc { .music-lrc {
background-color: inherit; background-color: inherit;
width: 500px; width: 500px;
height: 550px; height: 550px;
.now-text {
@apply text-red-500;
}
&-text { &-text {
@apply text-white text-lg flex justify-center items-center cursor-pointer; @apply text-white text-lg flex flex-col justify-center items-center cursor-pointer font-bold;
height: 50px; height: 60px;
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
&:hover { &:hover {
@apply font-bold text-xl text-red-500; @apply font-bold text-red-500;
} }
}
.now-text { &-tr {
@apply font-bold text-xl text-red-500; @apply text-sm font-normal;
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<!-- 展开全屏 --> <!-- 展开全屏 -->
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="(audio.value as HTMLAudioElement)" /> <music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="audio.value as HTMLAudioElement" />
<!-- 底部播放栏 --> <!-- 底部播放栏 -->
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')"> <div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
<n-image <n-image
@@ -19,8 +19,7 @@
<div class="music-content-name"> <div class="music-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1"> <n-ellipsis class="text-ellipsis" line-clamp="1">
<span v-for="(item, index) in playMusic.song.artists" :key="index"> <span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name {{ item.name }}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span> </span>
</n-ellipsis> </n-ellipsis>
</div> </div>
@@ -38,22 +37,14 @@
</div> </div>
<div class="music-time"> <div class="music-time">
<div class="time">{{ getNowTime }}</div> <div class="time">{{ getNowTime }}</div>
<n-slider <n-slider v-model:value="timeSlider" :step="0.05" :tooltip="false"></n-slider>
v-model:value="timeSlider"
:step="0.05"
:tooltip="false"
></n-slider>
<div class="time">{{ getAllTime }}</div> <div class="time">{{ getAllTime }}</div>
</div> </div>
<div class="audio-volume"> <div class="audio-volume">
<div> <div>
<i class="iconfont icon-notificationfill"></i> <i class="iconfont icon-notificationfill"></i>
</div> </div>
<n-slider <n-slider v-model:value="volumeSlider" :step="0.01" :tooltip="false"></n-slider>
v-model:value="volumeSlider"
:step="0.01"
:tooltip="false"
></n-slider>
</div> </div>
<div class="audio-button"> <div class="audio-button">
<!-- <n-tooltip trigger="hover" :z-index="9999999"> <!-- <n-tooltip trigger="hover" :z-index="9999999">
@@ -68,12 +59,12 @@
</template> </template>
解析播放 解析播放
</n-tooltip> --> </n-tooltip> -->
<!-- <n-tooltip trigger="hover" :z-index="9999999"> <n-tooltip trigger="hover" :z-index="9999999">
<template #trigger> <template #trigger>
<i class="iconfont icon-full" @click="setMusicFull"></i> <i class="iconfont ri-netease-cloud-music-line" @click="openLyric"></i>
</template> </template>
歌词 歌词
</n-tooltip> --> </n-tooltip>
<n-popover trigger="click" :z-index="99999999" content-class="music-play" raw :show-arrow="false" :delay="200"> <n-popover trigger="click" :z-index="99999999" content-class="music-play" raw :show-arrow="false" :delay="200">
<template #trigger> <template #trigger>
<n-tooltip trigger="manual" :z-index="9999999"> <n-tooltip trigger="manual" :z-index="9999999">
@@ -87,154 +78,143 @@
<div class="music-play-list-back"></div> <div class="music-play-list-back"></div>
<n-scrollbar> <n-scrollbar>
<div class="music-play-list-content"> <div class="music-play-list-content">
<song-item v-for="(item, index) in playList" :key="item.id" :item="item" mini></song-item> <song-item v-for="item in playList" :key="item.id" :item="item" mini></song-item>
</div> </div>
</n-scrollbar> </n-scrollbar>
</div> </div>
</n-popover> </n-popover>
</div> </div>
<!-- 播放音乐 --> <!-- 播放音乐 -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { SongResult } from '@/type/music' import { useStore } from 'vuex';
import { secondToMinute, getImgUrl } from '@/utils'
import { useStore } from 'vuex'
import { setAnimationClass } from '@/utils'
import {
loadLrc,
nowTime,
allTime
} from '@/hooks/MusicHook'
import MusicFull from './MusicFull.vue'
import SongItem from '@/components/common/SongItem.vue'
const store = useStore() import SongItem from '@/components/common/SongItem.vue';
import { allTime, loadLrc, nowTime, openLyric, sendLyricToWin } from '@/hooks/MusicHook';
import type { SongResult } from '@/type/music';
import { getImgUrl, secondToMinute, setAnimationClass } from '@/utils';
import MusicFull from './MusicFull.vue';
const store = useStore();
// 播放的音乐信息 // 播放的音乐信息
const playMusic = computed(() => store.state.playMusic as SongResult) const playMusic = computed(() => store.state.playMusic as SongResult);
// 是否播放 // 是否播放
const play = computed(() => store.state.play as boolean) const play = computed(() => store.state.play as boolean);
const playList = computed(() => store.state.playList as SongResult[]) const playList = computed(() => store.state.playList as SongResult[]);
const audio = { const audio = {
value: document.querySelector('#MusicAudio') as HTMLAudioElement value: document.querySelector('#MusicAudio') as HTMLAudioElement,
} };
watch( watch(
() => store.state.playMusicUrl, () => store.state.playMusicUrl,
() => { () => {
loadLrc(playMusic.value.id) loadLrc(playMusic.value.id);
}, },
{ immediate: true } { immediate: true },
) );
const audioPlay = () => { const audioPlay = () => {
if (audio.value) { if (audio.value) {
audio.value.play() audio.value.play();
} }
} };
const audioPause = () => {
if (audio.value) {
audio.value.pause()
}
}
// 计算属性 获取当前播放时间的进度 // 计算属性 获取当前播放时间的进度
const timeSlider = computed({ const timeSlider = computed({
get: () => (nowTime.value / allTime.value) * 100, get: () => (nowTime.value / allTime.value) * 100,
set: (value) => { set: (value) => {
if (!audio.value) return if (!audio.value) return;
audio.value.currentTime = (value * allTime.value) / 100 audio.value.currentTime = (value * allTime.value) / 100;
audioPlay() audioPlay();
store.commit('setPlayMusic', true) store.commit('setPlayMusic', true);
}, },
}) });
// 音量条 // 音量条
const audioVolume = ref(1) const audioVolume = ref(1);
const volumeSlider = computed({ const volumeSlider = computed({
get: () => audioVolume.value * 100, get: () => audioVolume.value * 100,
set: (value) => { set: (value) => {
if(!audio.value) return if (!audio.value) return;
audio.value.volume = value / 100 audio.value.volume = value / 100;
}, },
}) });
// 获取当前播放时间 // 获取当前播放时间
const getNowTime = computed(() => { const getNowTime = computed(() => {
return secondToMinute(nowTime.value) return secondToMinute(nowTime.value);
}) });
// 获取总时间 // 获取总时间
const getAllTime = computed(() => { const getAllTime = computed(() => {
return secondToMinute(allTime.value) return secondToMinute(allTime.value);
}) });
// 监听音乐播放 获取时间 // 监听音乐播放 获取时间
const onAudio = () => { const onAudio = () => {
if(audio.value){ if (audio.value) {
audio.value.removeEventListener('timeupdate', handleGetAudioTime) audio.value.removeEventListener('timeupdate', handleGetAudioTime);
audio.value.removeEventListener('ended', handleEnded) audio.value.removeEventListener('ended', handleEnded);
audio.value.addEventListener('timeupdate', handleGetAudioTime) audio.value.addEventListener('timeupdate', handleGetAudioTime);
audio.value.addEventListener('ended', handleEnded) audio.value.addEventListener('ended', handleEnded);
// 监听音乐播放暂停 // 监听音乐播放暂停
audio.value.addEventListener('pause', () => { audio.value.addEventListener('pause', () => {
store.commit('setPlayMusic', false) store.commit('setPlayMusic', false);
}) });
audio.value.addEventListener('play', () => { audio.value.addEventListener('play', () => {
store.commit('setPlayMusic', true) store.commit('setPlayMusic', true);
}) });
} }
} };
onAudio() onAudio();
function handleEnded() { function handleEnded() {
store.commit('nextPlay') store.commit('nextPlay');
} }
function handlePrev() { function handlePrev() {
store.commit('prevPlay') store.commit('prevPlay');
} }
const MusicFullRef = ref<any>(null) const MusicFullRef = ref<any>(null);
function handleGetAudioTime(this: any) { function handleGetAudioTime(this: any) {
// 监听音频播放的实时时间事件 // 监听音频播放的实时时间事件
const audio = this as HTMLAudioElement const audio = this as HTMLAudioElement;
// 获取当前播放时间 // 获取当前播放时间
nowTime.value = Math.floor(audio.currentTime) nowTime.value = Math.floor(audio.currentTime);
// 获取总时间 // 获取总时间
allTime.value = audio.duration allTime.value = audio.duration;
// 获取音量 // 获取音量
audioVolume.value = audio.volume audioVolume.value = audio.volume;
MusicFullRef.value?.lrcScroll() sendLyricToWin(store.state.isPlay);
MusicFullRef.value?.lrcScroll();
} }
// 播放暂停按钮事件 // 播放暂停按钮事件
const playMusicEvent = async () => { const playMusicEvent = async () => {
if (play.value) { if (play.value) {
store.commit('setPlayMusic', false) store.commit('setPlayMusic', false);
} else { } else {
store.commit('setPlayMusic', true) store.commit('setPlayMusic', true);
} }
} };
const musicFull = ref(false) const musicFull = ref(false);
// 设置musicFull // 设置musicFull
const setMusicFull = () => { const setMusicFull = () => {
musicFull.value = !musicFull.value musicFull.value = !musicFull.value;
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.text-ellipsis { .text-ellipsis {
width: 100%; width: 100%;
} }
@@ -243,7 +223,9 @@ const setMusicFull = () => {
@apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2; @apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2;
z-index: 9999; z-index: 9999;
box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034); box-shadow: 0px 0px 10px 2px rgba(203, 203, 203, 0.034);
background-color: rgba(0, 0, 0, 0.747); .music-content { background-color: rgba(0, 0, 0, 0.747);
animation-duration: 0.5s !important;
.music-content {
width: 140px; width: 140px;
@apply ml-4; @apply ml-4;
@@ -310,16 +292,15 @@ const setMusicFull = () => {
} }
} }
.music-play{ .music-play {
&-list {
&-list{
height: 50vh; height: 50vh;
@apply relative rounded-3xl overflow-hidden; @apply relative rounded-3xl overflow-hidden;
&-back{ &-back {
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
@apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75; @apply absolute top-0 left-0 w-full h-full bg-gray-800 bg-opacity-75;
} }
&-content{ &-content {
padding: 10px; padding: 10px;
} }
} }

View File

@@ -1,168 +1,161 @@
<template> <template>
<div class="search-box flex"> <div class="search-box flex">
<div class="search-box-input flex-1"> <div class="search-box-input flex-1">
<n-input <n-input
size="medium" v-model:value="searchValue"
round size="medium"
v-model:value="searchValue" round
:placeholder="hotSearchKeyword" :placeholder="hotSearchKeyword"
class="border border-gray-600" class="border border-gray-600"
@keydown.enter="search" @keydown.enter="search"
> >
<template #prefix> <template #prefix>
<i class="iconfont icon-search"></i> <i class="iconfont icon-search"></i>
</template> </template>
<template #suffix> <template #suffix>
<div class="w-20 px-3 flex justify-between items-center"> <div class="w-20 px-3 flex justify-between items-center">
<div>{{ searchTypeOptions.find(item => item.key === searchType)?.label }}</div> <div>{{ searchTypeOptions.find((item) => item.key === searchType)?.label }}</div>
<n-dropdown trigger="hover" @select="selectSearchType" :options="searchTypeOptions"> <n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
<i class="iconfont icon-xiasanjiaoxing"></i> <i class="iconfont icon-xiasanjiaoxing"></i>
</n-dropdown> </n-dropdown>
</div> </div>
</template> </template>
</n-input> </n-input>
</div> </div>
<div class="user-box"> <div class="user-box">
<n-dropdown trigger="hover" @select="selectItem" :options="userSetOptions"> <n-dropdown trigger="hover" :options="userSetOptions" @select="selectItem">
<i class="iconfont icon-xiasanjiaoxing"></i> <i class="iconfont icon-xiasanjiaoxing"></i>
</n-dropdown> </n-dropdown>
<n-avatar <n-avatar
class="ml-2 cursor-pointer" v-if="store.state.user"
circle class="ml-2 cursor-pointer"
size="medium" circle
:src="getImgUrl(store.state.user.avatarUrl)" size="medium"
v-if="store.state.user" :src="getImgUrl(store.state.user.avatarUrl)"
/> />
<div class="mx-2 rounded-full cursor-pointer text-sm" v-else @click="toLogin">登录</div> <div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">登录</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getSearchKeyword } from '@/api/home'; import { useRouter } from 'vue-router';
import { getUserDetail, logout } from '@/api/login'; import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex'; import { getSearchKeyword } from '@/api/home';
import request from '@/utils/request_mt' import { getUserDetail, logout } from '@/api/login';
import { getImgUrl } from '@/utils'; import { SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
import {USER_SET_OPTIONS, SEARCH_TYPES} from '@/const/bar-const' import { getImgUrl } from '@/utils';
import request from '@/utils/request_mt';
const router = useRouter()
const store = useStore(); const router = useRouter();
const userSetOptions = ref(USER_SET_OPTIONS) const store = useStore();
const userSetOptions = ref(USER_SET_OPTIONS);
// 推荐热搜词 // 推荐热搜词
const hotSearchKeyword = ref("搜索点什么吧...") const hotSearchKeyword = ref('搜索点什么吧...');
const hotSearchValue = ref("") const hotSearchValue = ref('');
const loadHotSearchKeyword = async () => { const loadHotSearchKeyword = async () => {
const { data } = await getSearchKeyword(); const { data } = await getSearchKeyword();
hotSearchKeyword.value = data.data.showKeyword hotSearchKeyword.value = data.data.showKeyword;
hotSearchValue.value = data.data.realkeyword hotSearchValue.value = data.data.realkeyword;
} };
const loadPage = async () => { const loadPage = async () => {
const token = localStorage.getItem("token") const token = localStorage.getItem('token');
if (!token) return if (!token) return;
const { data } = await getUserDetail() const { data } = await getUserDetail();
store.state.user = data.profile store.state.user = data.profile;
localStorage.setItem('user', JSON.stringify(data.profile)) localStorage.setItem('user', JSON.stringify(data.profile));
} };
watchEffect(() => {
watchEffect(() => { loadPage();
loadPage() if (store.state.user) {
if (store.state.user) { userSetOptions.value = USER_SET_OPTIONS;
userSetOptions.value = USER_SET_OPTIONS } else {
} else { userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
userSetOptions.value = USER_SET_OPTIONS.filter(item => item.key !== 'logout') }
} });
})
const toLogin = () => {
router.push('/login');
const toLogin = () => { };
router.push('/login')
} // 页面初始化
onMounted(() => {
// 页面初始化 loadHotSearchKeyword();
onMounted(() => { loadPage();
loadHotSearchKeyword() });
loadPage()
}) // 搜索词
const searchValue = ref('');
const searchType = ref(1);
// 搜索词 const search = () => {
const searchValue = ref("") const { value } = searchValue;
const searchType = ref(1) if (value === '') {
const search = () => { searchValue.value = hotSearchValue.value;
let value = searchValue.value } else {
if (value == "") { router.push({
searchValue.value = hotSearchValue.value path: '/search',
} else { query: {
router.push({ keyword: value,
path: "/search", type: searchType.value,
query: { },
keyword: value, });
type: searchType.value }
} };
})
} const selectSearchType = (key: any) => {
} searchType.value = key;
};
const selectSearchType = (key: any) => {
searchType.value = key const searchTypeOptions = ref(SEARCH_TYPES);
}
const selectItem = async (key: any) => {
// switch 判断
const searchTypeOptions = ref(SEARCH_TYPES) switch (key) {
case 'card':
const selectItem = async (key: any) => { await request.get('/?do=sign').then((res) => {
// switch 判断 console.log(res);
switch (key) { });
case 'card': break;
await request.get('/?do=sign') case 'card_music':
.then(res => { await request.get('/?do=daka').then((res) => {
console.log(res) console.log(res);
}) });
break; break;
case 'card_music': case 'listen':
await request.get('/?do=daka') await request.get('/?do=listen&id=1885175990&time=300').then((res) => {
.then(res => { console.log(res);
console.log(res) });
}) break;
break; case 'logout':
case 'listen': logout().then(() => {
await request.get('/?do=listen&id=1885175990&time=300') store.state.user = null;
.then(res => { localStorage.clear();
console.log(res) });
}) break;
break; case 'login':
case 'logout': router.push('/login');
logout().then(() => { break;
store.state.user = null case 'set':
localStorage.clear() router.push('/set');
}) break;
break; default:
case 'login': }
router.push("/login") };
break; </script>
case 'set':
router.push("/set") <style lang="scss" scoped>
break; .user-box {
} @apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
} background: #1a1a1a;
}
</script> .search-box {
@apply pb-4 pr-4;
<style lang="scss" scoped> }
.user-box { .search-box-input {
@apply ml-4 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600; @apply relative;
background: #1a1a1a; }
} </style>
.search-box{
@apply pb-4 pr-4;
}
.search-box-input {
@apply relative;
}
</style>

View File

@@ -5,9 +5,6 @@
<button @click="minimize"> <button @click="minimize">
<i class="iconfont icon-minisize"></i> <i class="iconfont icon-minisize"></i>
</button> </button>
<!-- <button @click="maximize">
<i class="iconfont icon-maxsize"></i>
</button> -->
<button @click="close"> <button @click="close">
<i class="iconfont icon-close"></i> <i class="iconfont icon-close"></i>
</button> </button>
@@ -16,18 +13,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDialog } from 'naive-ui' import { useDialog } from 'naive-ui';
const dialog = useDialog() const dialog = useDialog();
const windowData = window as any const windowData = window as any;
const minimize = () => { const minimize = () => {
windowData.electronAPI.minimize() windowData.electronAPI.minimize();
} };
const maximize = () => {
windowData.electronAPI.maximize()
}
const close = () => { const close = () => {
dialog.warning({ dialog.warning({
@@ -36,17 +29,17 @@ const close = () => {
positiveText: '最小化', positiveText: '最小化',
negativeText: '关闭', negativeText: '关闭',
onPositiveClick: () => { onPositiveClick: () => {
windowData.electronAPI.miniTray() windowData.electronAPI.miniTray();
}, },
onNegativeClick: () => { onNegativeClick: () => {
windowData.electronAPI.close() windowData.electronAPI.close();
} },
}) });
} };
const drag = (event: MouseEvent) => { const drag = (event: MouseEvent) => {
windowData.electronAPI.dragStart(event) windowData.electronAPI.dragStart(event);
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -1,5 +1,5 @@
import AppMenu from "./AppMenu.vue"; import AppMenu from './AppMenu.vue';
import PlayBar from "./PlayBar.vue"; import PlayBar from './PlayBar.vue';
import SearchBar from "./SearchBar.vue"; import SearchBar from './SearchBar.vue';
export { AppMenu, PlayBar, SearchBar }; export { AppMenu, PlayBar, SearchBar };

View File

@@ -5,20 +5,20 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps({ defineProps({
lrcList: { lrcList: {
type: Array, type: Array,
default: () => [] default: () => [],
}, },
lrcIndex: { lrcIndex: {
type: Number, type: Number,
default: 0 default: 0,
}, },
lrcTime: { lrcTime: {
type: Number, type: Number,
default: 0 default: 0,
}, },
}) });
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@@ -1,19 +1,17 @@
import { createApp } from "vue"; import 'vfonts/Lato.css';
import App from "./App.vue"; import 'vfonts/FiraCode.css';
// tailwind css
import naive from "naive-ui"; import './index.css';
import "vfonts/Lato.css"; import 'remixicon/fonts/remixicon.css';
import "vfonts/FiraCode.css";
import { createApp } from 'vue';
// tailwind css
import "./index.css"; import router from '@/router';
import store from '@/store';
import router from "@/router";
import App from './App.vue';
import store from "@/store";
const app = createApp(App);
const app = createApp(App); app.use(router);
app.use(router); app.use(store);
app.use(store); app.mount('#app');
// app.use(naive);
app.mount("#app");

View File

@@ -57,5 +57,5 @@ const layoutRouter = [
}, },
component: () => import('@/views/user/index.vue'), component: () => import('@/views/user/index.vue'),
}, },
] ];
export default layoutRouter; export default layoutRouter;

View File

@@ -1,6 +1,7 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router';
import AppLayout from '@/layout/AppLayout.vue'
import homeRouter from '@/router/home' import AppLayout from '@/layout/AppLayout.vue';
import homeRouter from '@/router/home';
const loginRouter = { const loginRouter = {
path: '/login', path: '/login',
@@ -11,7 +12,7 @@ const loginRouter = {
icon: 'icon-Home', icon: 'icon-Home',
}, },
component: () => import('@/views/login/index.vue'), component: () => import('@/views/login/index.vue'),
} };
const setRouter = { const setRouter = {
path: '/set', path: '/set',
@@ -22,7 +23,7 @@ const setRouter = {
icon: 'icon-Home', icon: 'icon-Home',
}, },
component: () => import('@/views/set/index.vue'), component: () => import('@/views/set/index.vue'),
} };
const routes = [ const routes = [
{ {
@@ -30,9 +31,13 @@ const routes = [
component: AppLayout, component: AppLayout,
children: [...homeRouter, loginRouter, setRouter], children: [...homeRouter, loginRouter, setRouter],
}, },
] {
path: '/lyric',
component: () => import('@/views/lyric/index.vue'),
},
];
export default createRouter({ export default createRouter({
routes: routes, routes,
history: createWebHashHistory(), history: createWebHashHistory(),
}) });

11
src/shims-vue.d.ts vendored
View File

@@ -1,5 +1,6 @@
declare module '*.vue' { declare module '*.vue' {
import { DefineComponent } from 'vue' import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>
export default component const component: DefineComponent<{}, {}, any>;
} export default component;
}

View File

@@ -1,20 +1,21 @@
import { createStore } from 'vuex' import { createStore } from 'vuex';
import { SongResult } from '@/type/music'
import { getMusicUrl, getParsingMusicUrl } from '@/api/music' import { getMusicUrl, getParsingMusicUrl } from '@/api/music';
import homeRouter from '@/router/home' import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { getMusicProxyUrl } from '@/utils' import homeRouter from '@/router/home';
import { useMusicHistory } from '@/hooks/MusicHistoryHook' import { SongResult } from '@/type/music';
import { getMusicProxyUrl } from '@/utils';
interface State { interface State {
menus: any[] menus: any[];
play: boolean play: boolean;
isPlay: boolean isPlay: boolean;
playMusic: SongResult playMusic: SongResult;
playMusicUrl: string playMusicUrl: string;
user: any user: any;
playList: SongResult[] playList: SongResult[];
playListIndex: number playListIndex: number;
setData: any setData: any;
} }
const state: State = { const state: State = {
@@ -27,85 +28,79 @@ const state: State = {
playList: [], playList: [],
playListIndex: 0, playListIndex: 0,
setData: null, setData: null,
} };
const windowData = window as any const windowData = window as any;
const musicHistory = useMusicHistory() const musicHistory = useMusicHistory();
const mutations = { const mutations = {
setMenus(state: State, menus: any[]) { setMenus(state: State, menus: any[]) {
state.menus = menus state.menus = menus;
}, },
async setPlay(state: State, playMusic: SongResult) { async setPlay(state: State, playMusic: SongResult) {
state.playMusic = playMusic state.playMusic = playMusic;
state.playMusicUrl = await getSongUrl(playMusic.id) state.playMusicUrl = await getSongUrl(playMusic.id);
state.play = true state.play = true;
musicHistory.addMusic(playMusic) musicHistory.addMusic(playMusic);
}, },
setIsPlay(state: State, isPlay: boolean) { setIsPlay(state: State, isPlay: boolean) {
state.isPlay = isPlay state.isPlay = isPlay;
}, },
setPlayMusic(state: State, play: boolean) { setPlayMusic(state: State, play: boolean) {
state.play = play state.play = play;
}, },
setPlayList(state: State, playList: SongResult[]) { setPlayList(state: State, playList: SongResult[]) {
state.playListIndex = playList.findIndex( state.playListIndex = playList.findIndex((item) => item.id === state.playMusic.id);
(item) => item.id === state.playMusic.id state.playList = playList;
)
state.playList = playList
}, },
async nextPlay(state: State) { async nextPlay(state: State) {
if (state.playList.length === 0) { if (state.playList.length === 0) {
state.play = true state.play = true;
return return;
} }
state.playListIndex = (state.playListIndex + 1) % state.playList.length state.playListIndex = (state.playListIndex + 1) % state.playList.length;
await updatePlayMusic(state) await updatePlayMusic(state);
}, },
async prevPlay(state: State) { async prevPlay(state: State) {
if (state.playList.length === 0) { if (state.playList.length === 0) {
state.play = true state.play = true;
return return;
} }
state.playListIndex = state.playListIndex = (state.playListIndex - 1 + state.playList.length) % state.playList.length;
(state.playListIndex - 1 + state.playList.length) % state.playList.length await updatePlayMusic(state);
await updatePlayMusic(state)
}, },
async setSetData(state: State, setData: any) { async setSetData(state: State, setData: any) {
state.setData = setData state.setData = setData;
windowData.electron.ipcRenderer.setStoreValue( windowData.electron.ipcRenderer.setStoreValue('set', JSON.parse(JSON.stringify(setData)));
'set',
JSON.parse(JSON.stringify(setData))
)
}, },
} };
const getSongUrl = async (id: number) => { const getSongUrl = async (id: number) => {
const { data } = await getMusicUrl(id) const { data } = await getMusicUrl(id);
let url = '' let url = '';
try { try {
if (data.data[0].freeTrialInfo || !data.data[0].url) { if (data.data[0].freeTrialInfo || !data.data[0].url) {
const res = await getParsingMusicUrl(id) const res = await getParsingMusicUrl(id);
url = res.data.data.url url = res.data.data.url;
} }
} catch (error) { } catch (error) {
console.error('error', error) console.error('error', error);
} }
url = url ? url : data.data[0].url url = url || data.data[0].url;
return getMusicProxyUrl(url) return getMusicProxyUrl(url);
} };
const updatePlayMusic = async (state: State) => { const updatePlayMusic = async (state: State) => {
state.playMusic = state.playList[state.playListIndex] state.playMusic = state.playList[state.playListIndex];
state.playMusicUrl = await getSongUrl(state.playMusic.id) state.playMusicUrl = await getSongUrl(state.playMusic.id);
state.play = true state.play = true;
musicHistory.addMusic(state.playMusic) musicHistory.addMusic(state.playMusic);
} };
const store = createStore({ const store = createStore({
state: state, state,
mutations: mutations, mutations,
}) });
export default store export default store;

View File

@@ -4,30 +4,30 @@ export interface IAlbumNew {
} }
export interface Album { export interface Album {
name: string name: string;
id: number id: number;
type: string type: string;
size: number size: number;
picId: number picId: number;
blurPicUrl: string blurPicUrl: string;
companyId: number companyId: number;
pic: number pic: number;
picUrl: string picUrl: string;
publishTime: number publishTime: number;
description: string description: string;
tags: string tags: string;
company: string company: string;
briefDesc: string briefDesc: string;
artist: Artist artist: Artist;
songs?: any songs?: any;
alias: string[] alias: string[];
status: number status: number;
copyrightId: number copyrightId: number;
commentThreadId: string commentThreadId: string;
artists: Artist2[] artists: Artist2[];
paid: boolean paid: boolean;
onSale: boolean onSale: boolean;
picId_str: string picId_str: string;
} }
interface Artist2 { interface Artist2 {

View File

@@ -1,4 +1,4 @@
export interface IData<T> { export interface IData<T> {
code: number code: number;
data: T data: T;
} }

View File

@@ -7,42 +7,42 @@ export interface IList {
} }
export interface Playlist { export interface Playlist {
name: string name: string;
id: number id: number;
trackNumberUpdateTime: number trackNumberUpdateTime: number;
status: number status: number;
userId: number userId: number;
createTime: number createTime: number;
updateTime: number updateTime: number;
subscribedCount: number subscribedCount: number;
trackCount: number trackCount: number;
cloudTrackCount: number cloudTrackCount: number;
coverImgUrl: string coverImgUrl: string;
coverImgId: number coverImgId: number;
description: string description: string;
tags: string[] tags: string[];
playCount: number playCount: number;
trackUpdateTime: number trackUpdateTime: number;
specialType: number specialType: number;
totalDuration: number totalDuration: number;
creator: Creator creator: Creator;
tracks?: any tracks?: any;
subscribers: Subscriber[] subscribers: Subscriber[];
subscribed: boolean subscribed: boolean;
commentThreadId: string commentThreadId: string;
newImported: boolean newImported: boolean;
adType: number adType: number;
highQuality: boolean highQuality: boolean;
privacy: number privacy: number;
ordered: boolean ordered: boolean;
anonimous: boolean anonimous: boolean;
coverStatus: number coverStatus: number;
recommendInfo?: any recommendInfo?: any;
shareCount: number shareCount: number;
coverImgId_str?: string coverImgId_str?: string;
commentCount: number commentCount: number;
copywriter: string copywriter: string;
tag: string tag: string;
} }
interface Subscriber { interface Subscriber {
@@ -120,8 +120,8 @@ interface AvatarDetail {
} }
interface Expert { interface Expert {
"2": string; '2': string;
"1"?: string; '1'?: string;
} }
// 推荐歌单 // 推荐歌单

View File

@@ -1,203 +1,203 @@
export interface IListDetail { export interface IListDetail {
code: number; code: number;
relatedVideos?: any; relatedVideos?: any;
playlist: Playlist; playlist: Playlist;
urls?: any; urls?: any;
privileges: Privilege[]; privileges: Privilege[];
sharedPrivilege?: any; sharedPrivilege?: any;
resEntrance?: any; resEntrance?: any;
} }
interface Privilege { interface Privilege {
id: number; id: number;
fee: number; fee: number;
payed: number; payed: number;
realPayed: number; realPayed: number;
st: number; st: number;
pl: number; pl: number;
dl: number; dl: number;
sp: number; sp: number;
cp: number; cp: number;
subp: number; subp: number;
cs: boolean; cs: boolean;
maxbr: number; maxbr: number;
fl: number; fl: number;
pc?: any; pc?: any;
toast: boolean; toast: boolean;
flag: number; flag: number;
paidBigBang: boolean; paidBigBang: boolean;
preSell: boolean; preSell: boolean;
playMaxbr: number; playMaxbr: number;
downloadMaxbr: number; downloadMaxbr: number;
rscl?: any; rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege; freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[]; chargeInfoList: ChargeInfoList[];
} }
interface ChargeInfoList { interface ChargeInfoList {
rate: number; rate: number;
chargeUrl?: any; chargeUrl?: any;
chargeMessage?: any; chargeMessage?: any;
chargeType: number; chargeType: number;
} }
interface FreeTrialPrivilege { interface FreeTrialPrivilege {
resConsumable: boolean; resConsumable: boolean;
userConsumable: boolean; userConsumable: boolean;
} }
export interface Playlist { export interface Playlist {
id: number id: number;
name: string name: string;
coverImgId: number coverImgId: number;
coverImgUrl: string coverImgUrl: string;
coverImgId_str: string coverImgId_str: string;
adType: number adType: number;
userId: number userId: number;
createTime: number createTime: number;
status: number status: number;
opRecommend: boolean opRecommend: boolean;
highQuality: boolean highQuality: boolean;
newImported: boolean newImported: boolean;
updateTime: number updateTime: number;
trackCount: number trackCount: number;
specialType: number specialType: number;
privacy: number privacy: number;
trackUpdateTime: number trackUpdateTime: number;
commentThreadId: string commentThreadId: string;
playCount: number playCount: number;
trackNumberUpdateTime: number trackNumberUpdateTime: number;
subscribedCount: number subscribedCount: number;
cloudTrackCount: number cloudTrackCount: number;
ordered: boolean ordered: boolean;
description: string description: string;
tags: string[] tags: string[];
updateFrequency?: any updateFrequency?: any;
backgroundCoverId: number backgroundCoverId: number;
backgroundCoverUrl?: any backgroundCoverUrl?: any;
titleImage: number titleImage: number;
titleImageUrl?: any titleImageUrl?: any;
englishTitle?: any englishTitle?: any;
officialPlaylistType?: any officialPlaylistType?: any;
subscribers: Subscriber[] subscribers: Subscriber[];
subscribed: boolean subscribed: boolean;
creator: Subscriber creator: Subscriber;
tracks: Track[] tracks: Track[];
videoIds?: any videoIds?: any;
videos?: any videos?: any;
trackIds: TrackId[] trackIds: TrackId[];
shareCount: number shareCount: number;
commentCount: number commentCount: number;
remixVideo?: any remixVideo?: any;
sharedUsers?: any sharedUsers?: any;
historySharedUsers?: any historySharedUsers?: any;
} }
interface TrackId { interface TrackId {
id: number; id: number;
v: number; v: number;
t: number; t: number;
at: number; at: number;
alg?: any; alg?: any;
uid: number; uid: number;
rcmdReason: string; rcmdReason: string;
} }
interface Track { interface Track {
name: string; name: string;
id: number; id: number;
pst: number; pst: number;
t: number; t: number;
ar: Ar[]; ar: Ar[];
alia: string[]; alia: string[];
pop: number; pop: number;
st: number; st: number;
rt?: string; rt?: string;
fee: number; fee: number;
v: number; v: number;
crbt?: any; crbt?: any;
cf: string; cf: string;
al: Al; al: Al;
dt: number; dt: number;
h: H; h: H;
m: H; m: H;
l?: H; l?: H;
a?: any; a?: any;
cd: string; cd: string;
no: number; no: number;
rtUrl?: any; rtUrl?: any;
ftype: number; ftype: number;
rtUrls: any[]; rtUrls: any[];
djId: number; djId: number;
copyright: number; copyright: number;
s_id: number; s_id: number;
mark: number; mark: number;
originCoverType: number; originCoverType: number;
originSongSimpleData?: any; originSongSimpleData?: any;
single: number; single: number;
noCopyrightRcmd?: any; noCopyrightRcmd?: any;
mst: number; mst: number;
cp: number; cp: number;
mv: number; mv: number;
rtype: number; rtype: number;
rurl?: any; rurl?: any;
publishTime: number; publishTime: number;
tns?: string[]; tns?: string[];
} }
interface H { interface H {
br: number; br: number;
fid: number; fid: number;
size: number; size: number;
vd: number; vd: number;
} }
interface Al { interface Al {
id: number; id: number;
name: string; name: string;
picUrl: string; picUrl: string;
tns: any[]; tns: any[];
pic_str?: string; pic_str?: string;
pic: number; pic: number;
} }
interface Ar { interface Ar {
id: number; id: number;
name: string; name: string;
tns: any[]; tns: any[];
alias: any[]; alias: any[];
} }
interface Subscriber { interface Subscriber {
defaultAvatar: boolean; defaultAvatar: boolean;
province: number; province: number;
authStatus: number; authStatus: number;
followed: boolean; followed: boolean;
avatarUrl: string; avatarUrl: string;
accountStatus: number; accountStatus: number;
gender: number; gender: number;
city: number; city: number;
birthday: number; birthday: number;
userId: number; userId: number;
userType: number; userType: number;
nickname: string; nickname: string;
signature: string; signature: string;
description: string; description: string;
detailDescription: string; detailDescription: string;
avatarImgId: number; avatarImgId: number;
backgroundImgId: number; backgroundImgId: number;
backgroundUrl: string; backgroundUrl: string;
authority: number; authority: number;
mutual: boolean; mutual: boolean;
expertTags?: any; expertTags?: any;
experts?: any; experts?: any;
djStatus: number; djStatus: number;
vipType: number; vipType: number;
remarkName?: any; remarkName?: any;
authenticationTypes: number; authenticationTypes: number;
avatarDetail?: any; avatarDetail?: any;
backgroundImgIdStr: string; backgroundImgIdStr: string;
anchor: boolean; anchor: boolean;
avatarImgIdStr: string; avatarImgIdStr: string;
avatarImgId_str: string; avatarImgId_str: string;
} }

View File

@@ -1,14 +1,14 @@
export interface ILyric { export interface ILyric {
sgc: boolean; sgc: boolean;
sfy: boolean; sfy: boolean;
qfy: boolean; qfy: boolean;
lrc: Lrc; lrc: Lrc;
klyric: Lrc; klyric: Lrc;
tlyric: Lrc; tlyric: Lrc;
code: number; code: number;
} }
interface Lrc { interface Lrc {
version: number; version: number;
lyric: string; lyric: string;
} }

View File

@@ -1,197 +1,197 @@
export interface IRecommendMusic { export interface IRecommendMusic {
code: number; code: number;
category: number; category: number;
result: SongResult[]; result: SongResult[];
} }
export interface SongResult { export interface SongResult {
id: number id: number;
type: number type: number;
name: string name: string;
copywriter?: any copywriter?: any;
picUrl: string picUrl: string;
canDislike: boolean canDislike: boolean;
trackNumberUpdateTime?: any trackNumberUpdateTime?: any;
song: Song song: Song;
alg: string alg: string;
count?: number count?: number;
} }
interface Song { interface Song {
name: string; name: string;
id: number; id: number;
position: number; position: number;
alias: string[]; alias: string[];
status: number; status: number;
fee: number; fee: number;
copyrightId: number; copyrightId: number;
disc: string; disc: string;
no: number; no: number;
artists: Artist[]; artists: Artist[];
album: Album; album: Album;
starred: boolean; starred: boolean;
popularity: number; popularity: number;
score: number; score: number;
starredNum: number; starredNum: number;
duration: number; duration: number;
playedNum: number; playedNum: number;
dayPlays: number; dayPlays: number;
hearTime: number; hearTime: number;
ringtone: string; ringtone: string;
crbt?: any; crbt?: any;
audition?: any; audition?: any;
copyFrom: string; copyFrom: string;
commentThreadId: string; commentThreadId: string;
rtUrl?: any; rtUrl?: any;
ftype: number; ftype: number;
rtUrls: any[]; rtUrls: any[];
copyright: number; copyright: number;
transName?: any; transName?: any;
sign?: any; sign?: any;
mark: number; mark: number;
originCoverType: number; originCoverType: number;
originSongSimpleData?: any; originSongSimpleData?: any;
single: number; single: number;
noCopyrightRcmd?: any; noCopyrightRcmd?: any;
rtype: number; rtype: number;
rurl?: any; rurl?: any;
mvid: number; mvid: number;
bMusic: BMusic; bMusic: BMusic;
mp3Url?: any; mp3Url?: any;
hMusic: BMusic; hMusic: BMusic;
mMusic: BMusic; mMusic: BMusic;
lMusic: BMusic; lMusic: BMusic;
exclusive: boolean; exclusive: boolean;
privilege: Privilege; privilege: Privilege;
} }
interface Privilege { interface Privilege {
id: number; id: number;
fee: number; fee: number;
payed: number; payed: number;
st: number; st: number;
pl: number; pl: number;
dl: number; dl: number;
sp: number; sp: number;
cp: number; cp: number;
subp: number; subp: number;
cs: boolean; cs: boolean;
maxbr: number; maxbr: number;
fl: number; fl: number;
toast: boolean; toast: boolean;
flag: number; flag: number;
preSell: boolean; preSell: boolean;
playMaxbr: number; playMaxbr: number;
downloadMaxbr: number; downloadMaxbr: number;
rscl?: any; rscl?: any;
freeTrialPrivilege: FreeTrialPrivilege; freeTrialPrivilege: FreeTrialPrivilege;
chargeInfoList: ChargeInfoList[]; chargeInfoList: ChargeInfoList[];
} }
interface ChargeInfoList { interface ChargeInfoList {
rate: number; rate: number;
chargeUrl?: any; chargeUrl?: any;
chargeMessage?: any; chargeMessage?: any;
chargeType: number; chargeType: number;
} }
interface FreeTrialPrivilege { interface FreeTrialPrivilege {
resConsumable: boolean; resConsumable: boolean;
userConsumable: boolean; userConsumable: boolean;
} }
interface BMusic { interface BMusic {
name?: any; name?: any;
id: number; id: number;
size: number; size: number;
extension: string; extension: string;
sr: number; sr: number;
dfsId: number; dfsId: number;
bitrate: number; bitrate: number;
playTime: number; playTime: number;
volumeDelta: number; volumeDelta: number;
} }
interface Album { interface Album {
name: string; name: string;
id: number; id: number;
type: string; type: string;
size: number; size: number;
picId: number; picId: number;
blurPicUrl: string; blurPicUrl: string;
companyId: number; companyId: number;
pic: number; pic: number;
picUrl: string; picUrl: string;
publishTime: number; publishTime: number;
description: string; description: string;
tags: string; tags: string;
company: string; company: string;
briefDesc: string; briefDesc: string;
artist: Artist; artist: Artist;
songs: any[]; songs: any[];
alias: string[]; alias: string[];
status: number; status: number;
copyrightId: number; copyrightId: number;
commentThreadId: string; commentThreadId: string;
artists: Artist[]; artists: Artist[];
subType: string; subType: string;
transName?: any; transName?: any;
onSale: boolean; onSale: boolean;
mark: number; mark: number;
picId_str: string; picId_str: string;
} }
interface Artist { interface Artist {
name: string; name: string;
id: number; id: number;
picId: number; picId: number;
img1v1Id: number; img1v1Id: number;
briefDesc: string; briefDesc: string;
picUrl: string; picUrl: string;
img1v1Url: string; img1v1Url: string;
albumSize: number; albumSize: number;
alias: any[]; alias: any[];
trans: string; trans: string;
musicSize: number; musicSize: number;
topicPerson: number; topicPerson: number;
} }
export interface IPlayMusicUrl { export interface IPlayMusicUrl {
data: Datum[]; data: Datum[];
code: number; code: number;
} }
interface Datum { interface Datum {
id: number; id: number;
url: string; url: string;
br: number; br: number;
size: number; size: number;
md5: string; md5: string;
code: number; code: number;
expi: number; expi: number;
type: string; type: string;
gain: number; gain: number;
fee: number; fee: number;
uf?: any; uf?: any;
payed: number; payed: number;
flag: number; flag: number;
canExtend: boolean; canExtend: boolean;
freeTrialInfo?: any; freeTrialInfo?: any;
level: string; level: string;
encodeType: string; encodeType: string;
freeTrialPrivilege: FreeTrialPrivilege; freeTrialPrivilege: FreeTrialPrivilege;
freeTimeTrialPrivilege: FreeTimeTrialPrivilege; freeTimeTrialPrivilege: FreeTimeTrialPrivilege;
urlSource: number; urlSource: number;
} }
interface FreeTimeTrialPrivilege { interface FreeTimeTrialPrivilege {
resConsumable: boolean; resConsumable: boolean;
userConsumable: boolean; userConsumable: boolean;
type: number; type: number;
remainTime: number; remainTime: number;
} }
interface FreeTrialPrivilege { interface FreeTrialPrivilege {
resConsumable: boolean; resConsumable: boolean;
userConsumable: boolean; userConsumable: boolean;
} }

View File

@@ -1,84 +1,84 @@
export interface IMvItem { export interface IMvItem {
id: number id: number;
cover: string cover: string;
name: string name: string;
playCount: number playCount: number;
briefDesc?: any briefDesc?: any;
desc?: any desc?: any;
artistName: string artistName: string;
artistId: number artistId: number;
duration: number duration: number;
mark: number mark: number;
mv: IMvData mv: IMvData;
lastRank: number lastRank: number;
score: number score: number;
subed: boolean subed: boolean;
artists: Artist[] artists: Artist[];
transNames?: string[] transNames?: string[];
alias?: string[] alias?: string[];
} }
export interface IMvData { export interface IMvData {
authId: number authId: number;
status: number status: number;
id: number id: number;
title: string title: string;
subTitle: string subTitle: string;
appTitle: string appTitle: string;
aliaName: string aliaName: string;
transName: string transName: string;
pic4v3: number pic4v3: number;
pic16v9: number pic16v9: number;
caption: number caption: number;
captionLanguage: string captionLanguage: string;
style?: any style?: any;
mottos: string mottos: string;
oneword?: any oneword?: any;
appword: string appword: string;
stars?: any stars?: any;
desc: string desc: string;
area: string area: string;
type: string type: string;
subType: string subType: string;
neteaseonly: number neteaseonly: number;
upban: number upban: number;
topWeeks: string topWeeks: string;
publishTime: string publishTime: string;
online: number online: number;
score: number score: number;
plays: number plays: number;
monthplays: number monthplays: number;
weekplays: number weekplays: number;
dayplays: number dayplays: number;
fee: number fee: number;
artists: Artist[] artists: Artist[];
videos: Video[] videos: Video[];
} }
interface Video { interface Video {
tagSign: TagSign tagSign: TagSign;
tag: string tag: string;
url: string url: string;
duration: number duration: number;
size: number size: number;
width: number width: number;
height: number height: number;
container: string container: string;
md5: string md5: string;
check: boolean check: boolean;
} }
interface TagSign { interface TagSign {
br: number br: number;
type: string type: string;
tagSign: string tagSign: string;
resolution: number resolution: number;
mvtype: string mvtype: string;
} }
interface Artist { interface Artist {
id: number id: number;
name: string name: string;
} }
// { // {
@@ -97,16 +97,16 @@ interface Artist {
// } // }
export interface IMvUrlData { export interface IMvUrlData {
id: number id: number;
url: string url: string;
r: number r: number;
size: number size: number;
md5: string md5: string;
code: number code: number;
expi: number expi: number;
fee: number fee: number;
mvFee: number mvFee: number;
st: number st: number;
promotionVo: null | any promotionVo: null | any;
msg: string msg: string;
} }

View File

@@ -6,11 +6,11 @@ export interface IPlayListSort {
} }
interface SortCategories { interface SortCategories {
"0": string; '0': string;
"1": string; '1': string;
"2": string; '2': string;
"3": string; '3': string;
"4": string; '4': string;
} }
interface SortAll { interface SortAll {

View File

@@ -540,7 +540,7 @@ interface Song2 {
} }
interface KsongInfos { interface KsongInfos {
"347230": _347230; '347230': _347230;
} }
interface _347230 { interface _347230 {

View File

@@ -1,64 +1,61 @@
// 设置歌手背景图片 // 设置歌手背景图片
export const setBackgroundImg = (url: String) => { export const setBackgroundImg = (url: String) => {
return 'background-image:' + 'url(' + url + ')' return `background-image:url(${url})`;
} };
// 设置动画类型 // 设置动画类型
export const setAnimationClass = (type: String) => { export const setAnimationClass = (type: String) => {
return 'animate__animated ' + type return `animate__animated ${type}`;
} };
// 设置动画延时 // 设置动画延时
export const setAnimationDelay = (index: number = 6, time: number = 50) => { export const setAnimationDelay = (index: number = 6, time: number = 50) => {
return 'animation-delay:' + index * time + 'ms' return `animation-delay:${index * time}ms`;
} };
//将秒转换为分钟和秒 // 将秒转换为分钟和秒
export const secondToMinute = (s: number) => { export const secondToMinute = (s: number) => {
if (!s) { if (!s) {
return '00:00' return '00:00';
} }
let minute: number = Math.floor(s / 60) const minute: number = Math.floor(s / 60);
let second: number = Math.floor(s % 60) const second: number = Math.floor(s % 60);
let minuteStr: string = const minuteStr: string = minute > 9 ? minute.toString() : `0${minute.toString()}`;
minute > 9 ? minute.toString() : '0' + minute.toString() const secondStr: string = second > 9 ? second.toString() : `0${second.toString()}`;
let secondStr: string = return `${minuteStr}:${secondStr}`;
second > 9 ? second.toString() : '0' + second.toString() };
return minuteStr + ':' + secondStr
}
// 格式化数字 千,万, 百万, 千万,亿 // 格式化数字 千,万, 百万, 千万,亿
export const formatNumber = (num: any) => { export const formatNumber = (num: any) => {
num = num * 1 num *= 1;
if (num < 10000) { if (num < 10000) {
return num return num;
} }
if (num < 100000000) { if (num < 100000000) {
return (num / 10000).toFixed(1) + '万' return `${(num / 10000).toFixed(1)}`;
} }
return (num / 100000000).toFixed(1) + '亿' return `${(num / 100000000).toFixed(1)}亿`;
} };
const windowData = window as any const windowData = window as any;
export const getIsMc = () => { export const getIsMc = () => {
if (!windowData.electron) { if (!windowData.electron) {
return false return false;
} }
if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) { if (windowData.electron.ipcRenderer.getStoreValue('set').isProxy) {
return true return true;
} }
return false return false;
} };
const ProxyUrl = const ProxyUrl = import.meta.env.VITE_API_PROXY || 'http://110.42.251.190:9856';
import.meta.env.VITE_API_PROXY + '' || 'http://110.42.251.190:9856'
export const getMusicProxyUrl = (url: string) => { export const getMusicProxyUrl = (url: string) => {
if (!getIsMc()) { if (!getIsMc()) {
return url return url;
} }
const PUrl = url.split('').join('+') const PUrl = url.split('').join('+');
return `${ProxyUrl}/mc?url=${PUrl}` return `${ProxyUrl}/mc?url=${PUrl}`;
} };
export const getImgUrl = computed(() => (url: string, size: string = '') => { export const getImgUrl = computed(() => (url: string, size: string = '') => {
const bdUrl = 'https://image.baidu.com/search/down?url=' const bdUrl = 'https://image.baidu.com/search/down?url=';
const imgUrl = encodeURIComponent(`${url}?param=${size}`) const imgUrl = encodeURIComponent(`${url}?param=${size}`);
return `${bdUrl}${imgUrl}` return `${bdUrl}${imgUrl}`;
}) });

View File

@@ -1,8 +1,9 @@
import axios from "axios"; import axios from 'axios';
let baseURL = import.meta.env.VITE_API + "";
const baseURL = `${import.meta.env.VITE_API}`;
const request = axios.create({ const request = axios.create({
baseURL: baseURL, baseURL,
timeout: 10000, timeout: 10000,
}); });
@@ -11,12 +12,12 @@ request.interceptors.request.use(
(config) => { (config) => {
// 在请求发送之前做一些处理 // 在请求发送之前做一些处理
// 在get请求params中添加timestamp // 在get请求params中添加timestamp
if (config.method === "get") { if (config.method === 'get') {
config.params = { config.params = {
...config.params, ...config.params,
timestamp: Date.now(), timestamp: Date.now(),
}; };
let token = localStorage.getItem("token"); const token = localStorage.getItem('token');
if (token) { if (token) {
config.params.cookie = token; config.params.cookie = token;
} }
@@ -27,7 +28,7 @@ request.interceptors.request.use(
(error) => { (error) => {
// 当请求异常时做一些处理 // 当请求异常时做一些处理
return Promise.reject(error); return Promise.reject(error);
} },
); );
export default request; export default request;

View File

@@ -1,7 +1,8 @@
import axios from "axios"; import axios from 'axios';
let baseURL = import.meta.env.VITE_API_MT + "";
const baseURL = `${import.meta.env.VITE_API_MT}`;
const request = axios.create({ const request = axios.create({
baseURL: baseURL, baseURL,
timeout: 10000, timeout: 10000,
}); });
@@ -13,7 +14,7 @@ request.interceptors.request.use(
(error) => { (error) => {
// 当请求异常时做一些处理 // 当请求异常时做一些处理
return Promise.reject(error); return Promise.reject(error);
} },
); );
export default request; export default request;

View File

@@ -1,19 +1,20 @@
import axios from "axios" import axios from 'axios';
let baseURL = import.meta.env.VITE_API_MUSIC + ""
const baseURL = `${import.meta.env.VITE_API_MUSIC}`;
const request = axios.create({ const request = axios.create({
baseURL: baseURL, baseURL,
timeout: 10000, timeout: 10000,
}) });
// 请求拦截器 // 请求拦截器
request.interceptors.request.use( request.interceptors.request.use(
(config) => { (config) => {
return config return config;
}, },
(error) => { (error) => {
// 当请求异常时做一些处理 // 当请求异常时做一些处理
return Promise.reject(error) return Promise.reject(error);
} },
) );
export default request export default request;

View File

@@ -1,10 +1,16 @@
<template> <template>
<div class="history-page"> <div class="history-page">
<div class="title">播放历史</div> <div class="title">播放历史</div>
<n-button @click="openLyric">打开歌词</n-button>
<n-scrollbar :size="100"> <n-scrollbar :size="100">
<div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')"> <div class="history-list-content" :class="setAnimationClass('animate__bounceInLeft')">
<div class="history-item" v-for="(item, index) in musicList" :key="item.id" <div
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)"> v-for="(item, index) in musicList"
:key="item.id"
class="history-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
>
<song-item class="history-item-content" :item="item" /> <song-item class="history-item-content" :item="item" />
<div class="history-item-count"> <div class="history-item-count">
{{ item.count }} {{ item.count }}
@@ -19,33 +25,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMusicHistory } from '@/hooks/MusicHistoryHook' import { useMusicHistory } from '@/hooks/MusicHistoryHook';
import { setAnimationClass, setAnimationDelay } from "@/utils"; import { setAnimationClass, setAnimationDelay } from '@/utils';
const {delMusic, musicList} = useMusicHistory(); const { delMusic, musicList } = useMusicHistory();
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.history-page{ .history-page {
@apply h-full w-full pt-2; @apply h-full w-full pt-2;
.title{ .title {
@apply text-xl font-bold; @apply text-xl font-bold;
} }
.history-list-content{ .history-list-content {
@apply px-4 mt-2; @apply px-4 mt-2;
.history-item{ .history-item {
@apply flex items-center justify-between; @apply flex items-center justify-between;
&-content{ &-content {
@apply flex-1; @apply flex-1;
} }
&-count{ &-count {
@apply px-4 text-lg; @apply px-4 text-lg;
} }
&-delete{ &-delete {
@apply cursor-pointer rounded-full border-2 border-gray-400 w-8 h-8 flex justify-center items-center; @apply cursor-pointer rounded-full border-2 border-gray-400 w-8 h-8 flex justify-center items-center;
} }
} }
} }
} }
</style> </style>

View File

@@ -1,27 +1,27 @@
<template> <template>
<div class="main-page"> <div class="main-page">
<!-- 推荐歌手 --> <!-- 推荐歌手 -->
<recommend-singer /> <recommend-singer />
<div class="main-content"> <div class="main-content">
<!-- 歌单分类列表 --> <!-- 歌单分类列表 -->
<playlist-type /> <playlist-type />
<!-- 本周最热音乐 --> <!-- 本周最热音乐 -->
<recommend-songlist /> <recommend-songlist />
<!-- 推荐最新专辑 --> <!-- 推荐最新专辑 -->
<recommend-album /> <recommend-album />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const RecommendSinger = defineAsyncComponent(() => import("@/components/RecommendSinger.vue")); const RecommendSinger = defineAsyncComponent(() => import('@/components/RecommendSinger.vue'));
const PlaylistType = defineAsyncComponent(() => import("@/components/PlaylistType.vue")); const PlaylistType = defineAsyncComponent(() => import('@/components/PlaylistType.vue'));
const RecommendSonglist = defineAsyncComponent(() => import("@/components/RecommendSonglist.vue")); const RecommendSonglist = defineAsyncComponent(() => import('@/components/RecommendSonglist.vue'));
const RecommendAlbum = defineAsyncComponent(() => import("@/components/RecommendAlbum.vue")); const RecommendAlbum = defineAsyncComponent(() => import('@/components/RecommendAlbum.vue'));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.main-page{ .main-page {
@apply h-full w-full; @apply h-full w-full;
} }
.main-content { .main-content {

View File

@@ -1,75 +1,66 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getRecommendList, getListDetail, getListByCat } from '@/api/list'
import type { IRecommendItem } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
import { setAnimationClass, setAnimationDelay, getImgUrl, formatNumber } from "@/utils";
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import MusicList from "@/components/MusicList.vue";
import { getListByCat, getListDetail, getRecommendList } from '@/api/list';
import PlayBottom from '@/components/common/PlayBottom.vue'; import PlayBottom from '@/components/common/PlayBottom.vue';
import MusicList from '@/components/MusicList.vue';
import type { IRecommendItem } from '@/type/list';
import type { IListDetail } from '@/type/listDetail';
import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const recommendList = ref() const recommendList = ref();
const showMusic = ref(false) const showMusic = ref(false);
const recommendItem = ref<IRecommendItem>() const recommendItem = ref<IRecommendItem>();
const listDetail = ref<IListDetail>() const listDetail = ref<IListDetail>();
const selectRecommendItem = async (item: IRecommendItem) => { const selectRecommendItem = async (item: IRecommendItem) => {
showMusic.value = true showMusic.value = true;
const { data } = await getListDetail(item.id) const { data } = await getListDetail(item.id);
recommendItem.value = item recommendItem.value = item;
listDetail.value = data listDetail.value = data;
} };
const route = useRoute(); const route = useRoute();
const listTitle = ref(route.query.type || "歌单列表"); const listTitle = ref(route.query.type || '歌单列表');
const loadList = async (type: any) => { const loadList = async (type: any) => {
const params = { const params = {
cat: type || '', cat: type || '',
limit: 30, limit: 30,
offset: 0 offset: 0,
} };
const { data } = await getListByCat(params); const { data } = await getListByCat(params);
recommendList.value = data.playlists recommendList.value = data.playlists;
} };
if (route.query.type) { if (route.query.type) {
loadList(route.query.type) loadList(route.query.type);
} else { } else {
getRecommendList().then((res: { data: { result: any; }; }) => { getRecommendList().then((res: { data: { result: any } }) => {
recommendList.value = res.data.result recommendList.value = res.data.result;
}) });
} }
watch( watch(
() => route.query, () => route.query,
async newParams => { async (newParams) => {
if(newParams.type){ if (newParams.type) {
const params = {
tag: newParams.type || '',
limit: 30,
before: 0
}
loadList(newParams.type); loadList(newParams.type);
} }
} },
) );
</script> </script>
<template> <template>
<div class="list-page"> <div class="list-page">
<div <div class="recommend-title" :class="setAnimationClass('animate__bounceInLeft')">{{ listTitle }}</div>
class="recommend-title"
:class="setAnimationClass('animate__bounceInLeft')"
>{{ listTitle }}</div>
<!-- 歌单列表 --> <!-- 歌单列表 -->
<n-scrollbar class="recommend" @click="showMusic = false" :size="100"> <n-scrollbar class="recommend" :size="100" @click="showMusic = false">
<div class="recommend-list" v-if="recommendList"> <div v-if="recommendList" class="recommend-list">
<div <div
v-for="(item, index) in recommendList"
:key="item.id"
class="recommend-item" class="recommend-item"
v-for="(item,index) in recommendList"
:class="setAnimationClass('animate__bounceIn')" :class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)" :style="setAnimationDelay(index, 30)"
@click.stop="selectRecommendItem(item)" @click.stop="selectRecommendItem(item)"
@@ -77,7 +68,7 @@ watch(
<div class="recommend-item-img"> <div class="recommend-item-img">
<n-image <n-image
class="recommend-item-img-img" class="recommend-item-img-img"
:src="getImgUrl( (item.picUrl || item.coverImgUrl), '200y200')" :src="getImgUrl(item.picUrl || item.coverImgUrl, '200y200')"
width="200" width="200"
height="200" height="200"
lazy lazy
@@ -91,9 +82,14 @@ watch(
<div class="recommend-item-title">{{ item.name }}</div> <div class="recommend-item-title">{{ item.name }}</div>
</div> </div>
</div> </div>
<PlayBottom/> <play-bottom />
</n-scrollbar> </n-scrollbar>
<MusicList v-if="listDetail?.playlist" v-model:show="showMusic" :name="listDetail?.playlist.name" :song-list="listDetail?.playlist.tracks" /> <music-list
v-if="listDetail?.playlist"
v-model:show="showMusic"
:name="listDetail?.playlist.name"
:song-list="listDetail?.playlist.tracks"
/>
</div> </div>
</template> </template>
@@ -150,5 +146,4 @@ watch(
} }
} }
} }
</style>
</style>

View File

@@ -1,170 +1,165 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getQrKey, createQr, checkQr, getLoginStatus } from '@/api/login' import { useMessage } from 'naive-ui';
import { onMounted } from '@vue/runtime-core'; import { onMounted } from 'vue';
import { getUserDetail, loginByCellphone } from '@/api/login'; import { useRouter } from 'vue-router';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { useMessage } from 'naive-ui'
import { setAnimationClass, setAnimationDelay } from "@/utils"; import { checkQr, createQr, getLoginStatus, getQrKey, getUserDetail, loginByCellphone } from '@/api/login';
import { useRouter } from 'vue-router'; import { setAnimationClass } from '@/utils';
const message = useMessage();
const message = useMessage() const store = useStore();
const store = useStore(); const router = useRouter();
const router = useRouter()
const qrUrl = ref<string>();
const qrUrl = ref<string>() onMounted(() => {
onMounted(() => { loadLogin();
loadLogin() });
})
const loadLogin = async () => {
const loadLogin = async () => { const qrKey = await getQrKey();
const qrKey = await getQrKey() const key = qrKey.data.data.unikey;
const key = qrKey.data.data.unikey const { data } = await createQr(key);
const { data } = await createQr(key) qrUrl.value = data.data.qrimg;
qrUrl.value = data.data.qrimg timerIsQr(key);
timerIsQr(key) };
}
const timerIsQr = (key: string) => {
const timer = setInterval(async () => {
const { data } = await checkQr(key);
const timerIsQr = (key: string) => {
const timer = setInterval(async () => { if (data.code === 800) {
const { data } = await checkQr(key) clearInterval(timer);
}
if (data.code === 800) { if (data.code === 803) {
clearInterval(timer) // 将token存入localStorage
} localStorage.setItem('token', data.cookie);
if (data.code === 803) { const user = await getUserDetail();
// 将token存入localStorage store.state.user = user.data.profile;
localStorage.setItem('token', data.cookie) message.success('登录成功');
const user = await getUserDetail()
store.state.user = user.data.profile await getLoginStatus().then((res) => {
message.success('登录成功') console.log(res);
});
await getLoginStatus().then(res => { clearInterval(timer);
console.log(res); setTimeout(() => {
}) router.push('/user');
clearInterval(timer) }, 1000);
setTimeout(() => { }
router.push('/user') }, 5000);
}, 1000); };
}
}, 5000); // 是否扫码登陆
} const isQr = ref(true);
const chooseQr = () => {
isQr.value = !isQr.value;
// 是否扫码登陆 };
const isQr = ref(true)
const chooseQr = () => { // 手机号登录
isQr.value = !isQr.value const phone = ref('');
} const password = ref('');
const loginPhone = async () => {
// 手机号登录 const { data } = await loginByCellphone(phone.value, password.value);
const phone = ref('') if (data.code === 200) {
const password = ref('') message.success('登录成功');
const loginPhone = async () => { store.state.user = data.profile;
const { data } = await loginByCellphone(phone.value, password.value) localStorage.setItem('token', data.cookie);
if (data.code === 200) { setTimeout(() => {
message.success('登录成功') router.push('/user');
store.state.user = data.profile }, 1000);
localStorage.setItem('token', data.cookie) }
setTimeout(() => { };
router.push('/user') </script>
}, 1000);
} <template>
} <div class="login-page">
<div class="phone-login">
</script> <div class="bg"></div>
<div class="content">
<template> <div v-if="isQr" class="phone" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-page"> <div class="login-title">扫码登陆</div>
<div class="phone-login"> <img class="qr-img" :src="qrUrl" />
<div class="bg"></div> <div class="text">使用网易云APP扫码登录</div>
<div class="content"> </div>
<div class="phone" v-if="isQr" :class="setAnimationClass('animate__fadeInUp')"> <div v-else class="phone" :class="setAnimationClass('animate__fadeInUp')">
<div class="login-title">扫码登陆</div> <div class="login-title">手机号登录</div>
<img class="qr-img" :src="qrUrl" /> <div class="phone-page">
<div class="text">使用网易云APP扫码登录</div> <input v-model="phone" class="phone-input" type="text" placeholder="手机号" />
</div> <input v-model="password" class="phone-input" type="password" placeholder="密码" />
<div class="phone" v-else :class="setAnimationClass('animate__fadeInUp')"> </div>
<div class="login-title">手机号登录</div> <n-button class="btn-login" @click="loginPhone()">登录</n-button>
<div class="phone-page"> </div>
<input v-model="phone" class="phone-input" type="text" placeholder="手机号" /> </div>
<input v-model="password" class="phone-input" type="password" placeholder="密码" /> <div class="bottom">
</div> <div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
<n-button class="btn-login" @click="loginPhone()">登录</n-button> </div>
</div> </div>
</div> </div>
<div class="bottom"> </template>
<div class="title" @click="chooseQr()">{{ isQr ? '手机号登录' : '扫码登录' }}</div>
</div> <style lang="scss" scoped>
</div> .login-page {
</div> @apply flex flex-col items-center justify-center p-20 pt-20;
</template> }
<style lang="scss" scoped> .login-title {
.login-page { @apply text-2xl font-bold mb-6;
@apply flex flex-col items-center justify-center p-20 pt-20; }
}
.text {
.login-title { @apply mt-4 text-green-500 text-xs;
@apply text-2xl font-bold mb-6; }
}
.phone-login {
.text { width: 350px;
@apply mt-4 text-green-500 text-xs; height: 550px;
} @apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg);
.phone-login { background-color: #383838;
width: 350px; box-shadow: inset 0px 0px 20px 5px #0000005e;
height: 550px;
@apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden; .bg {
background-image: url(http://tva4.sinaimg.cn/large/006opRgRgy1gw8nf6no7uj30rs15n0x7.jpg); @apply absolute w-full h-full bg-black opacity-30;
background-color: #383838; }
box-shadow: inset 0px 0px 20px 5px #0000005e;
.bottom {
.bg { width: 200%;
@apply absolute w-full h-full bg-black opacity-30; height: 250px;
} bottom: -180px;
border-radius: 50%;
.bottom { left: 50%;
width: 200%; padding: 10px;
height: 250px; transform: translateX(-50%);
bottom: -180px; color: #ffffff99;
border-radius: 50%; @apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer;
left: 50%; box-shadow: 10px 0px 20px #000000a9;
padding: 10px; }
transform: translateX(-50%);
color: #ffffff99; .content {
@apply absolute bg-black flex justify-center text-lg font-bold cursor-pointer; @apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center;
box-shadow: 10px 0px 20px #000000a9; .qr-img {
} @apply opacity-80 rounded-2xl cursor-pointer;
}
.content {
@apply absolute w-full h-full p-4 flex flex-col items-center justify-center pb-20 text-center; .phone {
.qr-img { animation-duration: 0.5s;
@apply opacity-80 rounded-2xl cursor-pointer; &-page {
} background-color: #ffffffdd;
width: 250px;
.phone { @apply rounded-2xl overflow-hidden;
animation-duration: 0.5s; }
&-page {
background-color: #ffffffdd; &-input {
width: 250px; height: 40px;
@apply rounded-2xl overflow-hidden; border-bottom: 1px solid #e5e5e5;
} @apply w-full text-black px-4 outline-none;
}
&-input { }
height: 40px; .btn-login {
border-bottom: 1px solid #e5e5e5; width: 250px;
@apply w-full text-black px-4 outline-none; height: 40px;
} @apply mt-10 text-white rounded-xl bg-black opacity-60;
} }
.btn-login { }
width: 250px; }
height: 40px; </style>
@apply mt-10 text-white rounded-xl bg-black opacity-60;
}
}
}
</style>

182
src/views/lyric/index.vue Normal file
View File

@@ -0,0 +1,182 @@
<template>
<div class="lyric-window" :class="theme">
<div class="lyric-bar" :class="{ 'lyric-bar-hover': isDrag }">
<div class="buttons">
<!-- <div class="music-buttons">
<div @click="handlePrev">
<i class="iconfont icon-prev"></i>
</div>
<div class="music-buttons-play" @click="playMusicEvent">
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div>
<div @click="handleEnded">
<i class="iconfont icon-next"></i>
</div>
</div> -->
<div class="check-theme" @click="checkTheme">
<i v-if="theme === 'light'" class="icon ri-sun-line"></i>
<i v-else class="icon ri-moon-line"></i>
</div>
<div class="button-move">
<i class="icon ri-drag-move-2-line"></i>
</div>
</div>
</div>
<div v-if="lyricData.lrcArray[lyricData.nowIndex]" class="lyric-box">
<h2 class="lyric lyric-current">{{ lyricData.lrcArray[lyricData.nowIndex].text }}</h2>
<p class="lyric-current">{{ lyricData.currentLrc.trText }}</p>
<template v-if="lyricData.lrcArray[lyricData.nowIndex + 1]">
<h2 class="lyric lyric-next">
{{ lyricData.lrcArray[lyricData.nowIndex + 1].text }}
</h2>
<p class="lyric-next">{{ lyricData.nextLrc.trText }}</p>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { useIpcRenderer } from '@vueuse/electron';
const windowData = window as any;
const ipcRenderer = useIpcRenderer();
const lyricData = ref({
currentLrc: {
text: '',
trText: '',
},
nextLrc: {
text: '',
trText: '',
},
currentTime: 0,
nextTime: 0,
nowTime: 0,
allTime: 0,
startCurrentTime: 0,
lrcArray: [] as any,
lrcTimeArray: [] as any,
nowIndex: 0,
});
onMounted(() => {
ipcRenderer.on('receive-lyric', (event, data) => {
try {
lyricData.value = JSON.parse(data);
console.log('lyricData.value', lyricData.value);
} catch (error) {
console.error('error', error);
}
});
});
const theme = ref('dark');
const checkTheme = () => {
if (theme.value === 'light') {
theme.value = 'dark';
} else {
theme.value = 'light';
}
};
// const drag = (event: MouseEvent) => {
// windowData.electronAPI.dragStart(event);
// };
</script>
<style>
body {
background-color: transparent !important;
}
</style>
<style lang="scss" scoped>
.lyric-window {
width: 100vw;
height: 100vh;
@apply overflow-hidden text-gray-600 hover:bg-gray-400 hover:bg-opacity-75;
&:hover .lyric-bar {
opacity: 1;
}
}
.icon {
@apply text-xl hover:text-white;
}
.lyric-bar {
background-color: #b1b1b1;
@apply flex flex-col justify-center items-center;
width: 100vw;
height: 100px;
opacity: 0;
&:hover {
opacity: 1;
}
}
.lyric-bar-hover {
opacity: 1;
}
.buttons {
width: 100vw;
height: 100px;
@apply flex justify-center items-center;
}
.button-move {
-webkit-app-region: drag;
cursor: move;
}
.music-buttons {
@apply mx-6;
-webkit-app-region: no-drag;
.iconfont {
@apply text-2xl hover:text-green-500 transition;
}
@apply flex items-center;
> div {
@apply cursor-pointer;
}
&-play {
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
background: #383838;
}
}
.check-theme {
font-size: 26px;
cursor: pointer;
opacity: 1;
}
.lyric {
text-shadow: 0 0 10px #fff;
font-size: 4vw;
@apply font-bold m-0 p-0 whitespace-nowrap select-none pointer-events-none;
}
.lyric-current {
color: #333;
}
.lyric-next {
color: #999;
margin: 10px;
}
.lyric-window.dark {
.lyric {
text-shadow: none;
}
.lyric-current {
color: #fff;
}
.lyric-next {
color: #cecece;
}
}
// .lyric-box {
// writing-mode: vertical-rl;
// }
</style>

View File

@@ -5,10 +5,22 @@
</div> </div>
<n-scrollbar :size="100"> <n-scrollbar :size="100">
<div class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')"> <div class="mv-list-content" :class="setAnimationClass('animate__bounceInLeft')">
<div class="mv-item" v-for="(item, index) in mvList" :key="item.id" <div
:class="setAnimationClass('animate__bounceIn')" :style="setAnimationDelay(index, 30)"> v-for="(item, index) in mvList"
:key="item.id"
class="mv-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index, 30)"
>
<div class="mv-item-img" @click="handleShowMv(item)"> <div class="mv-item-img" @click="handleShowMv(item)">
<n-image class="mv-item-img-img" :src="getImgUrl((item.cover), '200y112')" lazy preview-disabled width="200" height="112" /> <n-image
class="mv-item-img-img"
:src="getImgUrl(item.cover, '200y112')"
lazy
preview-disabled
width="200"
height="112"
/>
<div class="top"> <div class="top">
<div class="play-count">{{ formatNumber(item.playCount) }}</div> <div class="play-count">{{ formatNumber(item.playCount) }}</div>
<i class="iconfont icon-videofill"></i> <i class="iconfont icon-videofill"></i>
@@ -35,39 +47,39 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { getTopMv, getMvUrl } from '@/api/mv';
import { IMvItem } from '@/type/mv';
import { setAnimationClass, setAnimationDelay, getImgUrl, formatNumber } from "@/utils";
import { useStore } from 'vuex'; import { useStore } from 'vuex';
const showMv = ref(false) import { getMvUrl, getTopMv } from '@/api/mv';
const mvList = ref<Array<IMvItem>>([]) import { IMvItem } from '@/type/mv';
const playMvItem = ref<IMvItem>() import { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const playMvUrl = ref<string>()
const store = useStore() const showMv = ref(false);
const mvList = ref<Array<IMvItem>>([]);
const playMvItem = ref<IMvItem>();
const playMvUrl = ref<string>();
const store = useStore();
onMounted(async () => { onMounted(async () => {
const res = await getTopMv(30) const res = await getTopMv(30);
mvList.value = res.data.data mvList.value = res.data.data;
console.log('mvList.value', mvList.value) console.log('mvList.value', mvList.value);
}) });
const handleShowMv = async (item: IMvItem) => { const handleShowMv = async (item: IMvItem) => {
store.commit('setIsPlay', false) store.commit('setIsPlay', false);
store.commit('setPlayMusic', false) store.commit('setPlayMusic', false);
showMv.value = true showMv.value = true;
const res = await getMvUrl(item.id) const res = await getMvUrl(item.id);
playMvItem.value = item; playMvItem.value = item;
playMvUrl.value = res.data.data.url playMvUrl.value = res.data.data.url;
} };
const close = () => { const close = () => {
showMv.value = false showMv.value = false;
if (store.state.playMusicUrl) { if (store.state.playMusicUrl) {
store.commit('setIsPlay', true) store.commit('setIsPlay', true);
} }
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -157,4 +169,5 @@ const close = () => {
.mv-detail-title:hover { .mv-detail-title:hover {
@apply top-0; @apply top-0;
} }
}</style> }
</style>

View File

@@ -1,74 +1,63 @@
<template> <template>
<div class="search-page"> <div class="search-page">
<n-layout <n-layout class="hot-search" :class="setAnimationClass('animate__fadeInDown')" :native-scrollbar="false">
class="hot-search" <div class="title">热搜列表</div>
:class="setAnimationClass('animate__fadeInDown')" <div class="hot-search-list">
:native-scrollbar="false" <template v-for="(item, index) in hotSearchData?.data" :key="index">
> <div
<div class="title">热搜列表</div> :class="setAnimationClass('animate__bounceInLeft')"
<div class="hot-search-list"> :style="setAnimationDelay(index, 10)"
<template v-for="(item, index) in hotSearchData?.data"> class="hot-search-item"
<div @click.stop="clickHotKeyword(item.searchWord)"
:class="setAnimationClass('animate__bounceInLeft')" >
:style="setAnimationDelay(index, 10)" <span class="hot-search-item-count" :class="{ 'hot-search-item-count-3': index < 3 }">{{ index + 1 }}</span>
class="hot-search-item" {{ item.searchWord }}
@click.stop="clickHotKeyword(item.searchWord)" </div>
> </template>
<span </div>
class="hot-search-item-count" </n-layout>
:class="{ 'hot-search-item-count-3': index < 3 }" <!-- 搜索到的歌曲列表 -->
>{{ index + 1 }}</span> <n-layout class="search-list" :class="setAnimationClass('animate__fadeInUp')" :native-scrollbar="false">
{{ item.searchWord }} <div class="title">{{ hotKeyword }}</div>
</div> <div class="search-list-box">
<template v-if="searchDetail">
<div
v-for="(item, index) in searchDetail?.songs"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<song-item :item="item" @play="handlePlay" />
</div>
<template v-for="(list, key) in searchDetail">
<template v-if="key.toString() !== 'songs'">
<div
v-for="(item, index) in list"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<SearchItem :item="item" />
</div>
</template> </template>
</div> </template>
</n-layout> </template>
<!-- 搜索到的歌曲列表 --> </div>
<n-layout </n-layout>
class="search-list" </div>
:class="setAnimationClass('animate__fadeInUp')"
:native-scrollbar="false"
>
<div class="title">{{ hotKeyword }}</div>
<div class="search-list-box">
<template v-if="searchDetail">
<div
v-for="(item, index) in searchDetail?.songs"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<SongItem :item="item" @play="handlePlay"/>
</div>
<template v-for="(list, key) in searchDetail">
<template v-if="key.toString() !== 'songs'">
<div
v-for="(item, index) in list"
:key="item.id"
:class="setAnimationClass('animate__bounceInRight')"
:style="setAnimationDelay(index, 50)"
>
<SearchItem :item="item"/>
</div>
</template>
</template>
</template>
</div>
</n-layout>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getSearch } from "@/api/search"; import { useDateFormat } from '@vueuse/core';
import type { IHotSearch } from "@/type/search"; import { onMounted, ref, watch } from 'vue';
import { getHotSearch } from "@/api/home"; import { useRoute, useRouter } from 'vue-router';
import { useRoute, useRouter } from "vue-router"; import { useStore } from 'vuex';
import { setAnimationClass, setAnimationDelay } from "@/utils";
import { onMounted, ref, watch } from "vue"; import { getHotSearch } from '@/api/home';
import SongItem from "@/components/common/SongItem.vue"; import { getSearch } from '@/api/search';
import { useStore } from "vuex"; import SongItem from '@/components/common/SongItem.vue';
import { useDateFormat } from '@vueuse/core' import type { IHotSearch } from '@/type/search';
import { setAnimationClass, setAnimationDelay } from '@/utils';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -78,105 +67,104 @@ const searchType = ref(Number(route.query.type) || 1);
// 热搜列表 // 热搜列表
const hotSearchData = ref<IHotSearch>(); const hotSearchData = ref<IHotSearch>();
const loadHotSearch = async () => { const loadHotSearch = async () => {
const { data } = await getHotSearch(); const { data } = await getHotSearch();
hotSearchData.value = data; hotSearchData.value = data;
}; };
onMounted(() => { onMounted(() => {
loadHotSearch(); loadHotSearch();
}); });
const hotKeyword = ref(route.query.keyword || "搜索列表"); const hotKeyword = ref(route.query.keyword || '搜索列表');
const clickHotKeyword = (keyword: string) => { const clickHotKeyword = (keyword: string) => {
hotKeyword.value = keyword; hotKeyword.value = keyword;
router.push({ router.push({
path: "/search", path: '/search',
query: { query: {
keyword: keyword, keyword,
type: 1 type: 1,
}, },
}); });
// isHotSearchList.value = false; // isHotSearchList.value = false;
}; };
const dateFormat = (time:any) => useDateFormat(time, 'YYYY.MM.DD').value const dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;
const loadSearch = async (keywords: any) => { const loadSearch = async (keywords: any) => {
hotKeyword.value = keywords; hotKeyword.value = keywords;
searchDetail.value = undefined; searchDetail.value = undefined;
if (!keywords) return; if (!keywords) return;
const { data } = await getSearch({keywords, type:searchType.value}); const { data } = await getSearch({ keywords, type: searchType.value });
const songs = data.result.songs || []; const songs = data.result.songs || [];
const albums = data.result.albums || []; const albums = data.result.albums || [];
// songs map 替换属性 // songs map 替换属性
songs.map((item: any) => { songs.forEach((item: any) => {
item.picUrl = item.al.picUrl; item.picUrl = item.al.picUrl;
item.song = item; item.song = item;
item.artists = item.ar; item.artists = item.ar;
}); });
albums.map((item: any) => { albums.forEach((item: any) => {
item.desc = `${item.artist.name } ${ item.company } ${dateFormat(item.publishTime)}`; item.desc = `${item.artist.name} ${item.company} ${dateFormat(item.publishTime)}`;
}); });
searchDetail.value = { searchDetail.value = {
songs, songs,
albums albums,
} };
}; };
loadSearch(route.query.keyword); loadSearch(route.query.keyword);
watch( watch(
() => route.query, () => route.query,
async newParams => { async (newParams) => {
searchType.value = Number(newParams.type || 1) searchType.value = Number(newParams.type || 1);
loadSearch(newParams.keyword); loadSearch(newParams.keyword);
} },
) );
const store = useStore() const store = useStore();
const handlePlay = (item: any) => { const handlePlay = () => {
const tracks = searchDetail.value?.songs || [] const tracks = searchDetail.value?.songs || [];
store.commit('setPlayList', tracks) store.commit('setPlayList', tracks);
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-page { .search-page {
@apply flex h-full; @apply flex h-full;
} }
.hot-search { .hot-search {
@apply mr-4 rounded-xl flex-1 overflow-hidden; @apply mr-4 rounded-xl flex-1 overflow-hidden;
background-color: #0d0d0d; background-color: #0d0d0d;
animation-duration: 0.2s; animation-duration: 0.2s;
min-width: 400px; min-width: 400px;
height: 100%; height: 100%;
&-list{ &-list {
@apply pb-28; @apply pb-28;
} }
&-item { &-item {
@apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer; @apply px-4 py-3 text-lg hover:bg-gray-700 rounded-xl cursor-pointer;
&-count { &-count {
@apply text-green-500 inline-block ml-3 w-8; @apply text-green-500 inline-block ml-3 w-8;
&-3 { &-3 {
@apply text-red-600 font-bold inline-block ml-3 w-8; @apply text-red-600 font-bold inline-block ml-3 w-8;
} }
}
} }
}
} }
.search-list { .search-list {
@apply flex-1 rounded-xl; @apply flex-1 rounded-xl;
background-color: #0d0d0d; background-color: #0d0d0d;
height: 100%; height: 100%;
&-box{ &-box {
@apply pb-28; @apply pb-28;
} }
} }
.title { .title {
@apply text-gray-200 text-xl font-bold my-2 mx-4; @apply text-gray-200 text-xl font-bold my-2 mx-4;
} }
</style> </style>

View File

@@ -5,7 +5,7 @@
<div class="set-item-title">代理</div> <div class="set-item-title">代理</div>
<div class="set-item-content">无法听音乐时打开</div> <div class="set-item-content">无法听音乐时打开</div>
</div> </div>
<n-switch v-model:value="setData.isProxy"/> <n-switch v-model:value="setData.isProxy" />
</div> </div>
<div class="set-item"> <div class="set-item">
<div> <div>
@@ -30,35 +30,36 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import store from '@/store'
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const setData = ref(store.state.setData) import store from '@/store';
const router = useRouter()
const setData = ref(store.state.setData);
const router = useRouter();
const handelCancel = () => { const handelCancel = () => {
router.back() router.back();
} };
const windowData = window as any const windowData = window as any;
const handleSave = () => { const handleSave = () => {
store.commit('setSetData', setData.value) store.commit('setSetData', setData.value);
windowData.electronAPI.restart() windowData.electronAPI.restart();
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.set-page{ .set-page {
@apply flex flex-col justify-center items-center pt-8; @apply flex flex-col justify-center items-center pt-8;
} }
.set-item{ .set-item {
@apply w-3/5 flex justify-between items-center mb-4; @apply w-3/5 flex justify-between items-center mb-4;
.set-item-title{ .set-item-title {
@apply text-gray-200 text-base; @apply text-gray-200 text-base;
} }
.set-item-content{ .set-item-content {
@apply text-gray-400 text-sm; @apply text-gray-400 text-sm;
} }
} }
</style> </style>

View File

@@ -1,89 +1,87 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useStore } from "vuex"; import { computed, ref } from 'vue';
import { useRouter } from "vue-router"; import { useRouter } from 'vue-router';
import { getUserDetail, getUserPlaylist, getUserRecord } from "@/api/user"; import { useStore } from 'vuex';
import type { IUserDetail } from "@/type/user";
import { computed, ref } from "vue"; import { getListDetail } from '@/api/list';
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils"; import { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';
import { getListDetail } from '@/api/list' import PlayBottom from '@/components/common/PlayBottom.vue';
import SongItem from "@/components/common/SongItem.vue"; import SongItem from '@/components/common/SongItem.vue';
import MusicList from "@/components/MusicList.vue"; import MusicList from '@/components/MusicList.vue';
import type { Playlist } from '@/type/listDetail'; import type { Playlist } from '@/type/listDetail';
import PlayBottom from "@/components/common/PlayBottom.vue"; import type { IUserDetail } from '@/type/user';
import { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';
const store = useStore();
const store = useStore() const router = useRouter();
const router = useRouter() const userDetail = ref<IUserDetail>();
const userDetail = ref<IUserDetail>() const playList = ref<any[]>([]);
const playList = ref<any[]>([]) const recordList = ref();
const recordList = ref() let { user } = store.state;
let user = store.state.user
const loadPage = async () => { const loadPage = async () => {
if (!user) { if (!user) {
router.push("/login") router.push('/login');
return return;
} }
const { data: userData } = await getUserDetail(user.userId) const { data: userData } = await getUserDetail(user.userId);
userDetail.value = userData userDetail.value = userData;
const { data: playlistData } = await getUserPlaylist(user.userId) const { data: playlistData } = await getUserPlaylist(user.userId);
playList.value = playlistData.playlist playList.value = playlistData.playlist;
const { data: recordData } = await getUserRecord(user.userId) const { data: recordData } = await getUserRecord(user.userId);
recordList.value = recordData.allData recordList.value = recordData.allData;
} };
watchEffect(() => { watchEffect(() => {
const localUser = localStorage.getItem('user') const localUser = localStorage.getItem('user');
store.state.user = localUser ? JSON.parse(localUser) : null store.state.user = localUser ? JSON.parse(localUser) : null;
user = store.state.user user = store.state.user;
loadPage() loadPage();
}) });
const isShowList = ref(false);
const isShowList = ref(false) const list = ref<Playlist>();
const list = ref<Playlist>()
// 展示歌单 // 展示歌单
const showPlaylist = async (id: number) => { const showPlaylist = async (id: number) => {
const { data } = await getListDetail(id) const { data } = await getListDetail(id);
isShowList.value = true isShowList.value = true;
list.value = data.playlist list.value = data.playlist;
} };
// 格式化歌曲列表项 // 格式化歌曲列表项
const formatDetail = computed(() => (detail: any) => { const formatDetail = computed(() => (detail: any) => {
let song = { const song = {
artists: detail.ar, artists: detail.ar,
name: detail.al.name, name: detail.al.name,
id: detail.al.id, id: detail.al.id,
} };
detail.song = song detail.song = song;
detail.picUrl = detail.al.picUrl detail.picUrl = detail.al.picUrl;
return detail return detail;
}) });
const handlePlay = (item: any) => {
const tracks = recordList.value || []
store.commit('setPlayList', tracks)
}
const handlePlay = () => {
const tracks = recordList.value || [];
store.commit('setPlayList', tracks);
};
</script> </script>
<template> <template>
<div class="user-page" @click.stop="isShowList = false"> <div class="user-page" @click.stop="isShowList = false">
<div <div
class="left"
v-if="userDetail" v-if="userDetail"
class="left"
:class="setAnimationClass('animate__fadeInLeft')" :class="setAnimationClass('animate__fadeInLeft')"
:style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }" :style="{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }"
> >
<div class="page"> <div class="page">
<div class="user-name">{{ user.nickname }}</div> <div class="user-name">{{ user.nickname }}</div>
<div class="user-info"> <div class="user-info">
<n-avatar round :size="50" :src="getImgUrl(user.avatarUrl,'50y50')" /> <n-avatar round :size="50" :src="getImgUrl(user.avatarUrl, '50y50')" />
<div class="user-info-list"> <div class="user-info-list">
<div class="user-info-item"> <div class="user-info-item">
<div class="label">{{ userDetail.profile.followeds }}</div> <div class="label">{{ userDetail.profile.followeds }}</div>
@@ -104,24 +102,14 @@ const handlePlay = (item: any) => {
<div class="play-list" :class="setAnimationClass('animate__fadeInLeft')"> <div class="play-list" :class="setAnimationClass('animate__fadeInLeft')">
<div class="play-list-title">创建的歌单</div> <div class="play-list-title">创建的歌单</div>
<n-scrollbar> <n-scrollbar>
<div <div v-for="(item, index) in playList" :key="index" class="play-list-item" @click="showPlaylist(item.id)">
class="play-list-item" <n-image :src="getImgUrl(item.coverImgUrl, '50y50')" class="play-list-item-img" lazy preview-disabled />
v-for="(item,index) in playList"
:key="index"
@click="showPlaylist(item.id)"
>
<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-info">
<div class="play-list-item-name">{{ item.name }}</div> <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>
</div> </div>
<PlayBottom/> <play-bottom />
</n-scrollbar> </n-scrollbar>
</div> </div>
</div> </div>
@@ -131,20 +119,20 @@ const handlePlay = (item: any) => {
<div class="record-list"> <div class="record-list">
<n-scrollbar> <n-scrollbar>
<div <div
class="record-item"
v-for="(item, index) in recordList" v-for="(item, index) in recordList"
:key="item.song.id" :key="item.song.id"
class="record-item"
:class="setAnimationClass('animate__bounceInUp')" :class="setAnimationClass('animate__bounceInUp')"
:style="setAnimationDelay(index, 50)" :style="setAnimationDelay(index, 50)"
> >
<SongItem class="song-item" :item="formatDetail(item.song)" @play="handlePlay"/> <song-item class="song-item" :item="formatDetail(item.song)" @play="handlePlay" />
<div class="play-count">{{ item.playCount }}</div> <div class="play-count">{{ item.playCount }}</div>
</div> </div>
<PlayBottom/> <play-bottom />
</n-scrollbar> </n-scrollbar>
</div> </div>
</div> </div>
<MusicList v-if="list" v-model:show="isShowList" :name="list.name" :song-list="list.tracks" /> <music-list v-if="list" v-model:show="isShowList" :name="list.name" :song-list="list.tracks" />
</div> </div>
</template> </template>
@@ -222,5 +210,4 @@ const handlePlay = (item: any) => {
} }
} }
} }
</style>
</style>

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import path from 'path' import path from 'path'
import VueDevTools from 'vite-plugin-vue-devtools' // import VueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
@@ -10,7 +10,7 @@ import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
VueDevTools(), // VueDevTools(),
AutoImport({ AutoImport({
imports: [ imports: [
'vue', 'vue',