feat: 1. 更新音乐插件 2. 组件注册插件增加单插件模式

This commit is contained in:
539943419 committed 2024-09-27 14:04:41 +08:00
1 parent 4b5b7956c7
commit 38e1b098d2
9 files changed
+316 -41

No files matched your search

+1 -2
View File
@@ -113,7 +113,6 @@ onMounted(async () => {
await init(messageProviderRef.value?.alertTip) await init(messageProviderRef.value?.alertTip)
pluginCenter = usePluginCenter() pluginCenter = usePluginCenter()
pluginCenter.usePlugin('miaoDirectory', [rootDirectory], []) pluginCenter.usePlugin('miaoDirectory', [rootDirectory], [])
console.log(rootDirectory.tree)
}) })
// @ts-ignore // @ts-ignore
@@ -138,7 +137,7 @@ provide('rootDirectory', rootDirectory)
</span> </span>
<div class="tab-point" :style="{ backgroundColor: view.color }"></div> <div class="tab-point" :style="{ backgroundColor: view.color }"></div>
<div class="tab-control"> <div class="tab-control">
<n-icon class="tag-control-icon icon" @click="view.switchShow()"> <n-icon class="tag-control-icon icon" @click="view.switchShow()" v-if="view.allowCopy">
<EyeOutline v-show="view.visible" class="icon-inner" /> <EyeOutline v-show="view.visible" class="icon-inner" />
<EyeOffOutline v-show="!view.visible" class="icon-inner" /> <EyeOffOutline v-show="!view.visible" class="icon-inner" />
</n-icon> </n-icon>
+25 -15
View File
@@ -2,6 +2,7 @@ import useVirtualPages from "@/hooks/useVirtualPages";
import VirtualDirectory, { VirtualFile } from "./VirtualDirectory"; import VirtualDirectory, { VirtualFile } from "./VirtualDirectory";
import { ShallowRef, shallowRef } from "vue"; import { ShallowRef, shallowRef } from "vue";
import { alertTipType, usePluginHooksType } from "@/types/type"; import { alertTipType, usePluginHooksType } from "@/types/type";
import VirtualPage from "./VirtualPage";
export enum PluginGroup { export enum PluginGroup {
default = "default", default = "default",
@@ -17,6 +18,8 @@ export interface registerComponentOption {
filter: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[]) => boolean; filter: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[]) => boolean;
disable?: boolean; disable?: boolean;
exitConfirm?: boolean; exitConfirm?: boolean;
// 若为true,则只会创建一个页面,当存在已创建页面则会直接将VFile和VDir加到curr里面
single?: boolean;
} }
export type Plugin = { export type Plugin = {
@@ -31,12 +34,7 @@ export type Plugin = {
// 判断在未禁用时是否可以使用这个组件 // 判断在未禁用时是否可以使用这个组件
// 使用组件即调用下面的func // 使用组件即调用下面的func
filter: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[]) => boolean; filter: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[]) => boolean;
func: ( func: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[], hooks: usePluginHooksType, ...args: any[]) => void;
VDirectories: VirtualDirectory[],
VFiles: VirtualFile[],
hooks: usePluginHooksType,
...args: any[]
) => void;
}; };
export default class PluginCenter { export default class PluginCenter {
@@ -58,7 +56,7 @@ export default class PluginCenter {
* 便捷地注册一个组件为插件,触发时直接用新页面打开 * 便捷地注册一个组件为插件,触发时直接用新页面打开
*/ */
registerComponent(option: registerComponentOption): void { registerComponent(option: registerComponentOption): void {
const { key, name, getComponent, icon, filter, group = [PluginGroup.default], disable = false, exitConfirm = false } = option; const { key, name, getComponent, icon, filter, group = [PluginGroup.default], disable = false, exitConfirm = false, single = false } = option;
let _component: any; let _component: any;
const views = useVirtualPages(); const views = useVirtualPages();
this.register(key, { this.register(key, {
@@ -81,11 +79,18 @@ export default class PluginCenter {
timeout: 2000 timeout: 2000
}); });
} }
views.push(_component, VDirectories, VFiles, { if (single && views.find(_component) !== undefined) {
VirtualPageOption: { const view = views.find(_component) as VirtualPage;
exitConfirm view.currentFiles = [...view.currentFiles, ...VFiles];
} view.currentDirectories = [...view.currentDirectories, ...VDirectories];
}); } else {
views.push(_component, VDirectories, VFiles, {
VirtualPageOption: {
exitConfirm,
allowCopy: single === false
}
});
}
} }
}); });
} }
@@ -112,8 +117,13 @@ export default class PluginCenter {
} }
usePlugin(key: string, VDirectories?: VirtualDirectory[], VFiles?: VirtualFile[], ...args: any[]) { usePlugin(key: string, VDirectories?: VirtualDirectory[], VFiles?: VirtualFile[], ...args: any[]) {
this.pluginsMap[key].func(VDirectories ?? [], VFiles ?? [], { this.pluginsMap[key].func(
globalAlertTip: this.globalAlertTip VDirectories ?? [],
}, ...args); VFiles ?? [],
{
globalAlertTip: this.globalAlertTip
},
...args
);
} }
} }
+4 -2
View File
@@ -36,6 +36,7 @@ export class VirtualFile {
stats: stats; stats: stats;
parent: VirtualDirectory; parent: VirtualDirectory;
id: number; id: number;
storage: {[key: string]: any} = {};
get path() { get path() {
return `${this.parent.path}${this.name}`; return `${this.parent.path}${this.name}`;
} }
@@ -48,7 +49,6 @@ export class VirtualFile {
class VirtualDirectory { class VirtualDirectory {
constructor(info: directory, parent?: VirtualDirectory) { constructor(info: directory, parent?: VirtualDirectory) {
this.name = info.name; this.name = info.name;
this.type = "directory";
this.stats = info.stats; this.stats = info.stats;
this.parent = parent ?? undefined; this.parent = parent ?? undefined;
this.id = generateId(); this.id = generateId();
@@ -60,7 +60,7 @@ class VirtualDirectory {
// 子目录 // 子目录
directories?: VirtualDirectory[]; directories?: VirtualDirectory[];
// 没啥意义,标注一下就是了 // 没啥意义,标注一下就是了
type: "directory"; type: "directory" = 'directory';
// 文件夹创建时间等杂项属性 // 文件夹创建时间等杂项属性
stats: stats; stats: stats;
// 父文件夹 // 父文件夹
@@ -68,6 +68,8 @@ class VirtualDirectory {
id: number; id: number;
storage: {[key: string]: any} = {};
/** /**
* 获取相对于根目录的路径的层数 * 获取相对于根目录的路径的层数
* 用于目录下文件定位 * 用于目录下文件定位
+13 -2
View File
@@ -1,9 +1,10 @@
import { reactive, shallowRef } from 'vue' import { Component, reactive, shallowRef } from 'vue'
import VirtualDirectory, { VirtualFile } from './VirtualDirectory' import VirtualDirectory, { VirtualFile } from './VirtualDirectory'
import generateId from '@/hooks/generateId' import generateId from '@/hooks/generateId'
export interface VirtualPageOption { export interface VirtualPageOption {
exitConfirm?: boolean exitConfirm?: boolean
allowCopy?: boolean
} }
export interface ViewsPushOption { export interface ViewsPushOption {
@@ -44,6 +45,7 @@ export default class VirtualPage {
this.refreshKey = Math.random() this.refreshKey = Math.random()
this.color = getRandomWebSafeColor() this.color = getRandomWebSafeColor()
this.exitConfirm = initObjects?.option?.exitConfirm ?? false this.exitConfirm = initObjects?.option?.exitConfirm ?? false
this.allowCopy = initObjects?.option?.allowCopy ?? true
} }
// 页面使用的组件 // 页面使用的组件
component: any component: any
@@ -61,8 +63,10 @@ export default class VirtualPage {
color: string color: string
// 用于展示的title // 用于展示的title
_title?: string _title?: string
// 关闭时是否需要确认 // 关闭时是否需要确认(未实现)
exitConfirm: boolean exitConfirm: boolean
// 是否允许复制页面
allowCopy: boolean
get title() { get title() {
if (this._title) { if (this._title) {
@@ -155,4 +159,11 @@ export class VirtualPages {
deleteView(index: number) { deleteView(index: number) {
this._views.splice(index, 1) this._views.splice(index, 1)
} }
/**
* 返回第一个使用此组件的页面
*/
find(component: Component) {
return this._views.find(v=>v.component === component)
}
} }
+2 -1
View File
@@ -25,7 +25,8 @@ const pluginConfig: registerComponentOption = {
} }
return vFiles[0].name.endsWith('?') return vFiles[0].name.endsWith('?')
}, },
disable: false disable: false,
single: true
} }
export default pluginConfig export default pluginConfig
@@ -0,0 +1,42 @@
import { createApp, Reactive } from "vue";
import aplayerListNew from "../src/aplayerListNew.vue";
import apType from "../types/ap";
/**
* 对aplayer进行一些自定义的风格优化
*/
const useAplayerMiao = (ap: Reactive<apType>) => {
const container = ap.container as HTMLDivElement;
// 隐藏旧版歌单
const old_listDom = container.querySelector(".aplayer-list") as HTMLDivElement;
old_listDom.style.display = "none";
// 新版歌单
const new_list_dom = document.createElement("div");
new_list_dom.classList.add("aplayer-list");
new_list_dom.style.maxHeight = ap.options.listMaxHeight;
ap.on("listadd", () => {
ap.list.audios.forEach(v => {
!v.id && (v.id = Math.random());
});
});
ap.on("listshow", () => {
new_list_dom.classList.remove("aplayer-list-hide");
});
ap.on("listhide", () => {
new_list_dom.classList.add("aplayer-list-hide");
});
container.appendChild(new_list_dom);
const app = createApp(aplayerListNew, { ap });
app.mount(new_list_dom);
ap.on("destroy", () => {
app.unmount();
});
return app;
};
export default useAplayerMiao;
@@ -1,6 +1,10 @@
<template> <template>
<miao-drop-handler @on-virtual-files="handleDropVFiles"> <miao-drop-handler @on-virtual-files="handleDropVFiles">
<div class="miaoMusic-container"> <div class="miaoMusic-container">
<NScrollbar>
<div class="miaoMusic-main">
</div>
</NScrollbar>
<div ref="aplayer" /> <div ref="aplayer" />
</div> </div>
</miao-drop-handler> </miao-drop-handler>
@@ -8,40 +12,60 @@
<script setup lang="ts"> <script setup lang="ts">
import miaoDropHandler from '@/components/miaoDropHandler.vue' import miaoDropHandler from '@/components/miaoDropHandler.vue'
import useAplayerMiao from './hooks/aplayerMiao';
import { VirtualFile } from '@/class/VirtualDirectory'; import { VirtualFile } from '@/class/VirtualDirectory';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { NScrollbar } from 'naive-ui';
import 'APlayer/dist/APlayer.min.css'; import 'APlayer/dist/APlayer.min.css';
// @ts-ignore // @ts-ignore
import APlayer from 'APlayer'; import APlayer from 'APlayer';
import config from '@/config'; import config from '@/config';
import { difference, uniq } from 'lodash';
import apType from './types/ap';
const currentFiles = defineModel<VirtualFile[]>('currentFiles', { const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true required: true
}); });
const MusicList = computed(() => currentFiles.value.map(v => ({ watch(()=>currentFiles.value, (value, oldValue) => {
ap.list.add(getMusicList(difference(value, oldValue)))
})
const getMusicList = (mp3VFiles: VirtualFile[]) => mp3VFiles.map((v, index) => ({
url: `${config.baseUrl}${v.url}`, url: `${config.baseUrl}${v.url}`,
name: v.name, name: v.name,
artist: '???', artist: ' ',
}))); index
}));
const handleDropVFiles = (files: VirtualFile[]) => { const handleDropVFiles = (files: VirtualFile[]) => {
currentFiles.value = [...currentFiles.value, ...files]; currentFiles.value = uniq([...currentFiles.value, ...files]);
}; };
const aplayer = ref<any>(null); const aplayer = ref<any>(null);
let ap: any let ap: apType
let vueApp: any
onMounted(() => { onMounted(() => {
currentFiles.value = [...currentFiles.value.reverse()] currentFiles.value = [...currentFiles.value.reverse()]
const aplayerOption = { const aplayerOption = {
container: aplayer.value, container: aplayer.value,
audio: MusicList.value, audio: getMusicList(currentFiles.value),
autoplay: true, autoplay: true,
volume: 1,
theme: "#60e8a2"
} }
ap = new APlayer(aplayerOption); ap = reactive<apType>(new APlayer(aplayerOption))
vueApp = useAplayerMiao(ap)
ap.on('listremove', (...args: any) => {
const {index} = args[0] as {index: number}
const _name = ap.list.audios[index].name
currentFiles.value = currentFiles.value.filter(v=>v.name !== _name)
})
console.log({ ap, vueApp })
}); });
onUnmounted(() => { onUnmounted(() => {
@@ -53,21 +77,28 @@ onUnmounted(() => {
.miaoMusic-container { .miaoMusic-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
flex-direction: column;
.miaoMusic-main {
padding-bottom: 88px;
}
} }
</style> </style>
<style lang="scss"> <style lang="scss">
.aplayer { .aplayer {
height: calc(100% - 10px);
display: flex; display: flex;
flex-direction: column; flex-direction: column-reverse;
// .aplayer-list { position: fixed;
// max-height: none !important; bottom: 10px;
// height: fit-content; width: -webkit-fill-available;
// min-height: 0;
// ol { .aplayer-list {
// max-height: none !important; max-height: 444px !important;
// height: fit-content;
// } ol {
// } max-height: inherit !important;
}
}
} }
</style> </style>
@@ -0,0 +1,139 @@
<template>
<ol :style="{ maxHeight: ap.options.listMaxHeight }" :key="refreshKey">
<li v-for="(audio, index) in ap.list.audios" :class="activeAudioIndex == index ? 'aplayer-list-light' : ''"
class="aplayer-list-new" @click="switchAudio(index)" :key="audio.id">
<span class="aplayer-list-cur" :style="{ backgroundColor: apTheme }"></span>
<span class="aplayer-list-index">{{ index + 1 }}</span>
<span class="aplayer-list-title">{{ audio.name }}</span>
<span class="aplayer-list-author" v-show="audio.artist">{{ audio.artist }}</span>
<div class="aplayer-list-control">
<div class="aplayer-list-control-btn" @click.stop="listAudioUp(index)">
<ChevronUpOutline class="aplayer-list-control-btn-svg" />
</div>
<div class="aplayer-list-control-btn" @click.stop="listAudioDown(index)">
<ChevronDownOutline class="aplayer-list-control-btn-svg" />
</div>
<div class="aplayer-list-control-btn" @click.stop="listAudioRemove(index)">
<CloseOutline class="aplayer-list-control-btn-svg" />
</div>
</div>
</li>
</ol>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { ChevronUpOutline, ChevronDownOutline, CloseOutline } from '@vicons/ionicons5';
import apType, { audioType } from '../types/ap';
const props = defineProps<{
ap: apType
}>()
const refreshKey = ref(Math.random())
const apTheme = computed(() => props.ap.list.audios[activeAudioIndex.value].theme ?? props.ap.options.theme)
const activeAudioIndex = computed(() => props.ap.list.index)
props.ap.on('listswitch', (...args: any) => {
// 手动触发更新,不然监听不到
props.ap.list.index = args[0].index as number;
})
const refresh = () => {
refreshKey.value = Math.random()
}
props.ap.on('listremove', refresh)
props.ap.on('listadd', refresh)
const switchAudio = (index: number) => {
if (index === activeAudioIndex.value) {
return
}
props.ap.list.switch(index)
}
const listRefreshIndex = (curr: audioType) => {
for(let index in props.ap.list.audios){
if(props.ap.list.audios[index] === curr) {
props.ap.list.index = parseInt(index)
return
}
}
}
const listAudioUp = (index: number) => {
const arr = props.ap.list.audios;
const len = arr.length
const curr = props.ap.list.audios[props.ap.list.index]
if (index === 0) {
const _item = arr.shift();
_item && arr.push(_item);
} else {
const pre: number = (len - 1 + index) % len;
const _pre = arr[index];
arr[index] = arr[pre];
arr[pre] = _pre;
}
listRefreshIndex(curr)
};
const listAudioDown = (index: number) => {
const arr = props.ap.list.audios;
const len = arr.length
const curr = props.ap.list.audios[props.ap.list.index]
if (index === len - 1) {
const _item = arr.pop();
_item && arr.unshift(_item);
} else {
const pre: number = (len + 1 + index) % len;
const _pre = arr[index];
arr[index] = arr[pre];
arr[pre] = _pre;
}
listRefreshIndex(curr)
}
const listAudioRemove = (index: number) => {
props.ap.list.remove(index)
}
onMounted(() => {
console.log(props.ap)
})
</script>
<style lang="scss" scoped>
.aplayer-list-control {
float: right;
height: 32px;
display: flex;
align-items: center;
&-btn {
width: 25px;
height: 25px;
display: flex;
justify-content: center;
align-items: center;
&-svg {
width: 20px;
aspect-ratio: 1;
transition: all .1s ease;
&:hover {
width: 24px;
}
}
}
}
</style>
<style>
.aplayer svg circle,
.aplayer .aplayer-list-new svg path {
fill: #ffffff00;
}
</style>
+40
View File
@@ -0,0 +1,40 @@
export type audioType = {
url: string
artist: string
cover?: string
name: string
theme?: string
id?: number
index: number
}
type apType = {
list: {
audios: audioType[]
index: number
switch: (index: number) => void
remove: (index: number) => void
add: (audios: audioType[]) => void
refreshAudioIndex: () => void
}
container: HTMLDivElement
on: (event: string, callback: () => void) => void
options: {
audio: audioType[],
autoplay: boolean,
"volume": number,
"theme": string,
"mini": boolean,
"fixed": boolean,
"mutex": boolean,
"lrcType": number,
"preload": "auto",
"loop": "all",
"order": "list",
"listMaxHeight": string,
"storageName": "aplayer-setting"
},
destroy: () => {}
}
export default apType