12 Commits
108 changed files with 9 additions and 13315 deletions

No files matched your search

+2 -32
View File
@@ -1,32 +1,2 @@
# Logs web/*
logs miao-directory.exe
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vscode
miao-directory.exe
miao-directory-amd64-linux
miao-directory-amd64-win.exe
test
stats.html
src-server/web/*
+6
View File
@@ -0,0 +1,6 @@
{
"cSpell.words": [
"birthtime",
"gonic"
]
}
-28
View File
@@ -1,28 +0,0 @@
## TODO list:
| 计划 | 状态 |
|---|---|
| 拆分miaoDirectory | ✅ |
| 完善视频插件 | ✅ |
| 完善PDF插件 | ✅ |
| 完善Code插件 | ✅ |
| 实现miaoDirectory的搜索功能 | ✅ |
| 重构miaoFetchApi.getFile, 增加进度 | ❌ |
| plugin增加些属性,icon, disable, group之类的 | ❌ |
| 完善miaoDirectory的搜索功能 | ❌ |
| 实现miaoDirectory的排序功能 | ❌ |
| 完善图片浏览插件 | ❌ |
| VirtualDirectory更新时更新文件信息 | ❌ |
| 创建音乐播放器组件(仿QQ音乐移动端吧) | ❌ |
| miaoDirectory的复制文件/文件夹功能 | ❌ |
| 重构miaoDropHandler的实现 | ❌ |
| 重构主界面,拆分主界面组件的实现 | ❌ |
| 长期计划 |
|---|
| 无 |
## 碎碎念:
为什么不直接做成类似于windows的风格呢?其实技术实现上问题应该不大,重构也不用重构太多东西(APP.vue改改,插件部分改改这样),但是开学了事情一大堆,实习又没法比较美好地推掉。平时每天到宿舍都精疲力竭还要准备秋招和学校的破事。更何况搞不好还会被两个学分卡得没法毕业,好烦好烦。md想这么多干嘛,反正都快岩壁了,mdmdmdmd
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
View File
File renamed without changes.
-19
View File
@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./icon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MiaoDirectory</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<style>
#app{
height: 100vh;
width: 100vw;
}
</style>
</html>
+1 -1
View File
@@ -22,7 +22,7 @@ var webFS embed.FS
var port = ":17705" var port = ":17705"
func main() { func main() {
gin.SetMode(gin.ReleaseMode) // gin.SetMode(gin.ReleaseMode)
fsOperateEventCenter := fsOperateEventCenter.NewFsOperateEventCenter() fsOperateEventCenter := fsOperateEventCenter.NewFsOperateEventCenter()
-60
View File
@@ -1,60 +0,0 @@
{
"name": "miao-directory-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:host": "vite --host 0.0.0.0",
"build": "vue-tsc -b && vite build",
"build:auto": "pnpm run build && go env -w GOOS=windows GOARCH=amd64 && rm -rf ./src-server/web/* && cp -r ./dist/* ./src-server/web/ && cd ./src-server && go env -w GOOS=windows && go build -o ../miao-directory.exe",
"build:auto:release": "pnpm run build && rm -rf ./src-server/web/* && cp -r ./dist/* ./src-server/web/ && cd ./src-server && go env -w GOOS=windows GOARCH=amd64 && go build -o ../miao-directory-amd64-win.exe && go env -w GOOS=linux GOARCH=amd64 && go build -o ../miao-directory-amd64-linux",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/lang-go": "^6.0.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.5",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/lang-sass": "^6.0.2",
"@codemirror/lang-sql": "^6.7.1",
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.1",
"@types/sortablejs": "^1.15.8",
"@vicons/ionicons5": "^0.12.0",
"aes-js": "^3.1.2",
"aplayer": "^1.10.1",
"axios": "^1.7.3",
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"drag-drop-touch": "^1.3.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"naive-ui": "^2.39.0",
"pdfjs-dist": "^4.5.136",
"sass": "^1.77.8",
"sortablejs": "^1.15.3",
"vue": "3.5.0",
"vue-codemirror": "^6.1.1",
"vue-draggable-plus": "^0.5.3",
"xgplayer": "^3.0.20",
"xgplayer-subtitles": "^3.0.20"
},
"devDependencies": {
"@types/aes-js": "^3.1.4",
"@types/lodash": "^4.17.7",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.1.0",
"@types/pdfjs-dist": "^2.10.378",
"@vitejs/plugin-vue": "^5.1.2",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.5.3",
"vite": "^5.4.0",
"vite-plugin-singlefile": "^2.0.2",
"vue-tsc": "^2.0.29"
}
}
-2732
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File
File renamed without changes.
File renamed without changes.
-415
View File
@@ -1,415 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, provide, ref, shallowRef, useTemplateRef } from 'vue'
import { NIcon, NScrollbar, NDropdown } from 'naive-ui'
import {
EyeOffOutline,
EyeOutline,
CloseOutline,
CopyOutline,
EllipsisVertical,
QrCodeOutline,
ExtensionPuzzleOutline
} from '@vicons/ionicons5'
import type VirtualDirectory from './class/VirtualDirectory'
import type { VirtualFile } from './class/VirtualDirectory'
import MiaoMask from './components/miaoMask.vue'
import MiaoMessageProvider from './components/miaoAlertTipProvider.vue'
import config from './config'
import useVirtualPages from './hooks/useVirtualPages'
import init from './hooks/init'
import usePluginCenter from './hooks/usePluginCenter'
import PluginCenter, { PluginGroup } from './class/PluginCenter'
import { renderIcon } from './hooks/miaoTools'
import { DropdownMixedOption } from 'naive-ui/es/dropdown/src/interface'
import useRootVDirectory from './hooks/useRootVDirectory'
const { baseUrl } = config
const showModal = ref<boolean>(false)
const viewRef = ref<HTMLDivElement>()
const modalData = shallowRef<{
component: any
props: any
}>()
const messageProviderRef = useTemplateRef('messageProviderRef')
const views = useVirtualPages()
let pluginCenter = ref<PluginCenter>()
const rootDirectory = useRootVDirectory()
// component就是一个vue组件,类似于miaoDirectory
const createView = (
component: any,
name: string,
currentDirectories?: VirtualDirectory[],
currentFiles?: VirtualFile[],
index?: number
) => {
views.push(component, name, currentDirectories, currentFiles, { index })
}
const deleteView = (index: number) => {
views.deleteView(index)
}
// 点击标题是隐藏其他标签或显示其他标签,还挺好用的
const handleClickTitle = (index: number) => {
let count = views._views.filter((v) => v.visible).length
let op =
!views._views[index].visible || count > 1 ? 'hideOthers' : 'showOthers'
let _index = 0
while (_index < views._views.length) {
if (_index !== index) {
views._views[_index].visible = op === 'hideOthers' ? false : true
} else {
views._views[_index].visible = true
}
_index += 1
}
}
const openMenuOption = computed(() => [
{
label: '分享',
key: 'share',
icon: renderIcon(QrCodeOutline)
}, pluginCenter.value ? {
label: '插件',
key: 'plugin',
children: [
...pluginCenter.value
.getUsablePlugin([], [], {
group: [PluginGroup.mainMenu]
})
.map((v) => ({
label: `${v.name}`,
key: `plugin:${v.key}`,
icon: v.icon ? renderIcon(v.icon) : undefined
}))
],
icon: renderIcon(ExtensionPuzzleOutline)
} : undefined
].filter(v => v) as DropdownMixedOption[])
const handleMenuSelect = async (key: string) => {
if (key === 'share') {
showModal.value = true
modalData.value = {
component: (await import('naive-ui/es/qr-code')).NQrCode,
props: {
value: baseUrl,
size: 300,
errorCorrectionLevel: 'H'
}
}
} else if (key.startsWith('plugin:')) {
const pluginName = key.split(':')[1]
pluginCenter.value && pluginCenter.value.usePlugin(pluginName, [], [])
}
}
onMounted(async () => {
// @ts-ignore
await init(messageProviderRef.value?.alertTip)
pluginCenter.value = usePluginCenter()
})
// @ts-ignore
provide('globalAlertTip', messageProviderRef.value?.alertTip)
provide('rootDirectory', rootDirectory)
</script>
<template>
<div class="view" ref="viewRef">
<miao-message-provider ref="messageProviderRef">
<div class="view-controller">
<n-scrollbar x-scrollable>
<div class="tabs-container">
<transition-group name="tab">
<div class="tab" v-for="(view, index) of views._views" :key="view.id">
<span class="tab-title" :title="view.title" @click="handleClickTitle(index)">
{{
view.title.length > 5 && 0
? `${view.title.substring(0, 5)}...`
: view.title
}}
</span>
<div class="tab-control">
<div class="tab-point" :style="{ backgroundColor: view.color }"></div>
<n-icon class="tag-control-icon icon" @click="view.switchShow()">
<EyeOutline v-show="view.visible" class="icon-inner" />
<EyeOffOutline v-show="!view.visible" class="icon-inner" />
</n-icon>
<n-icon class="tag-control-icon icon" @click="
createView(
view.component,
view.name,
view.currentDirectories,
view.currentFiles,
index + 1
)
" v-if="view.allowCopy">
<CopyOutline />
</n-icon>
<n-icon class="tag-control-icon icon" @click="deleteView(index)">
<CloseOutline class="icon-inner" />
</n-icon>
</div>
</div>
</transition-group>
</div>
</n-scrollbar>
<div class="view-controller-menu">
<div class="view-controller-menu-item">
<n-dropdown trigger="click" :options="openMenuOption" @select="handleMenuSelect">
<n-icon class="icon">
<EllipsisVertical class="icon-inner" />
</n-icon>
</n-dropdown>
</div>
</div>
</div>
<div class="view-container">
<!-- <transition-group name="page"> -->
<div class="view-container-item" v-for="(view, index) of views._views"
:class="!view.visible ? 'view-container-item-hidden' : ''" :key="view.id">
<component :is="view.component" v-model:current-directories="view.currentDirectories"
v-model:current-files="view.currentFiles" :id="view.id" :color="view.color" :view="view"
:views="views" @exit="deleteView(index)"></component>
</div>
<!-- </transition-group> -->
</div>
</miao-message-provider>
</div>
<!-- 模态框展示,用来显示分享二维码,设置菜单之类的东西 -->
<MiaoMask v-model:show="showModal" @click="showModal = false">
<component @click="(e: any) => e.stopPropagation()" :is="modalData?.component" v-bind="modalData?.props">
</component>
</MiaoMask>
</template>
<style lang="scss" scoped>
$controller-height: 25px;
$tag-width: 170px;
.view {
background-color: #ffffff;
position: relative;
height: 100%;
width: 100%;
.view-controller {
display: flex;
height: $controller-height;
width: 100%;
background-color: #dadada;
// overflow: hidden;
.tabs-container {
display: flex;
margin-left: 5px;
flex: 1;
// overflow-x: scroll;
.tab {
position: relative;
width: $tag-width;
height: $controller-height;
margin: 0 5px;
// flex: 1;
display: flex;
border-radius: 5px 5px 0 0;
align-items: center;
.tab-title {
// font-family: monospace;
// font-weight: bolder;
margin-left: 4px;
width: 90px;
user-select: none;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span {
user-select: none;
cursor: pointer;
}
}
.tab-point {
height: 5px;
aspect-ratio: 1;
border-radius: 66px;
margin-right: 5px;
}
.tab-control {
position: absolute;
right: 10px;
display: flex;
align-items: center;
.tab-control-icon:not(:last-child) {
margin-right: 5px;
}
}
&:not(:last-child)::after {
content: '';
height: calc($controller-height * 0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
right: 0px;
}
}
}
.view-controller-menu {
position: relative;
display: flex;
align-items: center;
padding: 0 5px;
.view-controller-menu-item {
height: 100%;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
}
&::before {
content: '';
height: calc($controller-height * 0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
left: 2px;
}
}
.icon {
height: calc($controller-height * 0.7);
width: calc($controller-height * 0.7);
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background-color: rgba(128, 128, 128, 0);
transition: background-color 0.3s;
&:hover {
background-color: rgba(128, 128, 128, 0.4);
}
}
}
.view-container {
position: relative;
display: flex;
// flex-direction: column;
height: calc(100% - $controller-height);
overflow-y: hidden;
overflow-x: overlay;
.view-container-item {
flex: 1;
height: 100%;
min-width: 180px;
opacity: 1;
transition: flex .25s ease, opacity .25s ease;
}
.view-container-item-hidden {
flex: 0;
min-width: 0px;
opacity: 0;
// transition-delay: opacity .25s;
// transition: opacity 5.25s ease;
}
&::-webkit-scrollbar {
position: absolute;
width: 4px;
}
// 滚动条轨道
&::-webkit-scrollbar-track {
// background: rgb(239, 239, 239);
border-radius: 2px;
}
// 小滑块
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
// border-radius: 5px;
}
&::-webkit-scrollbar-thumb:hover {
background: #999999;
}
}
}
// transition动画相关
.view {
.view-controller {
.tabs-container {
.tab-move,
.tab-enter-active {
transition: all 0.2s ease;
}
.tab-leave-active {
opacity: 0;
transition: none;
}
.tab-enter-from {
opacity: 0;
transform: translateY(-30px);
}
// .tab-leave-to {
// opacity: 0;
// transform: translateY(-80px) translateX(calc($tag-width + 10px));
// }
.tab-leave-active {
position: absolute;
}
}
}
// .view-container {
// .page-move {
// // transition: all 0.55s ease;
// }
// .page-enter-active {}
// .page-leave-active {
// transition: none !important;
// }
// .page-enter-from {
// // opacity: 0;
// }
// .page-leave-active {
// display: none;
// position: absolute;
// }
// }
}
</style>
-155
View File
@@ -1,155 +0,0 @@
import useVirtualPages from "@/hooks/useVirtualPages";
import VirtualDirectory, { VirtualFile } from "./VirtualDirectory";
import { ShallowRef, shallowRef } from "vue";
import { alertTipType, usePluginHooksType } from "@/types/type";
import VirtualPage, { VirtualPages } from "./VirtualPage";
import useRootVDirectory from "@/hooks/useRootVDirectory";
export enum PluginGroup {
default = "default",
mainMenu = "mainMenu",
// 导入的时候会立即执行函数一次
immediately = "immediately"
}
export interface registerComponentOption {
key: string;
name: string;
icon?: any;
getComponent: () => Promise<any>;
group?: PluginGroup[];
filter: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[]) => boolean;
disable?: boolean;
exitConfirm?: boolean;
// 若为true,则只会创建一个页面,当存在已创建页面则会直接将VFile和VDir加到curr里面
single?: boolean;
priority?: number;
}
export type Plugin = {
// 用于显示的名字
name: string;
// 代表插件的icon
icon?: ShallowRef<any>;
// 是否禁用插件
disable: boolean;
// 插件所属的group,用于区分插件使用的场合
group: PluginGroup[];
// 插件优先级
priority: number;
// 判断在未禁用时是否可以使用这个组件
// 使用组件即调用下面的func
filter: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[], VPages: VirtualPages) => boolean;
func: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[], hooks: usePluginHooksType, ...args: any[]) => void;
};
export default class PluginCenter {
constructor(globalAlertTip: alertTipType) {
this.pluginsMap = {};
this.globalAlertTip = globalAlertTip;
}
pluginsMap: {
[key: string]: Plugin;
};
globalAlertTip: alertTipType;
register(key: string, plugin: Plugin) {
this.pluginsMap[key] = plugin;
if (plugin.group.includes(PluginGroup.immediately)) {
this.usePlugin(key, [], [], this.usePluginHook)
}
}
deregister(key: string) {
delete this.pluginsMap[key]
}
/**
* 便捷地注册一个组件为插件,触发时直接用新页面打开,页面名为key
*/
registerComponent(option: registerComponentOption): void {
const { key, name, getComponent, icon, filter, group = [PluginGroup.default], disable = false, exitConfirm = false, single = false, priority = 0 } = option;
let _component: any;
const views = useVirtualPages();
this.register(key, {
name,
filter,
icon: shallowRef(icon),
group,
disable,
priority,
func: async (VDirectories: VirtualDirectory[], VFiles: VirtualFile[]) => {
if (filter(VDirectories, VFiles) === false) {
return;
}
if (_component === undefined) {
const setAlertTip = this.globalAlertTip(`加载 ${name} 插件中`);
_component = await getComponent();
setAlertTip &&
setAlertTip(`加载 ${name} 插件完成`, {
type: "success",
timeout: 2000
});
}
const _findRes = views.find(_component) as VirtualPage[];
if (single && _findRes.length !== 0) {
const view = _findRes[0];
view.currentFiles = [...view.currentFiles, ...VFiles];
view.currentDirectories = [...view.currentDirectories, ...VDirectories];
} else {
views.push(_component, key, VDirectories, VFiles, {
VirtualPageOption: {
exitConfirm,
allowCopy: single === false
}
});
}
}
});
}
getUsablePlugin(
VDirectories: VirtualDirectory[],
VFiles: VirtualFile[],
option?: {
group?: PluginGroup[];
}
) {
const { group = [PluginGroup.default] } = option ?? {};
const usableList = [];
const views = useVirtualPages();
for (let key of Object.keys(this.pluginsMap)) {
const _plugin = this.pluginsMap[key];
if (!_plugin.disable && _plugin.group.filter(v => group.includes(v)).length > 0 && _plugin.filter(VDirectories, VFiles, views)) {
usableList.push(key);
}
}
const res = usableList.map(key => ({
key,
...this.pluginsMap[key]
}));
res.sort((a, b) => b.priority - a.priority);
return res;
}
get usePluginHook(): usePluginHooksType {
return {
globalAlertTip: this.globalAlertTip,
getViews: useVirtualPages,
getPluginCenter: () => this,
getRootVDirectory: () => useRootVDirectory()
}
}
usePlugin(key: string, VDirectories?: VirtualDirectory[], VFiles?: VirtualFile[], ...args: any[]): void;
usePlugin(key: string, VDirectories?: VirtualDirectory[], VFiles?: VirtualFile[], ...args: any[]) {
this.pluginsMap[key].func(
VDirectories ?? [],
VFiles ?? [],
this.usePluginHook,
...args
);
}
}
-165
View File
@@ -1,165 +0,0 @@
import generateId from "@/hooks/generateId";
import useMiaoFetchApi from "@/hooks/useMiaoFetchApi";
import VirtualDirectory from "./VirtualDirectory";
const miaoFetchApi = useMiaoFetchApi();
export class UploadQueueItem {
constructor(
file: File,
targetDirectory: VirtualDirectory,
config?: {
onSuccess?: () => void;
onFailed?: () => void;
}
) {
this.file = file;
this.targetDirectory = targetDirectory;
this.uid = generateId();
this.status = "waiting";
this.process = 0;
this.onSuccess = config && config.onSuccess;
this.onFailed = config && config.onFailed;
}
uid: number;
file: File;
targetDirectory: VirtualDirectory;
status: "waiting" | "uploading" | "success" | "failed";
process: number;
failedReason: unknown | undefined;
onSuccess?: () => void;
onFailed?: () => void;
_abort: (() => void | undefined) | undefined;
_status: Promise<any> | undefined;
/**
* 开始上传文件
*/
startUpload() {
if (this.status === "uploading") {
return;
}
const { abort, response } = miaoFetchApi.upload(this.file, this.targetDirectory, {
onProcess: percent => {
this.process = percent;
}
});
this._abort = abort;
this._status = response;
this.status = "uploading";
response
.then(() => {
this.status = "success";
this.onSuccess && this.onSuccess();
this._abort = undefined;
this._status = undefined;
})
.catch(e => {
this.status = "failed";
this.failedReason = e;
this.onFailed && this.onFailed();
});
}
/**
* 终止上传文件
*/
abortUpload() {
this._abort && this._abort();
this.status = "failed";
this.failedReason = "abort";
}
}
export default class UploadQueue {
constructor() {
this.uid = generateId();
this.queue = [];
this.mode = "serial";
}
uid: number;
queue: UploadQueueItem[];
current: UploadQueueItem | undefined;
// 未来再去实现,目前就是串行的
// 好像也不用去实现
mode: "serial" | "parallel";
get status() {
for (let item of this.queue) {
if (item.status === "uploading") {
return "busy";
}
}
return "free";
}
/**
* 获取下一个需要被上传的item
*/
get nextUplpadItem() {
for (let item of this.queue) {
if (item.status === "waiting") {
return item;
}
}
}
/**
* 开始串行的上传循环
*/
loop() {
if (this.status === "busy") {
return;
}
if (this.nextUplpadItem) {
this.nextUplpadItem.startUpload();
}
}
/**
* 向上传文件队列输送一批新的文件
* @param {any} files 需要上传的文件
* @param {any} targetDirectory 目标文件夹
* @param {any} option 每个文件上传完成以后都会触发一遍的回调函数
* @returns {any}
*/
push(
files: File[],
targetDirectory: VirtualDirectory,
option?: {
/**
* 每个文件上传完成以后都会触发一遍的回调函数
*/
onFinish?: () => void;
/**
* 是否立即开始上传,若否则按队列顺序一个一个上传
*/
immediately?: boolean;
}
) {
const onFinish = option?.onFinish;
const immediately = option?.immediately ?? false;
const uploadQueue = files.map(file => {
return new UploadQueueItem(file, targetDirectory, {
onSuccess: () => {
this.loop();
onFinish && onFinish();
},
onFailed: () => {
this.loop();
}
});
});
this.queue.push(...uploadQueue);
if (!immediately) {
this.loop();
} else {
uploadQueue.forEach(item => {
item.startUpload();
});
}
return uploadQueue;
}
}
-235
View File
@@ -1,235 +0,0 @@
import config from '@/config'
import generateId from '@/hooks/generateId'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import type { stats, file, directory } from '@/types/type.ts'
import difference from 'lodash/difference'
const miaoFetchApi = useMiaoFetchApi()
// const vDirectoryMap = new Map<string, VirtualDirectory>()
// const vFileMap = new Map<string, VirtualFile>()
export interface Tree {
name: string
type: 'directory'
path: string
stats: stats
files: {
name: string
type: 'file'
size: number
stats: stats
url: string
}[]
directories: Tree[]
}
export class VirtualFile {
constructor(info: file, parent: VirtualDirectory) {
this.name = info.name
this.size = info.size
this.stats = info.stats
this.parent = parent
this.type = 'file'
this.id = generateId()
// if (vFileMap.get(this.path) !== undefined) {
// throw `${this.path} is already exist`
// }
// vFileMap.set(this.path, this)
}
name: string
type: 'file'
size: number
stats: stats
parent: VirtualDirectory
id: number
storage: { [key: string]: any } = {}
get path() {
return `${this.parent.path}${this.name}`
}
get url() {
return `${config.api.get}${this.parent.path}${this.name}`
}
// destroy() {
// vFileMap.delete(this.path)
// }
}
class VirtualDirectory {
/**
* 除非为根节点,否则必须设置parent
*/
constructor(info: directory, parent?: VirtualDirectory) {
this.name = info.name
this.stats = info.stats
this.parent = parent ?? undefined
this.id = generateId()
// if (vDirectoryMap.get(this.path) !== undefined) {
// throw `${this.path} is already exist`
// }
// vDirectoryMap.set(this.path, this)
}
// 文件夹名
name: string
// 子文件
files: VirtualFile[] = []
// 子目录
directories: VirtualDirectory[] = []
// 没啥意义,标注一下就是了
type: 'directory' = 'directory'
// 文件夹创建时间等杂项属性
stats: stats
// 父文件夹
parent?: VirtualDirectory
_isUpdated: boolean = false
id: number
// 用来存储一些标记信息啥的
storage: { [key: string]: any } = {}
/**
* 获取相对于根目录的路径的层数
* 用于目录下文件定位
* @returns {string}
*/
get layer(): number {
return this.parent ? this.parent.layer + 1 : 0
}
/**
* 相对于根目录的路径, 如: "/", "/123/321/"
* 用于目录下文件定位
* @returns {string}
*/
get path(): string {
if (!this.parent) {
return '/'
}
return `${this.parent.path}${this.name}/`
}
/**
* 获取从根目录开始的按顺序的文件夹
* @returns {VirtualDirectory[]}
*/
get getParents(): VirtualDirectory[] {
if (!this.parent) {
return [this]
}
return [...this.parent.getParents, this]
}
/**
* 获取当前文件夹是否已经请求过至少一次数据
*/
get isUpdated() {
return this._isUpdated
}
/**
* 获取 layer 级父文件夹, 如getParent(1)就返回父级
* @param {number} layer 需要跳转的层数
* @returns {VirtualDirectory}
*/
getParent(layer: number): VirtualDirectory {
if (layer <= 0 || !this.parent) {
return this
}
return this.parent.getParent(layer - 1)
}
get tree(): Tree {
return {
name: this.name,
type: this.type,
path: this.path,
stats: this.stats,
files:
this.files?.map((v) => ({
name: v.name,
type: v.type,
size: v.size,
stats: v.stats,
url: v.url
})) ?? [],
directories: this.directories?.map((v) => v.tree) ?? []
}
}
// 时间复杂度大爆炸
updateContent(content: (file | directory)[]) {
this._isUpdated = true
const itemNames: string[] = content.map((v) => v.name)
// 删去远程端已不存在的文件与文件夹
const _delete = (target: (VirtualFile | VirtualDirectory)[]) => {
let index = 0
while (index < target.length) {
if (!itemNames.includes(target[index].name)) {
target.splice(index, 1)
} else {
index += 1
}
}
}
const _addFile = (item: file) => {
const nvf =
// vFileMap.get(`${this.path}${item.name}`) ??
new VirtualFile(item, this)
this.files.push(nvf)
}
const _addDirectory = (item: directory) => {
const nvd =
// vDirectoryMap.get(`${this.path}${item.name}/`) ??
new VirtualDirectory(item, this)
this.directories.push(nvd)
}
const newFileNames: string[] = difference(
content.filter((v) => v.type === 'file').map((v) => v.name),
this.files.map((v) => v.name)
)
const newDirectoryNames: string[] = difference(
content.filter((v) => v.type === 'directory').map((v) => v.name),
this.directories.map((v) => v.name)
)
_delete(this.files)
_delete(this.directories)
// 加入新的文件与文件夹
for (let item of content.filter((v) => newFileNames.includes(v.name))) {
_addFile(item as file)
}
for (let item of content.filter((v) =>
newDirectoryNames.includes(v.name)
)) {
_addDirectory(item as directory)
}
}
hasChild(item: VirtualDirectory | VirtualFile): boolean {
if (item.type === 'directory') {
return this.directories?.includes(item) ?? false
} else {
return this.files?.includes(item) ?? false
}
}
// destroy() {
// for(const item of [...this.files, ...this.directories]){
// item.destroy()
// }
// vDirectoryMap.delete(this.path)
// }
/**
* 调用后重新向远端拉取文件夹信息并进行更新
* @returns {void}
*/
async update(): Promise<void> {
const data: (file | directory)[] = await miaoFetchApi.get(this)
this.updateContent(data)
}
}
export default VirtualDirectory
-186
View File
@@ -1,186 +0,0 @@
import { Component, reactive, shallowRef } from 'vue'
import VirtualDirectory, { VirtualFile } from './VirtualDirectory'
import generateId from '@/hooks/generateId'
export interface VirtualPageOption {
exitConfirm?: boolean
allowCopy?: boolean
}
export interface ViewsPushOption {
index?: number
VirtualPageOption?: VirtualPageOption
}
const getRandomWebSafeColor = (() => {
const webSafeColors = [
'#009900',
'#999900',
'#009999',
'#990099',
'#0000ee'
]
let counter = 0
return () => {
const color = webSafeColors[counter % webSafeColors.length]
counter += 1
return color
}
})()
export default class VirtualPage {
constructor(
component: Component,
initObjects: {
name: string
directories?: VirtualDirectory[]
files?: VirtualFile[]
option?: VirtualPageOption
}
) {
this.name = initObjects?.name
this.component = shallowRef(component)
this.currentDirectories = initObjects?.directories ?? []
this.currentFiles = initObjects?.files ?? []
this.visible = true
this.id = generateId()
this.refreshKey = Math.random()
this.color = getRandomWebSafeColor()
this.exitConfirm = initObjects?.option?.exitConfirm ?? false
this.allowCopy = initObjects?.option?.allowCopy ?? true
}
// 页面使用的组件
component: Component
// 页面名
name: string
// 是否可见
visible: boolean
// 当前正在使用的文件夹
currentDirectories: VirtualDirectory[]
// 当前正在使用的文件
currentFiles: VirtualFile[]
// 区分每一个页面的唯一id
id: number
// 当refreshKey改变时需要刷新页面,目前还没接入刷新
refreshKey: number
// 为了区分,每个虚拟页面都会分配一个color,这个color会体现在tab上
color: string
// 用于展示的title
_title?: string
// 关闭时是否需要确认(未实现)
exitConfirm: boolean
// 是否允许复制页面
allowCopy: boolean
get title() {
if (this._title) {
return this._title
}
return (
this?.currentDirectories[0]?.name ??
this?.currentFiles[0]?.name ??
'未命名标签'
)
}
setTitle(title: string) {
this._title = title
}
// 切换显示状态
switchShow() {
this.visible = !this.visible
}
// 刷新,不过好像没有用得到的时候吧
// 事实上也不应该用得到,因为更新应该是响应式的
refresh() {
this.refreshKey = Math.random()
}
}
export class VirtualPages {
constructor() {
this._views = reactive<VirtualPage[]>([])
}
_views: VirtualPage[]
get length() {
return this._views.length
}
/**
* 查询一个页面位于_views中的次序
* @param {any} id 要查询的页面的id
* @returns {any}
*/
getIndex(id: number) {
for (let i in this._views) {
if (this._views[i].id === id) {
return parseInt(i)
}
}
return -1
}
/**
* 根据id得到view
* @param {any} id 要查询的页面的id
*/
getView(id: number) {
for (let v of this._views) {
if (v.id === id) {
return v
}
}
return undefined
}
/**
* 推入一个新的页面
*/
push(
component: Component,
name: string,
currentDirectories?: VirtualDirectory[],
currentFiles?: VirtualFile[],
option?: ViewsPushOption
) {
const { index = this.length, VirtualPageOption } = option ?? {}
this._views.splice(
index,
0,
reactive<VirtualPage>(
new VirtualPage(component, {
name,
directories: [...(currentDirectories ?? [])],
files: [...(currentFiles ?? [])],
option: VirtualPageOption
})
)
)
}
/**
* 根据次序删除一个页面
*/
deleteView(index: number) {
this._views.splice(index, 1)
}
/**
* 返回第一个使用此组件的页面
*/
find(component: Component) : VirtualPage[]
/**
* 通过页面名查找
*/
find(name: string) : VirtualPage[]
find(input: unknown) {
// console.log(this)
if (typeof input === 'string') {
return this._views.filter(v => v.component.name === input);
} else {
return this._views.filter(v => v.component === input);
}
}
}
-144
View File
@@ -1,144 +0,0 @@
<template>
<div class="miao-message-provider" ref="rootRef">
<slot></slot>
<transition-group name="message-fade" tag="div" class="miao-message-provider-container">
<div class="miao-message" v-for="msg in messageList" :key="msg.id"
:style="{ maxWidth: maxMessageWidth + 'px' }">
<n-icon class="miao-message-item" size="18" :color="msg.iconColor">
<component :is="msg.icon"></component>
</n-icon>
<div class="miao-message-item miao-message-item-content">{{ msg.content }}</div>
</div>
</transition-group>
</div>
</template>
<script setup lang="ts">
import generateId from '@/hooks/generateId';
import { NIcon } from 'naive-ui';
import { computed, onMounted, ref, shallowReactive } from 'vue';
import { InformationCircle, CheckmarkCircle, CloseCircle } from '@vicons/ionicons5'
import { AlertTipMessageOptions } from '@/types/type';
interface Message {
icon: any,
timer: any,
iconColor: string,
content: string,
}
const iconMap = {
'info': { icon: InformationCircle, color: 'blue' },
'success': { icon: CheckmarkCircle, color: 'green' },
'error': { icon: CloseCircle, color: 'red' },
}
const messageMap = shallowReactive<Record<string, Message | undefined>>({})
const rootRef = ref<HTMLDivElement>()
const maxMessageWidth = ref<number>(-1)
const messageList = computed(() => {
return Object.keys(messageMap).map(v => ({
id: v,
...messageMap[v],
}))
})
const alertTip = (content: string, options?: AlertTipMessageOptions) => {
let timer: any
const { timeout = 0, type = 'info', id = generateId() } = options ?? {}
if (timeout > 0) {
messageMap[id]?.timer && clearTimeout(messageMap[id]?.timer)
timer = setTimeout(() => {
delete messageMap[id]
}, timeout)
}
messageMap[id] = {
icon: iconMap[type].icon,
iconColor: iconMap[type].color,
content,
timer
}
return (newContent: string, newOptions?: AlertTipMessageOptions) => {
newOptions = newOptions ?? {}
newOptions.id = id
alertTip(newContent, newOptions)
}
}
onMounted(async () => {
// const set = message('test', { timeout: 5000 })
// setTimeout(() => {
// set('miao', { type: 'success' })
// }, 1000);
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
maxMessageWidth.value = entry.contentRect.width * 0.8;
}
});
if (rootRef.value) {
observer.observe(rootRef.value);
}
})
defineExpose({ alertTip });
</script>
<style scoped lang="scss">
.miao-message-provider {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
.miao-message-provider-container {
position: absolute;
width: 0;
height: 0;
left: 50%;
top: 30px;
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
z-index: 70582140539943419;
.miao-message {
display: flex;
align-items: center;
width: max-content;
background-color: rgb(249, 249, 249);
padding: 5px 10px;
background-color: rgb(255 255 255);
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05);
border-radius: 5px;
user-select: none;
margin-top: 10px;
.miao-message-item {
margin: 0 5px;
}
.miao-message-item-content {
white-space: pre-wrap;
}
}
}
.message-fade-move,
.message-fade-enter-active,
.message-fade-leave-active {
transition: all 0.5s ease;
}
.message-fade-enter-from,
.message-fade-leave-to {
opacity: 0;
transform: translateX(30px);
}
.message-fade-leave-active {
position: absolute;
}
}
</style>
-88
View File
@@ -1,88 +0,0 @@
<!-- 基于naiveui的下拉菜单实现的自定义下拉菜单 -->
<!-- 支持长按和右键呼出下拉菜单 -->
<template>
<div class="main" @contextmenu="handleContextMenu" @mouseleave="handleMainMouseLeave">
<slot></slot>
</div>
<n-dropdown placement="bottom-start" trigger="manual" :x="x" :y="y" :options="props.options"
:show="showContextMenu > 0" :on-clickoutside="onClickoutside" @select="handleSelect" @mouseleave="
showContextMenu -= 1" @mouseenter="showContextMenu += 1" />
</template>
<script setup lang="ts">
import { NDropdown } from 'naive-ui/es/dropdown';
import { DropdownOption } from 'naive-ui/es/dropdown';
import { nextTick, ref } from 'vue';
const props = defineProps<{
options: DropdownOption[]
// 长按呼出右键菜单的时延,undefined时不生效
touchTimeOut?: number
}>()
const emit = defineEmits<{
select: [key: string]
}>()
const x = ref(0)
const y = ref(0)
const showContextMenu = ref(0)
// 长按计时器
// let timer_touch: string | number | NodeJS.Timeout | undefined
const onClickoutside = async () => {
showContextMenu.value = 0
}
const handleMainMouseLeave = async () => {
setTimeout(() => {
showContextMenu.value -= 1
}, 20)
// nextTick(()=>{
// showContextMenu.value -= 1
// })
}
const handleSelect = async (key: string) => {
emit('select', key)
showContextMenu.value = 0
}
const handleContextMenu = (e: MouseEvent) => {
e.preventDefault()
showContextMenu.value = 0
nextTick().then(() => {
showContextMenu.value += 1
x.value = e.clientX
y.value = e.clientY
})
}
// const handleTouchStart = (e: TouchEvent) => {
// if(props.touchTimeOut === undefined) {
// return
// }
// timer_touch ?? clearTimeout(timer_touch)
// timer_touch = setTimeout(() => {
// showContextMenu.value = 1
// x.value = e.targetTouches[0].clientX
// y.value = e.targetTouches[0].clientY
// }, props.touchTimeOut)
// }
// const handleTouchEnd = () => {
// if(timer_touch) {
// clearTimeout(timer_touch)
// timer_touch = undefined
// }
// }
</script>
<style>
.main {
width: 100%;
height: 100%;
position: relative;
}
</style>
-34
View File
@@ -1,34 +0,0 @@
<template>
<div ref="divRef" @dragstart="onDs" :draggable="isDraggable">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
// import Sortable from 'sortablejs'
const props = defineProps<{
isDraggable: boolean
}>()
props
const emit = defineEmits<{
onDragStart: [e: DragEvent]
}>()
const divRef = ref<HTMLDivElement>()
const onDs = (e: DragEvent) => {
emit('onDragStart', e)
}
onMounted(() => {
divRef?.value?.addEventListener('dragstart', function (ev) {
var img = new Image();
img.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath /%3E%3C/svg%3E";
ev?.dataTransfer?.setDragImage(img, 0, 0);
})
})
</script>
<style scoped></style>
-131
View File
@@ -1,131 +0,0 @@
<template>
<div
class="container"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop">
<slot></slot>
<miao-mask v-model:show="showMask"></miao-mask>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import miaoMask from './miaoMask.vue'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import config from '@/config'
import useDataBus from '@/hooks/useDataBus'
const props = defineProps({
disable: {
type: Boolean,
default: false
}
})
const dataBus = useDataBus()
const { uploadSizeLimit } = config
const emit = defineEmits<{
onFiles: [files: File[]]
onVirtualFiles: [vFiles: VirtualFile[]]
onVirtualDirectory: [vDirectory: VirtualDirectory[]]
}>()
const showMask = ref<number>(0)
// File類型的數據可能爲文件夾,通過FileReader進行判斷
const isFile = (file: File) =>
new Promise((resolve) => {
const reader = new FileReader()
reader.onload = () => {
resolve(true)
reader.abort()
}
reader.onerror = () => {
resolve(false)
reader.abort()
}
reader.readAsText(file)
})
/**
* 只保留文件,移去文件夹
* @param {File[]} files
*/
const removeDirectory = async (files: File[]) => {
let index = 0
while (index < files.length) {
const currentFile = files[index]
const _isFile = await isFile(currentFile)
if (_isFile) {
index += 1
continue
} else {
files.splice(index, 1)
continue
}
}
}
const check = (event: DragEvent) => {
return (
[
...(event.dataTransfer?.types?.filter(v=>v === 'Files') ?? []),
...(dataBus.get('dragData_vFiles') ?? []),
...(dataBus.get('dragData_vDirectory') ?? [])
].length !== 0
)
}
const handleDragOver = async (event: DragEvent) => {
if (props.disable || check(event) === false) {
return
}
event.preventDefault()
if (showMask.value === 1) {
return
}
showMask.value = 1
}
const handleDragLeave = async () => {
showMask.value = 0
}
const handleDrop = async (event: DragEvent) => {
if (props.disable || check(event) === false) {
return
}
showMask.value = 0
event.preventDefault()
event.stopPropagation()
// todo: uploadSize的限制不應該在這裏
const files = [...(event.dataTransfer?.files ?? [])].filter(
(file) => file.size < uploadSizeLimit
)
await removeDirectory(files)
const virtualFiles: VirtualFile[] = dataBus.pop('dragData_vFiles')
const virtualDirectory: VirtualDirectory[] = dataBus.pop(
'dragData_vDirectory'
)
// console.log({ virtualDirectory, virtualFiles })
if (files && files.length) {
emit('onFiles', files)
}
if (virtualFiles && virtualFiles.length) {
emit('onVirtualFiles', virtualFiles)
}
if (virtualDirectory && virtualDirectory.length) {
emit('onVirtualDirectory', virtualDirectory)
}
}
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
position: relative;
}
</style>
-118
View File
@@ -1,118 +0,0 @@
<!-- 只适合用于渲染过程较为复杂的组件,不然可能得不偿失
这么泛用的组件,性能差点也能理解 -->
<template>
<div class="miao-lazy" ref="miaoLazyRoot"
:style="!show ? { minHeight: _autoHeight !== undefined ? _autoHeight : props.minHeight, minWidth, margin } : {}">
<slot v-if="show" />
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
const props = defineProps({
// disable: {
// type: Boolean,
// default: false,
// },
disableLazy: {
type: Boolean,
default: false,
},
// 仅在内容未加载时生效,用于占位
minHeight: {
type: String,
default: 'none',
},
minWidth: {
type: String,
default: 'none',
},
margin: {
type: String,
default: 'none',
},
autoHeight: {
type: Boolean,
default: false
}
})
const miaoLazyRoot = ref<HTMLDivElement>()
const _autoHeight = ref<string>()
const show = ref<boolean>(props.disableLazy)
const isVisible = (dom: HTMLDivElement) => {
const offset = dom.getBoundingClientRect().top
return offset > 0 && offset < 2000
}
const { showSlot, hideSlot, clearTimer } = (() => {
let timer: any
let timerInterval: any
const clearTimer = () => {
timer && clearTimeout(timer)
timerInterval && clearInterval(timerInterval)
timer = undefined
timerInterval = undefined
}
const hideSlot = () => {
clearTimer()
if (props.autoHeight) {
_autoHeight.value = `${miaoLazyRoot.value?.clientHeight}px`
}
show.value = false
}
const showSlot = () => {
if (show.value === true) {
return
}
clearTimer()
// timer = setTimeout(() => {
if (!miaoLazyRoot.value || isVisible(miaoLazyRoot.value) === false) {
return
}
show.value = true
timerInterval = setInterval(() => {
if (show.value === false || miaoLazyRoot.value === undefined) {
return
}
if (isVisible(miaoLazyRoot.value) === false) {
hideSlot()
}
}, 1000)
// if (props.autoHeight) {
// nextTick(() => {
// console.log(miaoLazyRoot.value?.clientHeight)
// _autoHeight.value = `${miaoLazyRoot.value?.clientHeight}px`
// })
// }
}
return { showSlot, hideSlot, clearTimer }
})()
onMounted(() => {
if (show.value === false) {
let options = {
threshold: 0
};
let observer: IntersectionObserver | undefined = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
showSlot()
// miaoLazyRoot.value && observer?.unobserve(miaoLazyRoot.value)
// observer = undefined
} else {
hideSlot()
}
}, options);
miaoLazyRoot.value && observer.observe(miaoLazyRoot.value);
}
})
onBeforeUnmount(() => {
clearTimer()
})
</script>
-55
View File
@@ -1,55 +0,0 @@
<template>
<transition name="transition">
<div class="mask-body" ref="maskBodyRef" v-show="showMask">
<slot></slot>
</div>
</transition>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const maskBodyRef = ref<HTMLDivElement>()
const showMask = defineModel<boolean | number>('show', { default: false, required: true })
</script>
<style lang="scss" scoped>
.mask-body {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
z-index: 7058214053994319;
// &-container {
// }
&-show {
display: flex;
transition: opacity 0.3s 0.01s;
opacity: 1;
}
&-hide {
opacity: 0;
transition: display 0.01s 0.3s;
display: none;
}
}
.transition-enter-active,
.transition-leave-active {
transition: opacity 0.3s ease;
}
.transition-enter-from,
.transition-leave-to {
opacity: 0;
}
</style>
-102
View File
@@ -1,102 +0,0 @@
<template>
<miao-mask v-model:show="showMask">
<n-card class="popup-card" :title="popupConfig?.title" size="medium">
{{ popupConfig?.content }}
<n-input
class="popup-input"
v-model:value="inputValue"
type="text"
v-bind="popupConfig?.inputProps"
passively-activated
autofocus
v-if="popupConfig?.inputProps" />
<div class="popup-button-container">
<n-button
class="popup-button-container-button"
v-for="option in popupConfig?.options"
:key="option.label"
:type="option.type"
@click="_resolve && _resolve(option.key)">
{{ option.label }}
</n-button>
</div>
</n-card>
</miao-mask>
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue'
import { NCard, NInput, NButton } from 'naive-ui'
import miaoMask from './miaoMask.vue'
import { InputProps } from 'naive-ui'
const showMask = ref(false)
const inputValue = ref<string>('')
const _resolve = ref()
type popupConfigType = {
title: string
content?: string
inputValue?: string
inputProps?: InputProps
options: {
label: string
key: unknown
type:
| 'default'
| 'tertiary'
| 'primary'
| 'success'
| 'info'
| 'warning'
| 'error'
}[]
}
const popupConfig = ref<popupConfigType>()
const popup = async (config: popupConfigType) => {
popupConfig.value = config
inputValue.value = config.inputValue ?? ''
showMask.value = true
const key = await new Promise((resolve: (key: unknown) => unknown) => {
_resolve.value = resolve
})
const text = inputValue.value
showMask.value = false
nextTick(() => {
inputValue.value = ''
_resolve.value = undefined
})
return {
text,
key
}
}
defineExpose({ popup })
</script>
<style scoped lang="scss">
.popup-card {
width: 80%;
.popup-input {
margin-bottom: 10px;
}
.popup-button-container {
height: 100%;
width: 100%;
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
.popup-button-container-button {
margin-top: 3px;
&:not(:last-child) {
margin-right: 5px;
}
}
}
}
</style>
-16
View File
@@ -1,16 +0,0 @@
const baseUrl = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
export default {
baseUrl,
uploadSizeLimit: 1024 * 1024 * 100,
api: {
get: '/get',
upload: '/upload',
delete: '/delete',
query: '/query',
rename: '/rename',
mkdir: '/mkdir',
cut: '/cut'
},
enableUpload: true
}
-15
View File
@@ -1,15 +0,0 @@
import VirtualDirectory, { Tree, VirtualFile } from "@/class/VirtualDirectory";
/**
* 通过VirtualDirectory的.tree重建文件夹树
* @param {any} data:Tree
* @returns {any}
*/
const buildFromTree = (data: Tree) => {
const root = new VirtualDirectory(data)
root.files = data.files?.map(v=>new VirtualFile(v, root))
root.directories = data.directories?.map(v=>buildFromTree(v))
return root
}
export default buildFromTree;
-20
View File
@@ -1,20 +0,0 @@
export default function dateFormatter(date: Date, fmt: string) {
var o:any = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"h+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
}
}
return fmt;
}
-109
View File
@@ -1,109 +0,0 @@
import config from "@/config";
const { uploadSizeLimit } = config;
export type dropHandlerHooks = {
/**
* 仅返回小于config.ts中大小限制的文件,不会返回文件夹
*/
onDropFiles?: (files: File[]) => void;
/**
* 当拖动时
*/
onOver?: () => void;
/**
* 当进入时
*/
onEnter?: () => void;
/**
* 当离开时
*/
onLeave?: () => void;
/**
* 离开或是放下时都会触发
* @returns {any}
*/
onEnd?: () => void;
};
const isFile = (file: File) =>
new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => {
resolve(true);
reader.abort();
};
reader.onerror = () => {
resolve(false);
reader.abort();
};
reader.readAsText(file);
});
/**
* 只保留文件,移去文件夹
* @param {File[]} files
*/
const filesFilter = async (files: File[]) => {
let index = 0;
while (index < files.length) {
const currentFile = files[index];
const _isFile = await isFile(currentFile);
if (_isFile) {
index += 1;
continue;
} else {
files.splice(index, 1);
continue;
}
}
};
export default function dropHandler(dom: HTMLDivElement, hooks?: dropHandlerHooks) {
dom.addEventListener(
"dragenter",
function (e) {
e.preventDefault();
e.stopPropagation();
hooks?.onEnter && hooks?.onEnter();
},
false
);
dom.addEventListener(
"dragover",
function (e) {
e.preventDefault();
e.stopPropagation();
hooks?.onOver && hooks?.onOver();
},
false
);
dom.addEventListener(
"dragleave",
function (e) {
e.preventDefault();
e.stopPropagation();
hooks?.onLeave && hooks?.onLeave();
hooks?.onEnd && hooks.onEnd();
},
false
);
dom.addEventListener(
"drop",
async function (e) {
// console.log(e, e.dataTransfer?.types);
e.preventDefault();
e.stopPropagation();
const files = [...(e.dataTransfer?.files ?? [])].filter(file => file.size < uploadSizeLimit);
await filesFilter(files);
hooks?.onDropFiles && hooks.onDropFiles(files);
hooks?.onEnd && hooks.onEnd();
},
false
);
}
-16
View File
@@ -1,16 +0,0 @@
const idList: number[] = []
/**
* 生成一个唯一的id
* @param {any} length 生成id的长度
* @returns {number}
*/
export default function generateId(length?: number) {
length || (length = 16)
let id
while (!id || idList.includes(id)) {
id = Math.ceil(Math.random() * 10 ** 16)
}
idList.push(id)
return id
}
-6
View File
@@ -1,6 +0,0 @@
import { alertTipType } from "@/types/type";
import initPlugin from "./init/initPlugin";
export default async function init(globalAlertTip: alertTipType) {
await initPlugin(globalAlertTip)
}
-24
View File
@@ -1,24 +0,0 @@
import usePluginCenter, { initPluginCenter } from "@/hooks/usePluginCenter";
import { Plugin, registerComponentOption } from "@/class/PluginCenter";
import { alertTipType, PluginExportType } from "@/types/type";
const initPlugin = async (globalAlertTip: alertTipType) => {
initPluginCenter(globalAlertTip);
const modulesFiles = import.meta.glob("@/plugins/**/export*.ts", { eager: true });
const pluginCenter = usePluginCenter();
for (let pluginConfig of Object.values(modulesFiles)) {
const _config = pluginConfig as {
default: registerComponentOption | Plugin;
type: PluginExportType;
key?: string;
};
const { type, key } = _config;
if (type === PluginExportType.component) {
pluginCenter.registerComponent(_config.default as registerComponentOption);
} else if (type === PluginExportType.default) {
pluginCenter.register(key as string, _config.default as Plugin);
}
}
};
export default initPlugin;
-64
View File
@@ -1,64 +0,0 @@
import { NIcon } from 'naive-ui'
import { h } from 'vue'
export const filePicker = (() => {
let _resolve: ((files: File[]) => void) | undefined
return (option?: { accept?: 'string'; multiple: boolean }) => {
if (_resolve) {
_resolve([])
_resolve = undefined
}
return new Promise<File[]>((resolve) => {
const inpEle = document.createElement('input')
inpEle.id = `__file_${Math.ceil(Math.random() * 100000000)}`
inpEle.type = 'file'
inpEle.style.display = 'none'
_resolve = resolve
// 文件类型限制
// inpEle.accept="image/*"
option?.accept && (inpEle.accept = option?.accept)
// 多选限制
option?.multiple && (inpEle.multiple = option?.multiple)
// inpEle.addEventListener("change", event => fn.call(inpEle, event, inpEle.files), { once: true });
inpEle.addEventListener(
'change',
() => {
if (!inpEle.files) {
return
}
const files: File[] = []
let i = 0
while (i < inpEle.files?.length) {
const _file = inpEle?.files?.item(i)
if (_file !== null) {
files.push(_file)
}
i += 1
}
resolve(files)
_resolve = undefined
},
{ once: true }
)
inpEle.click()
})
}
})()
export const renderIcon = (icon: any) => {
return () => {
return h(NIcon, null, {
default: () => h(icon)
})
}
}
export const sleep = async (timeout: number) => {
await new Promise(_=>setTimeout(_, timeout))
}
export default {
filePicker,
renderIcon,
sleep
}
-26
View File
@@ -1,26 +0,0 @@
import { reactive } from "vue";
let dataStorage: {
[key: string | number] : any
};
export default function useDataBus() {
if(dataStorage === undefined) {
dataStorage = reactive({})
}
return {
set(key: string | number, value: any) {
dataStorage[key] = value;
return value;
},
pop(key: string | number) {
const value = dataStorage[key];
dataStorage[key] = undefined;
return value;
},
get(key: string | number) {
const value = dataStorage[key];
return value;
}
};
}
-274
View File
@@ -1,274 +0,0 @@
import VirtualDirectory, { VirtualFile } from "@/class/VirtualDirectory";
import {
getOption,
getResponse,
miaoFetchConfig,
deleteOption,
uploadOption,
queryOption,
renameOption,
cutOption,
mkdirOption,
getFileOption
} from "@/types/fetch";
import Config from "@/config";
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
export const useMiaoFetch = (option?: miaoFetchConfig) => {
const retry = option?.retry ?? 0;
const controller = new AbortController();
let flag = true;
return {
miaoFetch: async (config: AxiosRequestConfig<any>): Promise<AxiosResponse<any, any>> => {
let counter = 0;
let response: any;
let errList = [];
while (flag && counter <= retry) {
try {
response = await axios({
...config,
signal: controller.signal
});
return response;
} catch (_e) {
errList.push(_e);
counter += 1;
}
}
throw errList;
},
/**
* 强制终止miaoFetch
*/
abortMiaoFetch: () => {
flag = false;
controller.abort();
}
};
};
const miaoFetchApi = {
/**
* 获取对应文件夹下的数据
*/
async get(vDirectory: VirtualDirectory, option?: getOption): Promise<getResponse> {
const { baseUrl, api } = Config;
const url = `${baseUrl}${api.get}${vDirectory.path}`;
const retry = option?.retry ?? 0;
const { miaoFetch } = useMiaoFetch({
retry
});
const response = await miaoFetch({
url
});
return response.data;
},
/**
* 获取对应文件下的数据
*/
getFile(vFile: VirtualFile, option?: getFileOption) {
// const { baseUrl, api } = Config
const url = vFile.url;
const retry = option?.retry ?? 0;
const axiosOption = option?.axiosOption ?? {};
const { miaoFetch, abortMiaoFetch: abort } = useMiaoFetch({
retry
});
const response = miaoFetch({
url,
responseType: "arraybuffer",
onDownloadProgress(progressEvent) {
option && option.onProcess && option.onProcess(progressEvent.total ? progressEvent.loaded / progressEvent.total : -1);
},
...axiosOption
});
return {
response: new Promise<Uint8Array>(async resolve => {
resolve(new Uint8Array((await response).data));
}),
abort
};
},
/**
* 上传单个文件到对应文件夹, 后续要将data类型改成buffer以支持分片上传
* 正常上传文件请使用uploadQueue
*/
upload(data: File, targetVDir: VirtualDirectory, option?: uploadOption) {
const { baseUrl, api } = Config;
const url = `${baseUrl}${api.upload}${targetVDir.path}`;
const retry = option?.retry ?? 0;
const { miaoFetch, abortMiaoFetch } = useMiaoFetch({
retry
});
const sizeTotal = data.size;
const opType = option?.type ?? "write";
const formData = new FormData();
formData.append("file", data);
// 必须这样传,不然中文乱码
formData.append("fileName", data.name);
formData.append("operateType", opType);
return {
abort: () => abortMiaoFetch(),
response: new Promise<{
message: "success" | "failed";
error?: string;
}>(async resolve => {
const response = await miaoFetch({
url,
method: "post",
data: formData,
onUploadProgress(progressEvent) {
option && option.onProcess && option.onProcess(progressEvent.loaded / sizeTotal);
}
});
resolve(response.data);
})
};
},
/**
* 在目标文件夹下创建新的文件夹
*/
mkdir(targetVDir: VirtualDirectory, newFolderName: string, option?: mkdirOption) {
const { baseUrl, api } = Config;
const url = `${baseUrl}${api.mkdir}${targetVDir.path}`;
const { miaoFetch } = useMiaoFetch({
retry: option?.retry ?? 0
});
return new Promise<{
message: "success" | "failed";
error?: string;
}>(async resolve => {
const response = await miaoFetch({
url,
method: "post",
data: {
folderName: newFolderName
}
});
resolve(response.data);
});
},
/**
* 在目标文件夹下创建新的空文件,实际上是通过upload空文件实现
*/
mkFile(targetVDir: VirtualDirectory, newFileName: string, option?: uploadOption) {
const emptyBlob = new Blob([]);
const emptyFile = new File([emptyBlob], newFileName, {
type: "text/plain"
});
return this.upload(emptyFile, targetVDir, option);
},
/**
* 删除对应的文件或文件夹,若为文件夹会递归删除
*/
delete(target: VirtualFile | VirtualDirectory, option?: deleteOption) {
const { baseUrl, api } = Config;
const url = `${baseUrl}${api.delete}${target.path}`;
const retry = option?.retry ?? 0;
const { miaoFetch } = useMiaoFetch({
retry
});
return {
response: new Promise<{
eventId: number;
}>(async resolve => {
const response = await miaoFetch({
url,
method: "post"
});
resolve(response.data);
})
};
},
/**
* 重命名对应的文件或文件夹
*/
rename(target: VirtualFile | VirtualDirectory, newName: string, option?: renameOption) {
const { baseUrl, api } = Config;
const url = `${baseUrl}${api.rename}${target.path}`;
const retry = option?.retry ?? 0;
const { miaoFetch } = useMiaoFetch({
retry
});
return {
response: new Promise<{
eventId: number;
}>(async resolve => {
const response = await miaoFetch({
url,
method: "post",
data: { newName }
});
resolve(response.data);
})
};
},
/**
* 剪切文件或文件夹
*/
cut(target: VirtualFile | VirtualDirectory, to: VirtualDirectory, option?: cutOption) {
const newPath = to.path;
const { baseUrl, api } = Config;
const url = `${baseUrl}${api.cut}${target.path}`;
const retry = option?.retry ?? 0;
const { miaoFetch } = useMiaoFetch({
retry
});
return {
response: new Promise<{
eventId: number;
}>(async resolve => {
const response = await miaoFetch({
url,
method: "post",
data: { newPath }
});
resolve(response.data);
})
};
},
/**
* 查询服务端操作事件的进展
*/
query(eventId: number, option?: queryOption) {
const { baseUrl, api } = Config;
const url = `${baseUrl}${api.query}`;
const retry = option?.retry ?? 0;
const { miaoFetch } = useMiaoFetch({
retry
});
return {
response: new Promise<{
status: "process" | "success" | "failed";
error?: unknown;
}>(async resolve => {
const response = await miaoFetch({
url,
method: "get",
params: {
id: eventId
}
});
resolve(response.data);
})
};
}
};
export default function useMiaoFetchApi() {
return miaoFetchApi;
}
-15
View File
@@ -1,15 +0,0 @@
import PluginCenter from "@/class/PluginCenter";
import { alertTipType } from "@/types/type";
import { reactive } from "vue";
let pluginCenter: PluginCenter
export const initPluginCenter = (globalAlertTip: alertTipType) => {
if(!pluginCenter) {
pluginCenter = reactive(new PluginCenter(globalAlertTip))
}
}
export default function usePluginCenter(){
return pluginCenter
}
-17
View File
@@ -1,17 +0,0 @@
import VirtualDirectory from "@/class/VirtualDirectory";
import { reactive } from "vue";
const root = reactive(
new VirtualDirectory({
name: '根目录',
stats: {
atimeMs: 0,
birthtimeMs: 0,
ctimeMs: 0,
mtimeMs: 0
}
})
)
export default function useRootVDirectory() {
return root;
}
-11
View File
@@ -1,11 +0,0 @@
import UploadQueue from "@/class/UploadQueue";
import { reactive } from "vue";
let uploadQueue: UploadQueue
export default () => {
if(uploadQueue === undefined){
uploadQueue = reactive(new UploadQueue())
}
return uploadQueue
}
-12
View File
@@ -1,12 +0,0 @@
import { VirtualPages } from "@/class/VirtualPage"
import { Reactive, reactive } from "vue"
let views: Reactive<VirtualPages>
export default function useVirtualPages(){
if(views === undefined) {
// views = reactive<VirtualPage[]>([])
views = reactive(new VirtualPages())
}
return views
}
-5
View File
@@ -1,5 +0,0 @@
import { createApp } from 'vue'
import App from './App.vue'
import 'drag-drop-touch'
createApp(App).mount('#app')
-16
View File
@@ -1,16 +0,0 @@
import { VirtualFile } from "@/class/VirtualDirectory"
import config from "@/config"
import { usePluginHooksType } from "@/types/type"
import copy2Clipboard from 'copy-to-clipboard'
const copy = (__: any, vFiles: VirtualFile[], hooks: usePluginHooksType) => {
const { globalAlertTip } = hooks
const vFile = vFiles[0]
console.log(vFile.url)
copy2Clipboard(`${config.baseUrl}${vFile.url}`)
globalAlertTip(`${vFile.name}的链接已复制至剪切板`, {
type: 'info',
timeout: 2000
})
}
export default copy
@@ -1,29 +0,0 @@
import { Plugin, PluginGroup } from "@/class/PluginCenter";
import Copy from "./copy";
import { CopyOutline } from "@vicons/ionicons5";
import { PluginExportType } from "@/types/type";
import { shallowRef } from "vue";
const pluginConfig: Plugin = {
name: "复制链接",
icon: shallowRef(CopyOutline),
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false;
}
if (vFiles.length !== 1) {
return false;
}
return true;
},
disable: false,
group: [PluginGroup.default],
func: Copy,
priority: -1
};
export default pluginConfig;
export const type = PluginExportType.default;
export const key = 'miaoCopy';
-23
View File
@@ -1,23 +0,0 @@
import { Plugin, PluginGroup } from "@/class/PluginCenter";
import fullScreen from "./fullScreen";
import { ExpandOutline } from "@vicons/ionicons5";
import { PluginExportType } from "@/types/type";
import { shallowRef } from "vue";
const pluginConfig: Plugin = {
name: "全屏",
icon: shallowRef(ExpandOutline),
filter: () => {
return true;
},
disable: false,
group: [PluginGroup.mainMenu],
func: fullScreen,
priority: -1
};
export default pluginConfig;
export const type = PluginExportType.default;
export const key = "fullScreen";
-4
View File
@@ -1,4 +0,0 @@
const fullScreen = () => {
document.body.requestFullscreen()
}
export default fullScreen
-49
View File
@@ -1,49 +0,0 @@
import { registerComponentOption } from '@/class/PluginCenter'
import { CodeOutline } from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
const pluginConfig: registerComponentOption = {
key: 'miaoCodeMirror',
name: '代码编辑',
icon: CodeOutline,
getComponent: async () =>
(await import('@/plugins/miaoCodeMirror/miaoCodeMirror.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
for (let end of [
'js',
'ts',
'jsx',
'tsx',
'py',
'html',
'css',
'scss',
'go',
'json',
'md',
'yaml',
'txt',
'xml',
'vue',
'srt',
'vtt',
'ass',
'ssa'
]) {
if (vFiles[0].name.endsWith(end)) {
return true
}
}
return false
}
}
export default pluginConfig
export const type = PluginExportType.component
@@ -1,209 +0,0 @@
<template>
<miao-message-provider ref="miaoMessage">
<div class="codemirror-container" ref="rootRef" :class="`codemirror-container-${theme}`">
<div class="codemirror-container-top">
<div class="codemirror-container-btn codemirror-container-btn-save" @click="handleSave">
保存
</div>
<div class="codemirror-container-btn codemirror-container-btn-save" @click="handleReset">
恢复到上次保存
</div>
</div>
<div class="codemirror-container-main">
<NScrollbar style="max-height: calc(100%)">
<codemirror v-model="codeData" :extensions="extensions" />
</NScrollbar>
</div>
</div>
</miao-message-provider>
</template>
<script setup lang="ts">
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
import { VirtualFile } from '@/class/VirtualDirectory'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import { onMounted, ref } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { NScrollbar } from 'naive-ui'
const miaoFetchApi = useMiaoFetchApi()
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const decoder = new TextDecoder('utf-8')
const miaoMessage = ref<InstanceType<typeof miaoMessageProvider>>()
const vFile = ref<VirtualFile>()
const rootRef = ref<HTMLDivElement>()
// const _key = ref(Math.random())
const codeData = ref<string>()
// 第一次点击保存时自动备份data,暂时懒得做
const bakData = ref<string>()
const extensions = ref<any[]>([])
const theme = ref<'light' | 'dark'>('light')
const handleSave = async () => {
if (bakData.value === codeData.value || !vFile.value) {
miaoMessage.value?.alertTip('内容未改动', {
timeout: 3000,
type: 'info'
})
return
}
const parentDir = vFile.value?.parent
const file_bak = new File(
[new Blob([bakData.value ?? ''])],
`${vFile.value?.name}.bak`
)
const file = new File([new Blob([codeData.value ?? ''])], vFile.value.name)
bakData.value = codeData.value
const { response } = miaoFetchApi.upload(file, parentDir)
false && miaoFetchApi.upload(file_bak, parentDir)
await response
miaoMessage.value?.alertTip('保存成功', {
timeout: 5000,
type: 'success'
})
}
const handleReset = async () => {
codeData.value = bakData.value
}
// const switchTheme = async () => {
// if (!vFile.value) {
// return
// }
// theme.value = theme.value === 'dark' ? 'light' : 'dark'
// extensions.value = [
// ...(await getLangExtensions(vFile.value?.name)),
// // theme.value === 'dark' ?
// ]
// _key.value = Math.random()
// }
const getLangExtensions = async (fileName: string) => {
const ext = fileName.split('.').pop()?.toLocaleLowerCase()
switch (ext) {
case 'js':
case 'jsx':
case 'ts':
case 'tsx':
const { javascript } = await import('@codemirror/lang-javascript')
return [
javascript({
jsx: ext.substring(2, 3) === 'x',
typescript: ext.substring(0, 2) === 'js'
})
]
break
case 'css':
case 'scss':
const { sass } = await import('@codemirror/lang-sass')
return [sass()]
break
case 'html':
const { html } = await import('@codemirror/lang-html')
return [html()]
break
case 'py':
const { python } = await import('@codemirror/lang-python')
return [python()]
break
case 'go':
const { go } = await import('@codemirror/lang-go')
return [go()]
break
case 'json':
const { json } = await import('@codemirror/lang-json')
return [json()]
break
case 'md':
const { markdown } = await import('@codemirror/lang-markdown')
return [markdown()]
break
case 'yaml':
const { yaml } = await import('@codemirror/lang-yaml')
return [yaml()]
break
case 'vue':
const { vue } = await import('@codemirror/lang-vue')
return [vue()]
break
case 'xml':
const { xml } = await import('@codemirror/lang-xml')
return [xml()]
break
default:
return []
}
}
onMounted(async () => {
vFile.value = currentFiles.value[0]
const fileName = currentFiles.value[0].name
codeData.value = decoder.decode(
await (miaoFetchApi.getFile(currentFiles.value[0], {
axiosOption: {
headers: {
'Cache-Control': 'no-cache'
}
}
}).response)
)
bakData.value = codeData.value
extensions.value = [...(await getLangExtensions(fileName))]
rootRef.value?.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault()
handleSave()
}
})
})
</script>
<style scoped lang="scss">
.codemirror-container {
width: 100%;
height: 100%;
overflow-y: hidden;
.codemirror-container-top {
height: 25px;
display: flex;
border-bottom: 1px solid #000;
}
.codemirror-container-btn {
height: 25px;
padding: 0 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
user-select: none;
background-color: none;
transition: background-color 0.25s;
&:hover {
background-color: #e8e8e8;
}
}
.codemirror-container-main {
height: calc(100% - 25px);
position: relative;
}
}
.codemirror-container-dark {
background-color: rgb(30, 30, 30);
}
.codemirror-container-light {
background-color: rgb(255, 255, 255);
}
</style>
-25
View File
@@ -1,25 +0,0 @@
// @ts-nocheck
import { registerComponentOption } from '@/class/PluginCenter'
import { PluginExportType } from '@/types/type'
const pluginConfig: registerComponentOption = {
key: 'miaoCrypto',
name: '文件加密',
getComponent: async () =>
(await import('./miaoCrypto.vue')).default,
filter: (vDirs, vFiles) => {
return true
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('txt')
},
disable: true,
}
export default pluginConfig
export const type = PluginExportType.component
-19
View File
@@ -1,19 +0,0 @@
import aesJs from 'aes-js'
import aes_worker from '../workers/aes-worker?worker'
/**
* 使用worker进行aes加密
* @param {any} key:string 须保证长度正确
* @param {any} data:Uint8Array
* @returns {Uint8Array}
*/
export default (key: string, data: Uint8Array) => {
const key_u8i = aesJs.utils.utf8.toBytes(key)
return new Promise<Uint8Array>((resolve) => {
const aesWorker = new aes_worker()
aesWorker.onmessage = (event) => {
resolve(event.data)
}
aesWorker.postMessage({ data, key: key_u8i })
})
}
-196
View File
@@ -1,196 +0,0 @@
<template>
<miao-drop-handler @on-virtual-files="handleDropVirtualFiles">
<div class="miaoCrypto-container">
<div class="miaoCrypto-container-encrypt miaoCrypto-container-item">
<n-scrollbar style="max-height: 100%">
<div v-for="item of workItemList" :key="item.id" class="miaoCrypto-container-item-item">
<div class="miaoCrypto-container-item-percentBar" :style="{
zIndex: 0,
width: item.getBufferProgress !== -1 ? `${item.getBufferProgress * 100}%` : `0%`,
backgroundColor: 'blue'
}"></div>
<div class="miaoCrypto-container-item-percentBar" :style="{
zIndex: 1,
width: item.status === 'finish' ? `100%` : `0%`,
backgroundColor: 'green'
}"></div>
<span class="miaoCrypto-container-item-content">{{ item.name }}{{ item.status }}</span>
</div>
</n-scrollbar>
</div>
<!-- <div class="miaoCrypto-container-decrypt miaoCrypto-container-item"></div> -->
</div>
</miao-drop-handler>
</template>
<script setup lang="ts">
import { NScrollbar } from 'naive-ui'
import { onUnmounted, ref, shallowReactive } from 'vue'
import aes_ctr_worker from './hooks/AES'
import { VirtualFile } from '@/class/VirtualDirectory'
import miaoDropHandler from '@/components/miaoDropHandler.vue'
import { reactive } from 'vue'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import { onMounted } from 'vue'
import { computed } from 'vue'
const miaoFetchApi = useMiaoFetchApi()
interface workItem {
id: number
status: 'wait' | 'process' | 'finish' | 'error'
buffer?: Uint8Array
getBuffer: () => Promise<Uint8Array>
getBufferProgress: number
getBufferAbort?: () => void
isGettingBuffer: boolean
name: string
}
class Work {
constructor(func: (key: string, data: Uint8Array) => Promise<Uint8Array>) {
this.queue = []
this.func = func
this.abortLoop = false
}
queue: workItem[]
func: (key: string, data: Uint8Array) => Promise<Uint8Array>
abortLoop: boolean
get isBusy() {
return this.queue.filter((v) => v.status === 'process').length > 0
}
async funcLoop() {
if (this.isBusy || this.abortLoop) {
return
}
const currentItem = this.queue.filter((v) => v.status === 'wait')[0]
if (currentItem === undefined || currentItem.buffer === undefined) {
return
}
currentItem.status = 'process'
try {
currentItem.buffer = await this.func(key.value, currentItem.buffer)
currentItem.status = 'finish'
} catch (e) {
currentItem.status = 'error'
}
this.funcLoop()
}
async getBufferLoop() {
const _list = this.queue.filter(
(v) => v.isGettingBuffer === false && v.buffer === undefined
)
if (_list.length === 0 || this.abortLoop) {
return
}
const item = _list[0]
item.isGettingBuffer = true
try {
item.buffer = await item.getBuffer()
} catch(e) {
item.status = 'error'
}
item.isGettingBuffer = false
this.funcLoop()
this.getBufferLoop()
}
push(itemList: workItem[]) {
this.queue.push(...itemList)
this.getBufferLoop()
}
pushVFiles(itemList: VirtualFile[]) {
this.push(
itemList.map((v) => {
const item: workItem = shallowReactive({
id: Math.random(),
status: 'wait',
name: v.name,
getBuffer: () => {
const { response, abort } = miaoFetchApi.getFile(v, {
onProcess(percent) {
item.getBufferProgress = percent
}
})
item.getBufferAbort = abort
return response
},
isGettingBuffer: false,
getBufferProgress: 0
})
return item
})
)
}
}
const encryptWork = reactive(new Work(aes_ctr_worker))
// const decryptWork = reactive(new Work(aes_ctr_worker))
const workItemList = computed(() => [
...encryptWork.queue
// ...decryptWork.queue
])
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const key = ref('ABlockIs16Bytes!')
const handleDropVirtualFiles = (vFiles: VirtualFile[]) => {
encryptWork.pushVFiles(vFiles)
}
onMounted(() => {
encryptWork.pushVFiles(currentFiles.value)
})
onUnmounted(() => {
encryptWork.abortLoop = true
encryptWork.queue.forEach(item => {
item.getBufferAbort && item.getBufferAbort()
})
})
</script>
<style scoped lang="scss">
.miaoCrypto-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
&-item {
max-height: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
&-item {
margin: 10px auto;
width: 95%;
height: 40px;
background-color: #fff;
box-shadow: 0 0 15px -7px rgba(0, 0, 0, 0.1725490196),
0 0 3px 1px #43434359;
border-radius: 5px;
position: relative;
.miaoCrypto-container-item-percentBar {
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 5px;
}
.miaoCrypto-container-item-content {
position: relative;
z-index: 3
}
}
}
}
</style>
@@ -1,9 +0,0 @@
import aesJs from 'aes-js'
self.onmessage = async function (event) {
const data = event.data.data as Uint8Array
const key = event.data.key as Uint8Array
var aesCtr = new aesJs.ModeOfOperation.ctr(key, new aesJs.Counter(7058))
const encryptData = aesCtr.encrypt(data)
self.postMessage(encryptData)
}
@@ -1,77 +0,0 @@
<template>
<div class="miao-directory-container-bottom" :style="{
backgroundColor:
index % 2 === 1 ? 'rgb(61 61 61)' : 'rgb(162 162 162)'
}">
<div class="container-bottom-item">
<div class="container-bottom-item-icon">
<add-outline :style="{
color:
index % 2 === 0
? 'rgb(0 0 0)'
: 'rgb(255 255 255)'
}" @click="emit('add-new-item')" />
</div>
</div>
<div class="container-bottom-item">
<div class="container-bottom-item-icon">
<cloud-upload-outline :style="{
color:
index % 2 === 0
? 'rgb(0 0 0)'
: 'rgb(255 255 255)'
}" @click="emit('pick-files-upload')" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { AddOutline, CloudUploadOutline } from '@vicons/ionicons5'
defineProps<{
index: number
}>()
const emit = defineEmits<{
'add-new-item': []
'pick-files-upload': []
}>()
</script>
<style scoped lang="scss">
.miao-directory-container-bottom {
height: 60px;
background-color: bisque;
display: flex;
justify-content: space-around;
align-items: center;
user-select: none;
box-shadow: 0px 1px 10px #888888;
z-index: 2;
.container-bottom-item {
height: 40px;
width: 40px;
display: flex;
justify-content: center;
align-items: center;
.container-bottom-item-icon {
svg {
width: 30px;
height: 30px;
transition: width 0.3s ease;
transition: height 0.3s ease;
cursor: pointer;
}
&:hover {
svg {
width: 40px;
height: 40px;
}
}
}
}
}
</style>
@@ -1,304 +0,0 @@
<template>
<miao-draggable-div class="miao-item" :isDraggable="!draggable" ref="itemRef" @on-drag-start="handleDragStart">
<miaoContextMenu :options="dropDownOptions ?? []" :touch-time-out="500" @select="handleDropdownSelect">
<div class="item-main" :class="selected ? 'item-main-selected' : ''" ref="mainRef">
<div class="item-main-front" :style="{ backgroundColor: props.color }"></div>
<Icon class="item-main-icon" v-show="clientWidth > 250"></Icon>
<div class="item-main-info">
<div class="item-main-info-name">
<Icon class="item-main-info-name-icon" v-if="clientWidth <= 250"></Icon>
<n-ellipsis :style="{ maxWidth: clientWidth > 250 ? '80%' : '75%' }">{{ name }}</n-ellipsis>
</div>
<div class="item-main-info-date" v-if="time">
{{ time }}
</div>
</div>
<miao-draggable-div class="item-main-dragBtn" :isDraggable="draggable">
<ReorderFourOutline></ReorderFourOutline>
</miao-draggable-div>
</div>
</miaoContextMenu>
</miao-draggable-div>
</template>
<script setup lang="ts">
import miaoContextMenu from '@/components/miaoContextMenu.vue'
import {
DocumentText,
FileTray,
Help,
Image,
LogoChrome,
LogoCss3,
LogoJavascript,
LogoVue,
PlayCircle,
Disc,
ReorderFourOutline,
} from '@vicons/ionicons5'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import dateFormatter from '@/hooks/dateFormatter'
import { NEllipsis } from 'naive-ui'
import { DropdownOption } from 'naive-ui/es/dropdown';
import miaoDraggableDiv from '@/components/miaoDraggableDiv.vue'
// import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
// const miaoFetchApi = useMiaoFetchApi()
const props = defineProps<{
item: {
name: string,
type: 'directory' | 'file'
stats: {
mtimeMs: number
}
}
name?: string
color: string
dropDownOptions?: DropdownOption[]
selected?: boolean
}>()
const emit = defineEmits<{
download: []
delete: []
rename: []
dragStart: [e: DragEvent]
onSelected: []
plugin: [plugin: string]
}>()
const mainRef = ref<HTMLDivElement>()
const draggable = ref(true)
// 使用ResizeObserver实现
// 什么?性能问题?这玩意性能问题多着呢,轮不到它。
const clientWidth = ref<number>(666)
const name = props.name ?? props.item.name
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
clientWidth.value = entry.contentRect.width;
}
});
const time = computed(() => {
return dateFormatter(
new Date(props.item.stats.mtimeMs),
'yyyy-MM-dd hh:mm:ss'
)
})
// 应该将所有事件emit出去才对
const handleDropdownSelect = (key: string) => {
if (key === 'rename') {
emit('rename')
} else if (key === 'download') {
emit('download')
} else if (key === 'delete') {
emit('delete')
} else if (key === 'select') {
emit('onSelected')
} else if (key.startsWith('plugin:')) {
const plugin = key.split(':')[1]
emit('plugin', plugin)
}
}
const Icon = computed(() => {
try {
if (props.item.type === 'directory') {
return FileTray
} else if (props.item.type === 'file') {
const aft = props.item.name.split('.').at(-1)?.toLowerCase()
if (!aft) {
throw 1
} else if (
['txt', 'doc', 'docs', 'docx', 'xls', 'xlsx'].includes(aft)
) {
return DocumentText
} else if (
['mp3', 'wav', 'ape', 'acc', 'ogg', 'flac'].includes(aft)
) {
return Disc
} else if (
[
'xbm',
'tif',
'pjp',
'svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'gif',
'svg',
'jfif',
'webp',
'png',
'bmp',
'pjpeg',
'avif'
].includes(aft)
) {
return Image
} else if (['mp4', 'm2v', 'mkv'].includes(aft)) {
return PlayCircle
} else if (['js', 'jsx', 'ts', 'tsx'].includes(aft)) {
return LogoJavascript
} else if (['html'].includes(aft)) {
return LogoChrome
} else if (['css', 'scss', 'less'].includes(aft)) {
return LogoCss3
} else if (['vue'].includes(aft)) {
return LogoVue
}
return DocumentText
}
} catch {
return Help
}
})
const itemRef = ref()
const handleDragStart = (e: DragEvent) => {
emit('dragStart', e)
}
onMounted(() => {
if (mainRef.value) {
observer.observe(mainRef.value);
}
})
onBeforeUnmount(() => {
if (mainRef.value) {
observer.unobserve(mainRef.value);
}
})
</script>
<style lang="scss" scoped>
.miao-item {
cursor: pointer;
width: calc(100% - 20px);
// min-width: 170px;
// aspect-ratio: 66/9;
height: 50px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
margin: 10px 10px;
.item-main {
position: relative;
width: 100%;
height: 95%;
background-color: rgb(255, 255, 255);
border-radius: 7px;
display: flex;
align-items: center;
transition: background-color 0.3s ease;
overflow-x: hidden;
.item-main-front {
height: 100%;
min-width: 10px;
border-radius: 7px 0 0 7px;
background-color: rgb(0, 0, 0);
}
.item-main-icon {
min-width: 30px;
margin-left: 10px;
height: 30px;
width: 30px;
color: #00403e;
transition: color 0.15s ease-out;
}
.item-main-info {
margin-left: 10px;
height: 100%;
width: calc(100% - 10px);
display: flex;
flex-direction: column;
&-name {
position: relative;
user-select: none;
display: flex;
align-items: center;
margin-top: 3px;
&-icon {
color: #00403e;
width: 20px;
margin-right: 5px;
transition: color 0.15s ease-out;
}
}
&-date {
user-select: none;
font-size: small;
text-wrap: nowrap;
}
}
.item-main-option {
height: 100%;
width: 20px;
position: absolute;
right: 0;
border-radius: 0 7px 7px 0;
background-color: rgba(0, 0, 0, 0);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: background-color 0.3s;
.item-main-option-icon {
color: #000;
transition: color 0.15s;
}
&:hover,
&:active {
background-color: #8b8b8b;
.item-main-option-icon {
color: #fff;
}
}
}
&:hover,
&:active {
background-color: #d2d2d2;
}
}
.item-main-selected {
background-color: rgba(0, 128, 207, 0.32);
&:hover {
background-color: rgba(0, 128, 207, 0.64);
;
}
}
}
.item-main-dragBtn {
width: 20px;
height: 20px;
position: absolute;
right: 5px;
}
</style>
@@ -1,182 +0,0 @@
<template>
<div
class="miao-directory-container-items"
:style="{
backgroundColor: index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)'
}">
<n-scrollbar ref="scrollbarRef" style="max-height: 100%">
<miao-lazy-div
v-for="dir in showData_directory"
:key="dir.id"
min-height="50px"
margin="10px">
<miaoDirectoryItem
:item="dir"
:color="props.color"
:selected="selectedItem.includes(dir)"
:drop-down-options="itemDropDownOptions(dir).value"
@delete="handleItemDelete(dir)"
@plugin="(p) => handleItemUsePlugin(dir)(p)"
@click="handleItemClick(dir)"
@drag-start="(e) => handleItemDragStart(dir)(e)"
@rename="handleItemRename(dir)"
@on-selected="handleItemSelect(dir)" />
</miao-lazy-div>
<miao-lazy-div
v-for="file in showData_files"
:key="file.id"
min-height="50px">
<miaoDirectoryItem
:item="file"
:color="props.color"
:selected="selectedItem.includes(file)"
:drop-down-options="itemDropDownOptions(file).value"
@click="handleItemClick(file)"
@download="handleItemDownload(file)"
@delete="handleItemDelete(file)"
@drag-start="(e) => handleItemDragStart(file)(e)"
@rename="handleItemRename(file)"
@plugin="(p) => handleItemUsePlugin(file)(p)"
@on-selected="handleItemSelect(file)" />
</miao-lazy-div>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import { NScrollbar } from 'naive-ui'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import miaoDirectoryItem from './miaoDirectoryItem.vue'
import miaoLazyDiv from '@/components/miaoLazyDiv.vue'
import { computed, ref } from 'vue'
import usePluginCenter from '@/hooks/usePluginCenter'
import { renderIcon } from '@/hooks/miaoTools'
const pluginCenter = usePluginCenter()
const props = defineProps<{
color: string
index: number
showData_directory: VirtualDirectory[]
showData_files: VirtualFile[]
selectedItem: (VirtualFile | VirtualDirectory)[]
}>()
const emit = defineEmits<{
'item-click': [item: VirtualDirectory | VirtualFile]
'item-download': [item: VirtualFile]
'item-delete': [item: VirtualDirectory | VirtualFile]
'item-drag-start': [_event: DragEvent, item: VirtualDirectory | VirtualFile]
'item-rename': [item: VirtualDirectory | VirtualFile]
'item-select': [item: VirtualDirectory | VirtualFile]
}>()
const scrollbarRef = ref()
const itemDropDownOptions = (item: VirtualDirectory | VirtualFile) =>
computed(() => {
const items = [...props.selectedItem]
const selected = props.selectedItem.includes(item)
if (!selected) {
items.push(item)
}
const vDirs = items.filter((v) => v.type === 'directory')
const vFiles = items.filter((v) => v.type === 'file')
const options = [
{
label: '重命名',
key: 'rename'
},
{
label: '删除',
key: 'delete'
}
]
if (item.type === 'file') {
options.push(
...[
{
label: '下载',
key: 'download'
}
]
)
}
if (selected === false) {
options.push(
...[
{
label: '多选:选择',
key: 'select'
}
]
)
} else {
options.push(
...[
{
label: '多选:取消',
key: 'select'
}
]
)
}
options.push(
...pluginCenter.getUsablePlugin(vDirs, vFiles).map((v) => ({
label: `插件:${v.name}`,
key: `plugin:${v.key}`,
icon: v.icon ? renderIcon(v.icon) : undefined
}))
)
return options
})
const handleItemClick = (item: VirtualDirectory | VirtualFile) => {
emit('item-click', item)
}
const handleItemDownload = (item: VirtualFile) => {
emit('item-download', item)
}
const handleItemDelete = (item: VirtualDirectory | VirtualFile) => {
emit('item-delete', item)
}
const handleItemDragStart =
(item: VirtualDirectory | VirtualFile) => (_event: DragEvent) => {
emit('item-drag-start', _event, item)
}
const handleItemRename = (item: VirtualDirectory | VirtualFile) => {
emit('item-rename', item)
}
const handleItemSelect = (item: VirtualDirectory | VirtualFile) => {
emit('item-select', item)
}
const handleItemUsePlugin =
(item: VirtualDirectory | VirtualFile) => (plugin: string) => {
const items = [...props.selectedItem]
const selected = props.selectedItem.includes(item)
if (!selected) {
items.push(item)
}
const vDirs = items.filter((v) => v.type === 'directory')
const vFiles = items.filter((v) => v.type === 'file')
pluginCenter.usePlugin(plugin, vDirs, vFiles)
}
defineExpose({
scrollTo: (...args: any) => scrollbarRef.value.scrollTo(...args)
})
</script>
<style scoped lang="scss">
.miao-directory-container-items {
width: 100%;
flex: 1;
min-height: 0;
}
</style>
@@ -1,152 +0,0 @@
<template>
<div class="miao-directory-container-top" :style="{
backgroundColor:
index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)'
}">
<div class="container-top-colorfulBar" :style="{ backgroundColor: props.color }"></div>
<div class="container-top-breadcrumb">
<div class="container-top-breadcrumb-container">
<n-breadcrumb separator=">" style="margin-left: 5px">
<n-breadcrumb-item :clickable="false">
<n-icon size="18">
<CloudOutline />
</n-icon>
</n-breadcrumb-item>
<n-breadcrumb-item v-for="dir in currentDirectory?.getParents"
@click="handleClickBreadcrumbItem(dir)" :clickable="true">
<div>{{ dir.name }}</div>
</n-breadcrumb-item>
</n-breadcrumb>
</div>
</div>
<div class="container-top-tools">
<n-icon class="container-top-tools-item" size="15" @click="$emit('back')">
<ChevronBackOutline />
</n-icon>
<n-icon class="container-top-tools-item" size="14" @click="$emit('reload')">
<ReloadOutline class="container-top-tools-item-reload" />
</n-icon>
<n-dropdown trigger="click" :options="openMenuOption" @select="(key: string) => emit('menuSelect', key)">
<n-icon class="container-top-tools-item" size="14">
<EllipsisVertical />
</n-icon>
</n-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import { NBreadcrumbItem, NBreadcrumb, NIcon, NDropdown } from 'naive-ui'
import {
CloudOutline,
ChevronBackOutline,
ReloadOutline,
EllipsisVertical,
} from '@vicons/ionicons5'
import VirtualDirectory from '@/class/VirtualDirectory'
const props = defineProps<{
color: string;
index: number
currentDirectory: VirtualDirectory | undefined;
openMenuOption: {
key: string,
label: string,
[key: string]: any,
}[]
}>()
const emit = defineEmits<{
back: [],
reload: [],
menuSelect: [key: string]
'set-current-directory': [virtualDirectory: VirtualDirectory]
}>()
const handleClickBreadcrumbItem = (virtualDirectory: VirtualDirectory) => {
emit('set-current-directory', virtualDirectory)
}
</script>
<style scoped lang="scss">
.miao-directory-container-top {
height: 30px;
width: 100%;
user-select: none;
position: relative;
display: flex;
align-items: center;
overflow: hidden;
background-color: #f1f1f1;
.container-top-colorfulBar {
height: 70%;
width: 5px;
margin-left: 10px;
border-radius: 3px 0 0 3px;
}
.container-top-breadcrumb {
position: relative;
height: 70%;
width: calc(100% - 95px);
display: flex;
justify-content: flex-start;
align-items: center;
// overflow: hidden;
overflow-y: hidden;
overflow-x: hidden;
background-color: #ffffff;
padding: 0 5px;
border-radius: 0 3px 3px 0;
.container-top-breadcrumb-container {
position: absolute;
height: 100%;
right: 0;
top: 0;
min-width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
}
}
.container-top-tools {
position: relative;
height: 100%;
// width: 70px;
padding: 0 5px;
display: flex;
justify-content: space-around;
align-items: center;
.container-top-tools-item {
width: 20px;
height: 20px;
margin: 0 1px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff00;
border-radius: 5px;
transition: background-color 0.3s;
cursor: pointer;
.container-top-tools-item-reload {
transition: transform 0.6s ease-in-out;
}
&:hover {
background-color: rgba(128, 128, 128, 0.4);
}
&:active .container-top-tools-item-reload {
transform: rotate(360deg);
}
}
}
}
</style>
-23
View File
@@ -1,23 +0,0 @@
import { registerComponentOption } from '@/class/PluginCenter'
import FileTray from '@vicons/ionicons5/es/FileTray'
// import MiaoDirectory from '@/plugins/miaoDirectory/miaoDirectory.vue'
const pluginConfig: registerComponentOption = {
key: 'miaoDirectory',
name: '文件夹',
icon: FileTray,
getComponent: async () =>
(await import('@/plugins/miaoDirectory/miaoDirectory.vue')).default,
filter: (vDirs, vFiles) => {
if (vFiles.length) {
return false
}
return vDirs.length === 1
}
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
@@ -1,37 +0,0 @@
import { Plugin, PluginGroup } from "@/class/PluginCenter";
import { PluginExportType } from "@/types/type";
import { watch } from "vue";
const pluginConfig: Plugin = {
name: "初始化完成之后打开miaoDirectory",
filter: () => {
return false;
},
disable: false,
group: [PluginGroup.immediately],
func: (_, __, hooks) => {
const { getPluginCenter, getRootVDirectory, getViews } = hooks
const views = getViews();
const pc = getPluginCenter();
const root = getRootVDirectory();
setTimeout(() => {
if (views.length === 0) {
pc.usePlugin('miaoDirectory', [root])
}
watch(() => views.length, () => {
if(views.length === 0) {
pc.usePlugin('miaoDirectory', [root])
}
})
pc.deregister('miaoDirectory_onInit')
})
},
priority: -1
};
export default pluginConfig;
export const type = PluginExportType.default;
export const key = "miaoDirectory_onInit";
-683
View File
@@ -1,683 +0,0 @@
<template>
<miao-drop-handler
@on-virtual-directory="handleDropVDirectory"
@on-files="handleDropFiles"
@on-virtual-files="handleDropVirtualFiles">
<miaoAlertTipProvider ref="miaoAlertTip">
<div class="miao-directory-item-container" ref="rootDomRef">
<!-- 顶部面包屑导航和工具栏 -->
<miao-directory-top
:color="props.color"
:current-directory="currentDirectory"
@back="handleBack"
:open-menu-option="topBarMenuOption"
@reload="handleReload"
@exit="emit('exit')"
:index="index"
@set-current-directory="setCurrentDirectory"
@search="handleSearch"
@menu-select="handleTopBarMenuSelect" />
<!-- 文件和文件夹列表 -->
<miao-directory-items
ref="miaoDirectoryItemRef"
:color="props.color"
:showData_directory="showData_directory"
:showData_files="showData_files"
:selected-item="selectedItem.value"
:index="index"
@item-click="handleItemClick"
@item-download="handleItemDownload"
@item-delete="handleItemDelete"
@item-drag-start="handleItemDragStart"
@item-rename="handleItemRename"
@item-select="handleItemSelect" />
<!-- 底部工具栏 -->
<miao-directory-bottom
:index="index"
@add-new-item="handleAddNewItem"
v-if="false"
@pick-files-upload="handlePickFilesUpload" />
<miao-mask v-model:show="showModel"></miao-mask>
<miao-popup-input ref="popupInput"></miao-popup-input>
</div>
</miaoAlertTipProvider>
</miao-drop-handler>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import Config from '@/config'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import useDataBus from '@/hooks/useDataBus'
import useUploadQueue from '@/hooks/useUploadQueue'
import useVirtualPages from '@/hooks/useVirtualPages'
import { filePicker, renderIcon } from '@/hooks/miaoTools'
import uniq from 'lodash/uniq'
import miaoDirectoryTop from './components/miaoDirectoryTop.vue'
import MiaoDirectoryItems from './components/miaoDirectoryItemsContainer.vue'
import MiaoDirectoryBottom from './components/miaoDirectoryBottom.vue'
import MiaoMask from '@/components/miaoMask.vue'
import MiaoPopupInput from '@/components/miaoPopupInput.vue'
import MiaoDropHandler from '@/components/miaoDropHandler.vue'
import miaoAlertTipProvider from '@/components/miaoAlertTipProvider.vue'
import {
CloseOutline,
SearchOutline,
FilterOutline,
CheckmarkDoneOutline,
DocumentText,
ExtensionPuzzleOutline,
FileTray,
AddOutline,
CloudUploadOutline
} from '@vicons/ionicons5'
import usePluginCenter from '@/hooks/usePluginCenter'
const { baseUrl, api } = Config
const dataBus = useDataBus()
const views = useVirtualPages()
const uploadQueue = useUploadQueue()
const miaoFetchApi = useMiaoFetchApi()
const pluginCenter = usePluginCenter()
const props = defineProps<{
color: string
id: number
}>()
const emit = defineEmits<{
exit: []
}>()
const popupInput = ref<InstanceType<typeof MiaoPopupInput>>()
// 是否显示模态框
const showModel = ref(0)
const searchText = ref<string>('')
const showDirs = ref<boolean>(true)
const showFiles = ref<boolean>(true)
const miaoDirectoryItemRef = ref()
const miaoAlertTip = ref<InstanceType<typeof miaoAlertTipProvider>>()
class SelectedItem {
constructor() {
this._selectedItem = []
}
_selectedItem: (VirtualDirectory | VirtualFile)[]
push(item: VirtualDirectory | VirtualFile) {
this._selectedItem = uniq([item, ...this._selectedItem])
}
pushArray(items: (VirtualDirectory | VirtualFile)[]) {
this._selectedItem = uniq([...items, ...this._selectedItem])
}
remove(item: VirtualDirectory | VirtualFile) {
this._selectedItem = this._selectedItem.filter((v) => v !== item)
}
/**
* 仅保留filter为true的元素, filter默认为()=>false
*/
clear(filter?: (item: VirtualDirectory | VirtualFile) => boolean) {
filter || (filter = (__) => false)
this._selectedItem = this.value.filter(filter)
}
// 保证内容仍然存在于dir中且各个元素不重复
get value() {
const _filterList = [
...(currentDirectory.value?.directories ?? []),
...(currentDirectory.value?.files ?? [])
]
return uniq(this._selectedItem).filter((v) => {
if (currentDirectory.value === undefined) {
return false
}
// @ts-ignore
return _filterList.includes(v)
})
}
}
// 已选择的item,用于多选功能
const selectedItem = reactive(new SelectedItem())
const currentDirectories = defineModel<VirtualDirectory[]>(
'currentDirectories',
{
required: true
}
)
// 目前展示的Dir
const currentDirectory = computed(() => {
return currentDirectories.value[0]
})
// 当前组件在views中的序号
const index = computed(() => {
return views.getIndex(props.id)
})
// 用于展示的数据,后续会增加排序等操作
const showData_directory = computed<VirtualDirectory[]>(() => {
if (showDirs.value === false) {
return []
}
let data = currentDirectory.value?.directories
? [...currentDirectory.value?.directories]
: []
if (searchText.value !== '') {
data = data.filter((vd) => vd.name.includes(searchText.value))
}
return data
})
const showData_files = computed<VirtualFile[]>(() => {
if (showFiles.value === false) {
return []
}
let data = currentDirectory.value?.files
? [...currentDirectory.value?.files]
: []
if (searchText.value !== '') {
data = data.filter((vd) => vd.name.includes(searchText.value))
}
return data
})
const topBarMenuOption = computed(() => {
return [
{
label: '新建',
key: 'create',
icon: renderIcon(AddOutline)
},
{
label: '上传',
key: 'upload',
icon: renderIcon(CloudUploadOutline)
},
{
label: '搜索',
key: 'search',
icon: renderIcon(SearchOutline)
},
searchText.value !== ''
? {
label: '重置搜索',
key: 'searchReset',
icon: renderIcon(SearchOutline)
}
: undefined,
// selectedItem.value.length !== 0
// ? {
// label: '重置多选',
// key: 'selectedItemReset',
// icon: renderIcon(CheckmarkDoneOutline)
// }
// : undefined,
{
label: '筛选',
key: 'filter',
children: [
{
label: `${showDirs.value ? '隐藏' : '显示'}文件夹`,
key: 'showDirs',
icon: renderIcon(FileTray)
},
{
label: `${showFiles.value ? '隐藏' : '显示'}文件`,
key: 'showFiles',
icon: renderIcon(DocumentText)
}
],
icon: renderIcon(FilterOutline)
},
{
label: '多选',
key: 'multiSelect',
children: [
{
label: `全选`,
key: 'multiSelect:all'
},
{
label: `全不选`,
key: 'multiSelect:reset'
},
{
label: `反选`,
key: 'multiSelect:inversion'
},
selectedItem.value.length === 2
? {
label: `区间内全选`,
key: 'multiSelect:range'
}
: undefined
].filter((v) => v !== undefined),
icon: renderIcon(CheckmarkDoneOutline)
},
{
label: '插件',
key: 'plugin',
children: [
...pluginCenter
.getUsablePlugin([currentDirectories.value[0]], [])
.map((v) => ({
label: `${v.name}`,
key: `plugin:${v.key}`,
icon: v.icon ? renderIcon(v.icon) : undefined
}))
],
icon: renderIcon(ExtensionPuzzleOutline)
},
{
label: '关闭',
key: 'close',
icon: renderIcon(CloseOutline)
}
].filter((v) => v !== undefined)
})
// 设置显示的dir
const setCurrentDirectory = async (virtualDirectory: VirtualDirectory) => {
try {
// 若没有缓存数据则同步刷新,有缓存数据则异步刷新
if (!virtualDirectory.isUpdated) {
await virtualDirectory.update()
} else {
virtualDirectory.update()
}
if (currentDirectories.value[0] !== virtualDirectory) {
selectedItem.clear()
handleSearchReset()
nextTick(() => {
miaoDirectoryItemRef.value.scrollTo({
top: 0,
behavior: 'instant'
})
})
}
currentDirectories.value[0] = virtualDirectory
} catch(e) {
miaoAlertTip.value?.alertTip(`获取文件夹内容出错`, {
type: 'error',
timeout: 3000
})
}
}
const reload = () => {
setCurrentDirectory(currentDirectory.value)
}
const openUrl = (
href: string,
config?: {
download?: string
target?: string
}
) => {
let { target, download } = config ?? {}
const a = document.createElement('a')
a.setAttribute('href', href)
a.setAttribute('target', target ?? '_blank')
download && a.setAttribute('download', download)
a.click()
}
const handleTopBarMenuSelect = (key: string) => {
if (key === 'close') {
emit('exit')
} else if (key === 'search') {
handleSearch()
} else if (key === 'upload') {
handlePickFilesUpload()
} else if (key === 'create') {
handleAddNewItem()
} else if (key === 'searchReset') {
handleSearchReset()
// } else if (key === 'selectedItemReset') {
// selectedItem.clear()
} else if (key === 'showDirs') {
showDirs.value = !showDirs.value
showDirs.value === false &&
selectedItem.clear((v) => v.type !== 'directory')
} else if (key === 'showFiles') {
showFiles.value = !showFiles.value
showFiles.value === false &&
selectedItem.clear((v) => v.type !== 'file')
} else if (key.startsWith('plugin:')) {
const pluginName = key.split(':')[1]
pluginCenter.usePlugin(pluginName, [currentDirectory.value], [])
} else if (key.startsWith('multiSelect:')) {
const opName = key.split(':')[1]
switch (opName) {
case 'all':
selectedItem.pushArray([
...showData_directory.value,
...showData_files.value
])
break
case 'reset':
selectedItem.clear()
break
case 'inversion':
const sArr = selectedItem.value
const arr = [
...showData_directory.value,
...showData_files.value
].filter((i) => !sArr.includes(i))
selectedItem.clear()
selectedItem.pushArray(arr)
break
case 'range':
const _sArr = selectedItem.value
const _arr = [
...showData_directory.value,
...showData_files.value
]
const _nArr = []
let _counter = 0
for (const i of _arr) {
if (_sArr.includes(i)) {
_counter++
}
if (_counter === 1) {
_nArr.push(i)
} else if (_counter === 2) {
_nArr.push(i)
break
}
}
selectedItem.clear()
selectedItem.pushArray(_nArr)
break
}
}
}
const handleBack = () => {
if (currentDirectory.value?.parent) {
setCurrentDirectory(currentDirectory.value.parent)
}
}
const handleReload = () => {
reload()
}
const handleItemClick = (item: VirtualDirectory | VirtualFile) => {
if (selectedItem.value.length > 0) {
handleItemSelect(item)
} else {
if (item.type === 'file') {
const useablePlugins = pluginCenter.getUsablePlugin([], [item])
if (useablePlugins.length > 0) {
pluginCenter.usePlugin(useablePlugins[0].key, [], [item])
} else {
openUrl(`${baseUrl}${api.get}${item.path}`)
}
} else if (item.type === 'directory') {
setCurrentDirectory(item)
}
}
}
const handleItemDownload = (item: VirtualFile) => {
openUrl(`${baseUrl}${api.get}${item.path}`, {
download: item.name
})
}
const handleItemDelete = async (_item: VirtualDirectory | VirtualFile) => {
for (let item of uniq([...selectedItem.value, _item])) {
;(async () => {
const { response } = miaoFetchApi.delete(item, {
retry: 5
})
// todo: 将删除事件统一用一个事件管理中心管理
const id = (await response).eventId
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
}
})()
}
}
const handleItemRename = async (item: VirtualDirectory | VirtualFile) => {
const popupRes = await popupInput.value?.popup({
title: '重命名',
inputValue: item.name,
inputProps: {
placeholder: item.name
},
options: [
{
label: '取消',
key: false,
type: 'error'
},
{
label: '确认',
key: true,
type: 'success'
}
]
})
if (popupRes?.key === false) {
return
}
const newName = popupRes?.text
if (!newName) {
return
}
const { response } = miaoFetchApi.rename(item, newName, {
retry: 5
})
const id = (await response).eventId
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
}
}
const handleItemDragStart = async (
_event: DragEvent,
item: VirtualDirectory | VirtualFile
) => {
const items = uniq([item, ...selectedItem.value])
dataBus.set(
'dragData_vFiles',
items.filter((v) => v.type === 'file')
)
dataBus.set(
'dragData_vDirectory',
items.filter((v) => v.type === 'directory')
)
// selectedItem.clear()
}
const handleDropFiles = async (files: File[]) => {
currentDirectory.value &&
uploadQueue.push(files, currentDirectory.value, {
onFinish() {
reload()
}
})
}
const handleDropVDirectory = async (vDirs: VirtualDirectory[]) => {
// 不能移到自己或自己的子文件夹内
for (let vDir of vDirs) {
if (currentDirectory.value.getParents.includes(vDir)) {
return
}
if (vDir.parent === currentDirectory.value) {
return
}
}
for (let dir of vDirs) {
;(async () => {
const _from = dir.parent
const { response } = miaoFetchApi.cut(dir, currentDirectory.value)
const id = (await response).eventId
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
_from && _from.update()
}
})()
}
}
const handleDropVirtualFiles = (files: VirtualFile[]) => {
if (files[0].parent === currentDirectory.value) {
return
}
for (let file of files) {
;(async () => {
const _from = file.parent
const { response } = miaoFetchApi.cut(file, currentDirectory.value)
const id = (await response).eventId
const { response: res } = miaoFetchApi.query(id)
const eventResult = await res
if (eventResult.status === 'success') {
reload()
_from.update()
}
})()
}
}
const handlePickFilesUpload = async () => {
const files = await filePicker({
multiple: true
})
const _currDir = currentDirectory.value
uploadQueue.push(files, _currDir, {
onFinish() {
_currDir.update()
}
})
}
const handleAddNewItem = async () => {
const popupRes = await popupInput.value?.popup({
title: '新建文件/文件夹',
inputProps: {
placeholder: ''
},
options: [
{
label: '取消',
key: 'cancel',
type: 'error'
},
{
label: '文件',
key: 'file',
type: 'success'
},
{
label: '文件夹',
key: 'dir',
type: 'success'
}
]
})
// todo: 上传前如重名文件夹等等的错误提前检查
if (popupRes?.key === 'cancel' || popupRes?.text === '') {
return
}
const _currDir = currentDirectory.value
const name = popupRes?.text
if (!name) {
return
}
// @ts-ignore
const opType: 'file' | 'dir' = popupRes?.key
if (opType === 'dir') {
const res = await miaoFetchApi.mkdir(currentDirectory.value, name)
if (res.message === 'success') {
_currDir.update()
}
} else {
const { response } = miaoFetchApi.mkFile(currentDirectory.value, name)
const res = await response
if (res.message === 'success') {
_currDir.update()
}
}
}
const handleItemSelect = (item: VirtualDirectory | VirtualFile) => {
if (!selectedItem.value.includes(item)) {
selectedItem.push(item)
} else {
selectedItem.remove(item)
}
}
const handleSearch = async () => {
const popupRes = await popupInput.value?.popup({
title: '搜索当前文件夹',
inputValue: searchText.value,
inputProps: {
placeholder: ''
},
options: [
{
label: '取消',
key: 'cancel',
type: 'default'
},
{
label: '重置',
key: 'reset',
type: 'default'
},
{
label: '搜索',
key: 'search',
type: 'success'
}
]
})
if (!popupRes) {
return
}
if (popupRes.key === 'search') {
searchText.value = popupRes.text
} else if (popupRes.key === 'reset') {
handleSearchReset()
}
}
const handleSearchReset = () => {
searchText.value = ''
}
onMounted(async () => {
await setCurrentDirectory(currentDirectory.value)
})
</script>
<style scoped lang="scss">
$top-height: 30px;
$bottom-bar-height: 60px;
.miao-directory-item-container {
position: relative;
height: 100%;
width: inherit;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 170px;
}
</style>
-26
View File
@@ -1,26 +0,0 @@
import { registerComponentOption } from '@/class/PluginCenter'
import LogoChrome from '@vicons/ionicons5/es/LogoChrome'
const pluginConfig: registerComponentOption = {
key: 'miaoIframe',
name: '框架',
icon: LogoChrome,
getComponent: async () =>
(await import('@/plugins/miaoIframe/miaoIframe.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('html')
},
priority: 2
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
-52
View File
@@ -1,52 +0,0 @@
<template>
<div class="miao-markdown-container-miaomiaomiao">
<iframe
ref="iframeRef"
:src="iframeSrc"
frameborder="0"
class="iframe-container-iframe"></iframe>
</div>
</template>
<script setup lang="ts">
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import cloneDeep from 'lodash/cloneDeep';
import { inject, onMounted, ref } from 'vue'
import buildFromTree from '../../hooks/buildFromTree';
const root = inject('rootDirectory') as VirtualDirectory
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const iframeSrc = ref<string>()
const iframeRef = ref<HTMLIFrameElement>();
const sendRootTree = () => {
iframeRef.value?.contentWindow && iframeRef.value.contentWindow.postMessage(cloneDeep(root.tree), "*");
}
const handleIframeMessage = (e: MessageEvent<any>) => {
e
// const data = e.data;
sendRootTree()
}
onMounted(() => {
iframeSrc.value = currentFiles.value[0].url
if (typeof window.addEventListener != "undefined") {
window.addEventListener("message", handleIframeMessage, false);
}
console.log(buildFromTree(cloneDeep(root.tree)))
})
</script>
<style scoped lang="scss">
.miao-markdown-container-miaomiaomiao {
width: 100%;
height: 100%;
.iframe-container-iframe {
width: inherit;
height: inherit;
}
}
</style>
-49
View File
@@ -1,49 +0,0 @@
import { registerComponentOption } from '@/class/PluginCenter'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import { Image } from '@vicons/ionicons5'
const pluginConfig: registerComponentOption = {
key: 'miaoImages',
name: '图片浏览',
icon: Image,
getComponent: async () =>
(await import('@/plugins/miaoImages/miaoImages.vue')).default,
filter: (vDirs: VirtualDirectory[], vFiles: VirtualFile[]) => {
if (vDirs.length === 0 && vFiles.length === 0) {
return false
}
if (vDirs.length) {
return true
}
for (let vf of vFiles)
for (let end of [
'xbm',
'tif',
'pjp',
'svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'gif',
'svg',
'jfif',
'webp',
'png',
'.bmp',
'pjpeg',
'avif'
]) {
if (vf.name.endsWith(end)) {
return true
}
}
return false
}
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
-368
View File
@@ -1,368 +0,0 @@
<template>
<miao-drop-handler
@on-virtual-directory="handleDrop"
@on-virtual-files="handleDrop">
<miao-message-provider ref="miaoMessageRef">
<div class="miaoImage" ref="rootRef">
<n-scrollbar ref="NScrollbarRef" style="max-height: 100%;">
<div
class="miaoImage-container"
v-for="imageGroup in imageGroupList">
<div class="miaoImage-container-day">
<!-- <div class="miaoImage-container-divide miaoImage-container-divide-before">
<div class="miaoImage-container-divide-line"></div>
</div> -->
<span class="miaoImage-container-day-text">
{{ imageGroup.day }}
</span>
<!-- <div class="miaoImage-container-divide">
<div class="miaoImage-container-divide-line"></div>
</div> -->
</div>
<div
class="miaoImage-container-row"
v-for="vFile in imageGroup.items"
:key="vFile.id">
<img
:src="vFile.url"
class="miaoImage-container-row-image"
loading="lazy"
:class="
activeImage === vFile
? 'miaoImage-container-row-image-active'
: ''
"
@click="activeImage = vFile" />
</div>
</div>
</n-scrollbar>
<miao-mask
:show="activeImage !== undefined"
@click="activeImage = undefined"
class="miaoImage-mask"
@wheel="handleWheel">
<ChevronBack
class="miaoImage-mask-btn miaoImage-mask-btn-back"
@click="handleActiveImageBack" />
<img
:src="activeImage?.url"
class="miaoImage-mask-active"
@click="(e) => e.stopPropagation()"
draggable="false"
:style="{transform: `scale(${imgViewScale})`}" />
<ChevronForward
class="miaoImage-mask-btn miaoImage-mask-btn-forward"
@click="handleActiveImageForward" />
</miao-mask>
</div>
</miao-message-provider>
</miao-drop-handler>
</template>
<script setup lang="ts">
import miaoMask from '@/components/miaoMask.vue'
import miaoDropHandler from '@/components/miaoDropHandler.vue'
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import uniq from 'lodash/uniq'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { NScrollbar } from 'naive-ui'
import { ChevronBack, ChevronForward } from '@vicons/ionicons5'
const imageSuffixList = [
'xbm',
'tif',
'pjp',
'svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'gif',
'svg',
'jfif',
'webp',
'png',
'bmp',
'pjpeg',
'avif'
]
const currentDirectories = defineModel<VirtualDirectory[]>(
'currentDirectories',
{
required: true
}
)
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const NScrollbarRef = ref()
const rootRef = ref<any>()
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
const imgViewScale = ref(1)
const imageFileList = ref<VirtualFile[]>([])
const activeImage = ref<VirtualFile>()
const imageGroupList = computed(() => {
return groupByDay(imageFileList.value)
})
watch(activeImage, () => {
nextTick(scrollToActiveImage)
imgViewScale.value = 1
})
const groupByDay = (array: VirtualFile[]) => {
const result = []
let currentDay = null
let currentGroup: VirtualFile[] = []
for (const item of array) {
const date = new Date(item.stats.mtimeMs)
const day = `${date.getFullYear()}年 ${String(
date.getMonth() + 1
).padStart(2, '0')}月 ${String(date.getDate()).padStart(2, '0')}日`
if (day !== currentDay) {
if (currentGroup.length > 0) {
result.push({
day: currentDay,
items: currentGroup
})
}
currentDay = day
currentGroup = [item]
} else {
currentGroup.push(item)
}
}
if (currentGroup.length > 0) {
result.push({
day: currentDay,
items: currentGroup
})
}
return result
}
const scrollToActiveImage = () => {
const dom = rootRef.value?.querySelector(
'.miaoImage-container-row-image-active'
)
dom && NScrollbarRef.value.scrollTo({
top: dom.offsetTop - 30,
behavior: 'smooth'
})
}
const handleActiveImageBack = (e: any) => {
e.stopPropagation()
if (!activeImage.value) {
return
}
const index = imageFileList.value.indexOf(activeImage.value)
activeImage.value =
imageFileList.value[
(index - 1 + imageFileList.value.length) %
imageFileList.value.length
]
}
const handleActiveImageForward = (e: any) => {
e.stopPropagation()
if (!activeImage.value) {
return
}
const index = imageFileList.value.indexOf(activeImage.value)
activeImage.value =
imageFileList.value[
(index + 1 + imageFileList.value.length) %
imageFileList.value.length
]
}
const handleDrop = async (vItems: (VirtualFile | VirtualDirectory)[]) => {
const length_before = imageFileList.value.length
if (vItems[0].type === 'directory') {
// @ts-ignore
currentDirectories.value = [...currentDirectories.value, ...vItems]
for (const i of vItems) {
// @ts-ignore
await updateImageSrcList(i)
}
} else {
// @ts-ignore
currentFiles.value = [...currentFiles, ...vItems]
// @ts-ignore
await updateImageSrcList(vItems)
}
miaoMessageRef.value?.alertTip(
length_before === imageFileList.value.length
? '未发现任何可添加图片'
: `导入${imageFileList.value.length - length_before}张图片`,
{
type:
length_before === imageFileList.value.length
? 'info'
: 'success',
timeout: 5000
}
)
}
const updateImageSrcList = async (source: VirtualFile[] | VirtualDirectory) => {
let newFileList: VirtualFile[]
if ('type' in source) {
await source.update()
newFileList = [
...(source.files?.filter((v) =>
imageSuffixList.includes(v.name.split('.').pop() ?? '')
) ?? [])
]
} else {
newFileList = [
...(source.filter((v) =>
imageSuffixList.includes(v.name.split('.').pop() ?? '')
) ?? [])
]
}
imageFileList.value = uniq([...imageFileList.value, ...newFileList]).sort(
(a, b) => -a.stats.mtimeMs + b.stats.mtimeMs
)
}
const handleWheel = (e: WheelEvent) => {
if (e.deltaY < 0) {
imgViewScale.value *= 1.42857
} else {
imgViewScale.value /= 1.42857
}
}
onMounted(async () => {
const length_before = imageFileList.value.length
for (const i of [currentFiles.value, ...currentDirectories.value]) {
await updateImageSrcList(i)
}
miaoMessageRef.value?.alertTip(
length_before === imageFileList.value.length
? '未发现任何新增图片'
: `导入${imageFileList.value.length - length_before}张图片`,
{
type:
length_before === imageFileList.value.length
? 'info'
: 'success',
timeout: 5000
}
)
})
</script>
<style scoped lang="scss">
.miaoImage {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-wrap: wrap;
gap: 10px;
.miaoImage-container {
width: 100%;
display: flex;
flex-wrap: wrap;
margin-bottom: 20px;
margin-top: 20px;
.miaoImage-container-day {
width: 100%;
display: flex;
align-items: center;
position: relative;
margin: 0 20px;
.miaoImage-container-divide {
flex: 1;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.miaoImage-container-divide-line {
height: 3px;
width: 100%;
border-radius: 5px;
background-color: rgb(244 244 244);
}
.miaoImage-container-divide-before {
flex: 0.1;
}
.miaoImage-container-day-text {
margin: 0 0 0 10px;
font-size: 15px;
user-select: none;
}
}
.miaoImage-container-row {
display: flex;
justify-content: center;
align-items: center;
width: 25%;
aspect-ratio: 1;
.miaoImage-container-row-image {
width: 80%;
aspect-ratio: 1;
object-fit: cover;
transition: width 0.25s;
box-shadow: 0 0 10px -5px;
cursor: pointer;
user-select: none;
&:hover {
width: 90%;
}
&-active {
width: 90%;
}
}
}
}
.miaoImage-mask {
background-color: rgba(0, 0, 0, 0.444);
overflow: hidden;
}
.miaoImage-mask-active {
max-width: 80%;
max-height: 80%;
user-select: none;
}
.miaoImage-mask-btn {
color: #ffffff99;
width: 7%;
position: absolute;
cursor: pointer;
&-back {
left: 1%;
}
&-forward {
right: 1%;
}
}
}
</style>
-25
View File
@@ -1,25 +0,0 @@
import { registerComponentOption } from '@/class/PluginCenter'
import DocumentText from '@vicons/ionicons5/es/DocumentText'
const pluginConfig: registerComponentOption = {
key: 'miaoMarkdown',
name: 'markdown浏览',
icon: DocumentText,
getComponent: async () =>
(await import('@/plugins/miaoMarkdown/miaoMarkdown.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('md')
}
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
-67
View File
@@ -1,67 +0,0 @@
<template>
<n-scrollbar>
<div class="miao-markdown-container-miaomiaomiao" v-html="renderedMarkdown"></div>
<div style="height: 50px; width: 100%;"></div>
</n-scrollbar>
</template>
<script setup lang="ts">
import { NScrollbar } from 'naive-ui';
import { VirtualFile } from '@/class/VirtualDirectory'
import { onMounted, ref } from 'vue'
import MarkdownIt from 'markdown-it'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi';
const decoder = new TextDecoder('utf-8');
const miaoFetchApi = useMiaoFetchApi()
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const markdownSrc = ref<string>()
const renderedMarkdown = ref<string>('')
const md = new MarkdownIt()
onMounted(async () => {
markdownSrc.value = currentFiles.value[0].url
const text = decoder.decode(await(miaoFetchApi.getFile(currentFiles.value[0])).response)
renderedMarkdown.value = md.render(text)
})
</script>
<style lang="scss">
.miao-markdown-container-miaomiaomiao {
width: 95%;
margin: 20px;
table,
td,
th {
border: 1px solid #ddd;
text-align: left;
}
table {
border-collapse: collapse;
margin-bottom: 20px;
}
th,
td {
padding: 15px;
}
tr:nth-child(even) {
background-color: #f2f2f2
}
th {
background-color: #04AA6D;
color: white;
}
}
</style>
-36
View File
@@ -1,36 +0,0 @@
import { registerComponentOption } from "@/class/PluginCenter";
import { PluginExportType } from "@/types/type";
import Disc from "@vicons/ionicons5/es/Disc";
const pluginConfig: registerComponentOption = {
key: "miaoMusicPlayer",
name: "音乐",
icon: Disc,
getComponent: async () => (await import("./miaoMusicPlayer.vue")).default,
filter: (vDirs, vFiles) => {
if (vDirs.length === 0 && vFiles.length === 0) {
return true;
}
if (vDirs.length !== 0) {
return false;
}
if (vFiles.length === 0) {
return false;
}
for (let i of vFiles) {
for (let s of ["mp3", "flac", "ogg", "mp4"]) {
if (i.name.endsWith(s)) {
return true;
}
}
}
return false;
},
disable: false,
single: true,
priority: 5
};
export default pluginConfig;
export const type = PluginExportType.component;
@@ -1,26 +0,0 @@
import { Plugin, PluginGroup } from "@/class/PluginCenter";
import { PluginExportType } from "@/types/type";
import { shallowRef } from "vue";
import Disc from "@vicons/ionicons5/es/Disc";
const pluginConfig: Plugin = {
name: "音乐",
icon: shallowRef(Disc),
filter: (_, __, views) => {
return views.find("miaoMusicPlayer").length === 0;
},
disable: false,
group: [PluginGroup.mainMenu],
func(_, __, hooks) {
const { getPluginCenter } = hooks;
const PluginCenter = getPluginCenter();
PluginCenter.usePlugin("miaoMusicPlayer", [], []);
},
priority: -1
};
export default pluginConfig;
export const type = PluginExportType.default;
export const key = "musicPlayer-mainMenu";
@@ -1,49 +0,0 @@
import { createApp, Reactive } from "vue";
import aplayerListNew from "../src/components/aplayerListNew.vue";
import apType, { audioType } from "../types/ap";
export type aplayerMiaoOption = {
onDragItemStart?: () => void
onDragItemEnd?: () => void
onPlayVideo?: (element: audioType) => void
onSaveCollection?: (audios: audioType[]) => void
}
/**
* 对aplayer进行一些自定义的风格优化
*/
const useAplayerMiao = (ap: Reactive<apType>, options?: aplayerMiaoOption) => {
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, ...(options ?? {}) });
app.mount(new_list_dom);
ap.on("destroy", () => {
app.unmount();
});
return app;
};
export default useAplayerMiao;
@@ -1,219 +0,0 @@
<template>
<miao-drop-handler @on-virtual-files="handleDropVFiles">
<miao-alert-tip-provider ref="miaoMessageRef">
<div class="miaoMusic-container">
<miao-music-main v-if="ap" :ap="ap" v-model:data="data" :updateLocalData="updateLocalData"
@new-collection="saveCurrPlayList"></miao-music-main>
<miao-mask :show="isAPlayerListShow" style="z-index: 5" @click="() => ap?.list.hide()"></miao-mask>
<div ref="aplayer" style="z-index: 10" />
<miao-mask :show="isSavingCollection" @click="isSavingCollection = false"></miao-mask>
</div>
</miao-alert-tip-provider>
</miao-drop-handler>
</template>
<script setup lang="ts">
import useAplayerMiao from './hooks/aplayerMiao'
import { VirtualFile } from '@/class/VirtualDirectory'
import {
App,
onBeforeMount,
onMounted,
onUnmounted,
reactive,
ref,
toRaw,
watch
} from 'vue'
import './src/APlayer.fix.css'
// @ts-ignore
import APlayer from 'APlayer'
import config from '@/config'
import difference from 'lodash/difference'
import uniq from 'lodash/uniq'
import apType from './types/ap'
import miaoDropHandler from '@/components/miaoDropHandler.vue'
import miaoAlertTipProvider from '@/components/miaoAlertTipProvider.vue'
import usePluginCenter from '@/hooks/usePluginCenter'
import VirtualPage from '@/class/VirtualPage'
import miaoMask from '@/components/miaoMask.vue'
import miaoMusicMain from './src/main.vue'
import collectionType from './types/collection'
import cloneDeep from 'lodash/cloneDeep'
import localForage from 'localforage'
import debounce from 'lodash/debounce'
const pluginCenter = usePluginCenter()
let ap = ref<apType>()
let vueApp: App<Element>
const miaoMessageRef = ref<InstanceType<typeof miaoAlertTipProvider>>()
const aplayer = ref<any>(null)
const isAPlayerListShow = ref(true)
const isSavingCollection = ref(false)
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const props = defineProps<{
view: VirtualPage
}>()
const data = ref<collectionType[]>()
watch(
data,
(_v, oldV) => {
updateLocalData(oldV !== undefined)
},
{
deep: true
}
)
watch(
() => currentFiles.value,
(value, oldValue) => {
const diff = difference(value, oldValue)
if (diff.length > 0) {
const _s = `等${diff.length}个资源`
miaoMessageRef.value?.alertTip(
`已添加 ${diff[0].name}${diff.length !== 1 ? _s : ''}`,
{
type: 'success',
timeout: 3000
}
)
ap?.value?.list.add(getMusicList(diff))
}
}
)
const getMusicList = (mp3VFiles: VirtualFile[]) =>
mp3VFiles.map((v, index) => ({
url: `${config.baseUrl}${v.url}`,
name: v.name,
artist: ' ',
index
}))
const handleDropVFiles = (files: VirtualFile[]) => {
currentFiles.value = uniq([...currentFiles.value, ...files])
}
const saveCurrPlayList = () => {
if (ap.value) {
data.value?.unshift({
id: Math.random(),
name: `新建歌单-${new Date()}`,
intro: 'none',
createTime: new Date(),
coverUrl: '',
audios: cloneDeep(ap.value.list.audios).map((v, i) => ({ ...v, index: i }))
})
miaoMessageRef.value?.alertTip('已创建新的歌单', {
timeout: 1000,
type: 'success'
})
}
}
const initAPlayerMiao = () => {
const aplayerOption = {
container: aplayer.value,
audio: getMusicList(currentFiles.value),
autoplay: true,
volume: 1,
theme: '#60e8a2',
listMaxHeight: '500px'
}
ap.value = reactive<apType>(new APlayer(aplayerOption))
vueApp = useAplayerMiao(ap.value, {
onDragItemStart: () => { },
onDragItemEnd: () => { },
onPlayVideo: (element) => {
pluginCenter.usePlugin(
'miaoVideoPlayer',
[],
[
currentFiles.value.find(
(v) => v.name === element.name
) as VirtualFile
]
)
},
onSaveCollection: saveCurrPlayList
})
ap.value.on('listremove', (...args: any) => {
const { index } = args[0] as { index: number }
const _name = ap?.value?.list.audios[index].name
currentFiles.value = currentFiles.value.filter((v) => v.name !== _name)
})
ap.value.on('play', () => {
const name = ap.value?.list.audios[ap.value.list.index].name as string
props.view.setTitle(name)
})
ap.value.on('listshow', () => {
isAPlayerListShow.value = true
})
ap.value.on('listhide', () => {
isAPlayerListShow.value = false
})
}
const updateLocalData = debounce((log?: boolean) => {
log && miaoMessageRef.value?.alertTip(`保存设置成功`, {
type: 'success',
timeout: 999,
})
localForage.setItem('miaoMusic-storage-musicCollections', toRaw(data.value))
}, 1000, {
'leading': false,
'trailing': true
})
const getLocalData = async () => {
const _data =
(await localForage.getItem('miaoMusic-storage-musicCollections') as collectionType[]) ?? []
_data.forEach(v => v.audios.sort((a, b) => a.index - b.index))
data.value = _data
console.log('data ==>', toRaw(data.value))
}
onBeforeMount(async () => {
await getLocalData()
})
onMounted(async () => {
currentFiles.value = [...currentFiles.value.reverse()]
initAPlayerMiao()
})
onUnmounted(() => {
vueApp.unmount()
ap?.value?.destroy()
})
</script>
<style scoped lang="scss">
.miaoMusic-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #ffffff;
}
</style>
<style lang="scss">
.aplayer {
display: flex;
flex-direction: column-reverse;
position: absolute;
bottom: 10px;
width: -webkit-fill-available;
.aplayer-icon-order,
.aplayer-icon-loop {
display: none !important;
}
}
</style>
@@ -1,730 +0,0 @@
.aplayer {
background: #fff;
font-family: Arial, Helvetica, sans-serif;
margin: 5px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .07), 0 1px 5px 0 rgba(0, 0, 0, .1);
border-radius: 5px;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
line-height: normal;
position: relative
}
.aplayer * {
box-sizing: content-box
}
.aplayer svg {
width: 100%;
height: 100%
}
/* .aplayer svg circle,
.aplayer svg path {
fill: #fff
} */
.aplayer.aplayer-withlist .aplayer-info {
border-bottom: 1px solid #e9e9e9
}
.aplayer.aplayer-withlist .aplayer-list {
display: block
}
.aplayer.aplayer-withlist .aplayer-icon-order,
.aplayer.aplayer-withlist .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon.aplayer-icon-menu {
display: inline
}
.aplayer.aplayer-withlrc .aplayer-pic {
height: 90px;
width: 90px
}
.aplayer.aplayer-withlrc .aplayer-info {
margin-left: 90px;
height: 90px;
padding: 10px 7px 0
}
.aplayer.aplayer-withlrc .aplayer-lrc {
display: block
}
.aplayer.aplayer-narrow {
width: 66px
}
.aplayer.aplayer-narrow .aplayer-info,
.aplayer.aplayer-narrow .aplayer-list {
display: none
}
.aplayer.aplayer-narrow .aplayer-body,
.aplayer.aplayer-narrow .aplayer-pic {
height: 66px;
width: 66px
}
.aplayer.aplayer-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 0;
z-index: 99;
overflow: visible;
max-width: 400px;
box-shadow: none
}
.aplayer.aplayer-fixed .aplayer-list {
margin-bottom: 65px;
border: 1px solid #eee;
border-bottom: none
}
.aplayer.aplayer-fixed .aplayer-body {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 0;
z-index: 99;
background: #fff;
padding-right: 18px;
transition: all .3s ease;
max-width: 400px
}
.aplayer.aplayer-fixed .aplayer-lrc {
display: block;
position: fixed;
bottom: 10px;
left: 0;
right: 0;
margin: 0;
z-index: 98;
pointer-events: none;
text-shadow: -1px -1px 0 #fff
}
.aplayer.aplayer-fixed .aplayer-lrc:after,
.aplayer.aplayer-fixed .aplayer-lrc:before {
display: none
}
.aplayer.aplayer-fixed .aplayer-info {
-webkit-transform: scaleX(1);
transform: scaleX(1);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
transition: all .3s ease;
border-bottom: none;
border-top: 1px solid #e9e9e9
}
.aplayer.aplayer-fixed .aplayer-info .aplayer-music {
width: calc(100% - 105px)
}
.aplayer.aplayer-fixed .aplayer-miniswitcher {
display: block
}
.aplayer.aplayer-fixed.aplayer-narrow .aplayer-info {
display: block;
-webkit-transform: scaleX(0);
transform: scaleX(0)
}
.aplayer.aplayer-fixed.aplayer-narrow .aplayer-body {
width: 66px !important
}
.aplayer.aplayer-fixed.aplayer-narrow .aplayer-miniswitcher .aplayer-icon {
-webkit-transform: rotateY(0);
transform: rotateY(0)
}
.aplayer.aplayer-fixed .aplayer-icon-back,
.aplayer.aplayer-fixed .aplayer-icon-forward,
.aplayer.aplayer-fixed .aplayer-icon-lrc,
.aplayer.aplayer-fixed .aplayer-icon-play {
display: inline-block
}
.aplayer.aplayer-fixed .aplayer-icon-back,
.aplayer.aplayer-fixed .aplayer-icon-forward,
.aplayer.aplayer-fixed .aplayer-icon-menu,
.aplayer.aplayer-fixed .aplayer-icon-play {
position: absolute;
bottom: 27px;
width: 20px;
height: 20px
}
.aplayer.aplayer-fixed .aplayer-icon-back {
right: 75px
}
.aplayer.aplayer-fixed .aplayer-icon-play {
right: 50px
}
.aplayer.aplayer-fixed .aplayer-icon-forward {
right: 25px
}
.aplayer.aplayer-fixed .aplayer-icon-menu {
right: 0
}
.aplayer.aplayer-arrow .aplayer-icon-loop,
.aplayer.aplayer-arrow .aplayer-icon-order,
.aplayer.aplayer-mobile .aplayer-icon-volume-down {
display: none
}
.aplayer.aplayer-loading .aplayer-info .aplayer-controller .aplayer-loading-icon {
display: block
}
.aplayer.aplayer-loading .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played .aplayer-thumb {
-webkit-transform: scale(1);
transform: scale(1)
}
.aplayer .aplayer-body {
position: relative
}
.aplayer .aplayer-icon {
width: 15px;
height: 15px;
border: none;
background-color: transparent;
outline: none;
cursor: pointer;
opacity: .8;
vertical-align: middle;
padding: 0;
font-size: 12px;
margin: 0;
display: inline-block
}
.aplayer .aplayer-icon path {
transition: all .2s ease-in-out
}
.aplayer .aplayer-icon-back,
.aplayer .aplayer-icon-forward,
.aplayer .aplayer-icon-lrc,
.aplayer .aplayer-icon-order,
.aplayer .aplayer-icon-play {
display: none
}
.aplayer .aplayer-icon-lrc-inactivity svg {
opacity: .4
}
.aplayer .aplayer-icon-forward {
-webkit-transform: rotate(180deg);
transform: rotate(180deg)
}
.aplayer .aplayer-lrc-content {
display: none
}
.aplayer .aplayer-pic {
position: relative;
float: left;
height: 66px;
width: 66px;
background-size: cover;
background-position: 50%;
transition: all .3s ease;
cursor: pointer
}
.aplayer .aplayer-pic:hover .aplayer-button {
opacity: 1
}
.aplayer .aplayer-pic .aplayer-button {
position: absolute;
border-radius: 50%;
opacity: .8;
text-shadow: 0 1px 1px rgba(0, 0, 0, .2);
box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
background: rgba(0, 0, 0, .2);
transition: all .1s ease
}
.aplayer .aplayer-pic .aplayer-button path {
fill: #fff
}
.aplayer .aplayer-pic .aplayer-hide {
display: none
}
.aplayer .aplayer-pic .aplayer-play {
width: 26px;
height: 26px;
border: 2px solid #fff;
bottom: 50%;
right: 50%;
margin: 0 -15px -15px 0
}
.aplayer .aplayer-pic .aplayer-play svg {
position: absolute;
top: 3px;
left: 4px;
height: 20px;
width: 20px
}
.aplayer .aplayer-pic .aplayer-pause {
width: 16px;
height: 16px;
border: 2px solid #fff;
bottom: 4px;
right: 4px
}
.aplayer .aplayer-pic .aplayer-pause svg {
position: absolute;
top: 2px;
left: 2px;
height: 12px;
width: 12px
}
.aplayer .aplayer-info {
margin-left: 66px;
padding: 14px 7px 0 10px;
height: 66px;
box-sizing: border-box
}
.aplayer .aplayer-info .aplayer-music {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0 0 13px 5px;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
cursor: default;
padding-bottom: 2px;
height: 20px
}
.aplayer .aplayer-info .aplayer-music .aplayer-title {
font-size: 14px;
user-select: none;
}
.aplayer .aplayer-info .aplayer-music .aplayer-author {
font-size: 12px;
color: #666
}
.aplayer .aplayer-info .aplayer-controller {
position: relative;
display: flex
}
.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap {
margin: 0 0 0 5px;
padding: 4px 0;
cursor: pointer !important;
flex: 1
}
.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap:hover .aplayer-bar .aplayer-played .aplayer-thumb {
-webkit-transform: scale(1);
transform: scale(1)
}
.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar {
position: relative;
height: 2px;
width: 100%;
background: #cdcdcd
}
.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-loaded {
position: absolute;
left: 0;
top: 0;
bottom: 0;
background: #aaa;
height: 2px;
transition: all .5s ease
}
.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played {
position: absolute;
left: 0;
top: 0;
bottom: 0;
height: 2px
}
.aplayer .aplayer-info .aplayer-controller .aplayer-bar-wrap .aplayer-bar .aplayer-played .aplayer-thumb {
position: absolute;
top: 0;
right: 5px;
margin-top: -4px;
margin-right: -10px;
height: 10px;
width: 10px;
border-radius: 50%;
cursor: pointer;
transition: all .3s ease-in-out;
-webkit-transform: scale(0);
transform: scale(0)
}
.aplayer .aplayer-info .aplayer-controller .aplayer-time {
position: relative;
right: 0;
bottom: 4px;
height: 17px;
color: #999;
font-size: 11px;
padding-left: 7px
}
.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-time-inner {
vertical-align: middle
}
.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon {
cursor: pointer;
transition: all .2s ease
}
.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon path {
fill: #666
}
.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon.aplayer-icon-loop {
margin-right: 2px
}
.aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon:hover path {
fill: #000
}
/* .aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon.aplayer-icon-menu,
.aplayer .aplayer-info .aplayer-controller .aplayer-time.aplayer-time-narrow .aplayer-icon-menu,
.aplayer .aplayer-info .aplayer-controller .aplayer-time.aplayer-time-narrow .aplayer-icon-mode {
display: none
} */
.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap {
position: relative;
display: inline-block;
margin-left: 3px;
cursor: pointer !important
}
.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap:hover .aplayer-volume-bar-wrap {
height: 40px
}
.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap {
position: absolute;
bottom: 15px;
right: -3px;
width: 25px;
height: 0;
z-index: 99;
overflow: hidden;
transition: all .2s ease-in-out
}
.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap.aplayer-volume-bar-wrap-active {
height: 40px
}
.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap .aplayer-volume-bar {
position: absolute;
bottom: 0;
right: 10px;
width: 5px;
height: 35px;
background: #aaa;
border-radius: 2.5px;
overflow: hidden
}
.aplayer .aplayer-info .aplayer-controller .aplayer-volume-wrap .aplayer-volume-bar-wrap .aplayer-volume-bar .aplayer-volume {
position: absolute;
bottom: 0;
right: 0;
width: 5px;
transition: all .1s ease
}
.aplayer .aplayer-info .aplayer-controller .aplayer-loading-icon {
display: none
}
.aplayer .aplayer-info .aplayer-controller .aplayer-loading-icon svg {
position: absolute;
-webkit-animation: rotate 1s linear infinite;
animation: rotate 1s linear infinite
}
.aplayer .aplayer-lrc {
display: none;
position: relative;
height: 30px;
text-align: center;
overflow: hidden;
margin: -10px 0 7px
}
.aplayer .aplayer-lrc:before {
top: 0;
height: 10%;
background: linear-gradient(180deg, #fff 0, hsla(0, 0%, 100%, 0));
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#ffffff", endColorstr="#00ffffff", GradientType=0)
}
.aplayer .aplayer-lrc:after,
.aplayer .aplayer-lrc:before {
position: absolute;
z-index: 1;
display: block;
overflow: hidden;
width: 100%;
content: " "
}
.aplayer .aplayer-lrc:after {
bottom: 0;
height: 33%;
background: linear-gradient(180deg, hsla(0, 0%, 100%, 0) 0, hsla(0, 0%, 100%, .8));
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#00ffffff", endColorstr="#ccffffff", GradientType=0)
}
.aplayer .aplayer-lrc p {
font-size: 12px;
color: #666;
line-height: 16px !important;
height: 16px !important;
padding: 0 !important;
margin: 0 !important;
transition: all .5s ease-out;
opacity: .4;
overflow: hidden
}
.aplayer .aplayer-lrc p.aplayer-lrc-current {
opacity: 1;
overflow: visible;
height: auto !important;
min-height: 16px
}
.aplayer .aplayer-lrc.aplayer-lrc-hide {
display: none
}
.aplayer .aplayer-lrc .aplayer-lrc-contents {
width: 100%;
transition: all .5s ease-out;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
cursor: default
}
.aplayer .aplayer-list {
overflow: auto;
transition: all .35s ease;
will-change: height;
/* display: none; */
overflow: hidden
}
.aplayer .aplayer-list.aplayer-list-hide {
max-height: 0 !important
}
.aplayer .aplayer-list ol {
list-style-type: none;
margin: 0;
padding: 0;
overflow-y: auto
}
.aplayer .aplayer-list ol::-webkit-scrollbar {
width: 5px
}
.aplayer .aplayer-list ol::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: #eee
}
.aplayer .aplayer-list ol::-webkit-scrollbar-thumb:hover {
background-color: #ccc
}
.aplayer .aplayer-list ol li {
position: relative;
height: 32px;
line-height: 32px;
padding: 0 15px;
font-size: 12px;
border-top: 1px solid #e9e9e9;
cursor: pointer;
/* transition: all .2s ease; */
transition: background .2s ease;
overflow: hidden;
margin: 0
}
.aplayer .aplayer-list ol li:first-child {
border-top: none
}
/* .aplayer .aplayer-list ol li:hover {
background: #efefef
} */
.aplayer .aplayer-list ol li.aplayer-list-light {
background: #e9e9e9
}
.aplayer .aplayer-list ol li.aplayer-list-light .aplayer-list-cur {
display: inline-block
}
.aplayer .aplayer-list ol li .aplayer-list-cur {
display: none;
width: 3px;
height: 22px;
position: absolute;
left: 0;
top: 5px;
cursor: pointer
}
.aplayer .aplayer-list ol li .aplayer-list-index {
color: #666;
margin-right: 12px;
cursor: pointer
}
.aplayer .aplayer-list ol li .aplayer-list-author {
color: #666;
float: right;
cursor: pointer
}
.aplayer .aplayer-notice {
opacity: 0;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
font-size: 12px;
border-radius: 4px;
padding: 5px 10px;
transition: all .3s ease-in-out;
overflow: hidden;
color: #fff;
pointer-events: none;
background-color: #f4f4f5;
color: #909399
}
.aplayer .aplayer-miniswitcher {
display: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
height: 100%;
background: #e6e6e6;
width: 18px;
border-radius: 0 2px 2px 0
}
.aplayer .aplayer-miniswitcher .aplayer-icon {
height: 100%;
width: 100%;
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
transition: all .3s ease
}
.aplayer .aplayer-miniswitcher .aplayer-icon path {
fill: #666
}
.aplayer .aplayer-miniswitcher .aplayer-icon:hover path {
fill: #000
}
@-webkit-keyframes aplayer-roll {
0% {
left: 0
}
to {
left: -100%
}
}
@keyframes aplayer-roll {
0% {
left: 0
}
to {
left: -100%
}
}
@-webkit-keyframes rotate {
0% {
-webkit-transform: rotate(0);
transform: rotate(0)
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
@keyframes rotate {
0% {
-webkit-transform: rotate(0);
transform: rotate(0)
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
@@ -1,301 +0,0 @@
<template>
<div class="collection-info-container">
<div class="collection-info-container-top">
<div class="collection-info-container-top-exit" @click="emit('exit')">
<ArrowBackOutline></ArrowBackOutline>
</div>
<div class="collection-info-container-top-title">
<span :tooltip="false" v-if="!isEditing">
{{ collection.name }}
</span>
<n-input v-if="isEditing" v-model:value="collection.name"></n-input>
</div>
<div class="collection-info-container-top-delete" v-if="isEditing" @click="
clickDeleteCount !== 2
? clickDeleteCount++
: emit('deleteSelf')
">
<TrashOutline></TrashOutline>
<span>
{{
clickDeleteCount === 0
? `删除`
: clickDeleteCount === 1
? '确认删除'
: '再次确认'
}}
</span>
</div>
</div>
<div class="collection-info-container-controller">
<div class="collection-info-container-controller-play" @click="emit('playAll', collection)">
<div class="collection-info-container-controller-play-icon">
<CaretForwardOutline></CaretForwardOutline>
</div>
<div class="collection-info-container-controller-play-text">
{{ `全部播放` }}
</div>
</div>
<div class="collection-info-container-controller-right">
<div class="collection-info-container-controller-edit" @click="isEditing = !isEditing">
<component :is="isEditing ? CheckmarkOutline : CreateOutline
"></component>
</div>
</div>
</div>
<NScrollbar>
<div class="collection-info-container-audioList">
<VueDraggable v-model="collection.audios" :animation="150"
handle=".collection-info-container-audioList-item-controller-item-drag" ghostClass="iSDragging"
@end="updateAudiosIndex">
<div v-for="(audio, index) in collection.audios" class="collection-info-container-audioList-item">
<div class="collection-info-container-audioList-item-index">
<div>{{ index + 1 }}</div>
</div>
<div class="collection-info-container-audioList-item-info">
<div v-if="!isEditing" class="collection-info-container-audioList-item-info-name">
<NEllipsis :tooltip="false">
{{ audio.name }}
</NEllipsis>
</div>
<n-input v-model:value="audio.name" v-if="isEditing"></n-input>
</div>
<div class="collection-info-container-audioList-item-controller">
<PlayOutline class="collection-info-container-audioList-item-controller-item"
v-if="!isEditing" @click="
emit('playAll', collection, index)
"></PlayOutline>
<TrashOutline class="collection-info-container-audioList-item-controller-item"
v-if="isEditing" @click="handleDelete(index)"></TrashOutline>
<ReorderFourOutline v-if="isEditing"
class="collection-info-container-audioList-item-controller-item collection-info-container-audioList-item-controller-item-drag">
</ReorderFourOutline>
</div>
</div>
</VueDraggable>
</div>
</NScrollbar>
</div>
</template>
<script setup lang="ts">
import { NScrollbar, NEllipsis, NInput } from 'naive-ui'
import { VueDraggable } from 'vue-draggable-plus'
import {
ArrowBackOutline,
CaretForwardOutline,
ReorderFourOutline,
PlayOutline,
CreateOutline,
CheckmarkOutline,
TrashOutline
} from '@vicons/ionicons5'
import collectionType from '../types/collection'
import { ref, watch } from 'vue'
const props = defineProps<{
collection: collectionType
}>()
const isEditing = ref(false)
const clickDeleteCount = ref(0)
watch(isEditing, (v) => v && (clickDeleteCount.value = 0))
const handleDelete = (index: number) => {
props.collection.audios.splice(index, 1)
}
const updateAudiosIndex = () => {
props.collection.audios = props.collection.audios.map((v, i) => ({ ...v, index: i }))
}
const emit = defineEmits<{
exit: []
deleteSelf: []
playAll: [collection: collectionType, index?: number]
}>()
</script>
<style lang="scss" scoped>
$ig: linear-gradient(90deg, #a1ee5e 0%, #00e3ae 100%);
.collection-info-container {
width: 100%;
height: 100%;
background-color: #ffffff;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
&-top {
height: 70px;
padding: 0 15px;
background: $ig;
display: flex;
align-items: center;
&-exit {
width: 30px;
aspect-ratio: 1;
color: #063b2a;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
svg {
width: 25px;
aspect-ratio: 1;
transition: width 0.25s ease;
}
&:hover {
svg {
width: 30px;
}
}
}
&-title {
display: flex;
margin-left: 10px;
&-confirm {
margin-left: 10px;
width: 32px;
// color: #ffffff;
}
span {
user-select: none;
font-size: larger;
}
}
&-delete {
display: flex;
height: 25px;
position: absolute;
right: 28px;
color: red;
user-select: none;
cursor: pointer;
&>span {
position: relative;
top: 3px;
margin-left: 3px;
}
}
}
&-controller {
height: 50px;
padding: 0 15px;
display: flex;
align-items: center;
&-play {
display: flex;
align-items: center;
&>div {
cursor: pointer;
}
&-icon {
height: 30px;
width: 45px;
background: $ig;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 30px;
height: 25px;
color: aliceblue;
}
}
&-text {
user-select: none;
margin-left: 10px;
font-size: larger;
}
}
&-right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
&>div {
margin: 0 10px;
cursor: pointer;
height: 25px;
width: 25px;
color: #002911;
}
}
}
&-audioList {
flex: 1;
color: #002911;
padding-bottom: 88px;
&-item {
// padding: 0 20px;
padding-right: 7px;
display: flex;
height: 70px;
align-items: center;
&-index {
width: 50px;
user-select: none;
font-size: 24px;
text-align: right;
&>div {
margin: auto;
width: fit-content;
}
}
&-info {
flex: 1;
display: flex;
user-select: none;
overflow: hidden;
&-name {
overflow: hidden;
}
}
&-controller {
display: flex;
align-items: center;
&-item {
width: 30px;
cursor: pointer;
margin: 0 5px;
}
}
}
}
}
.isDragging {
opacity: 0.5;
}
</style>
@@ -1,449 +0,0 @@
<template>
<div class="aplayer-list-top">
<div @click="loopModeToggle" class="aplayer-list-top-loopMode">
<component
:is="loopModeMap[loopMode].icon"
class="aplayer-list-top-loopMode-icon"></component>
<span class="aplayer-list-top-loopMode-name">
{{ loopModeMap[loopMode].name }}
</span>
</div>
<div class="aplayer-list-top-buttons">
<AddCircleOutline
v-if="onSaveCollection"
class="aplayer-list-top-buttons-icon"
@click="onSaveCollection(ap.list.audios)"></AddCircleOutline>
</div>
<span></span>
</div>
<ol
:style="{ maxHeight: `calc( ${ap.options.listMaxHeight} - 30px )` }"
:class="[
!isDragging ? 'miaoMusic-ol-isDragging' : 'miaoMusic-ol-notDragging'
]"
:key="refreshKey"
ref="ol">
<VueDraggable
v-model="ap.list.audios"
@start="onDragStart"
@end="onDragEnd"
item-key="id"
handle=".aplayer-list-control-btn-reorder"
ghostClass="iSDragging"
:animation="150">
<!-- <miaoLazyDiv :min-height="'32px'" auto-height> -->
<li
v-for="(element, index) in ap.list.audios"
:class="[activeAudioIndex == index ? 'aplayer-list-light' : '']"
class="aplayer-list-new"
:key="element.id">
<span
class="aplayer-list-cur"
:style="{ backgroundColor: apTheme }"></span>
<span class="aplayer-list-index">{{ index + 1 }}</span>
<span class="aplayer-list-title">{{ element.name }}</span>
<div class="aplayer-list-control">
<div
v-if="isVideo(element)"
class="aplayer-list-control-btn aplayer-list-control-btn-hover aplayer-list-control-btn-play"
@click.stop="onPlayVideo && onPlayVideo(element)">
<VideocamOutline class="aplayer-list-control-btn-svg" />
</div>
<div
class="aplayer-list-control-btn aplayer-list-control-btn-hover aplayer-list-control-btn-play"
@click.stop="switchAudio(index)">
<component
:is="
activeAudioIndex == index && !isPaused
? PauseOutline
: PlayOutline
"
class="aplayer-list-control-btn-svg"></component>
</div>
<div
class="aplayer-list-control-btn aplayer-list-control-btn-hover"
@click.stop="switchAudio(index)"
v-if="false">
<HeartOutline class="aplayer-list-control-btn-svg" />
</div>
<div
class="aplayer-list-control-btn"
@click.stop="listAudioUp(index)"
v-if="false">
<ChevronUpOutline
class="aplayer-list-control-btn-svg" />
</div>
<div
class="aplayer-list-control-btn"
@click.stop="listAudioDown(index)"
v-if="false">
<ChevronDownOutline
class="aplayer-list-control-btn-svg" />
</div>
<div
class="aplayer-list-control-btn aplayer-list-control-btn-hover"
@click.stop="listAudioRemove(index)">
<CloseOutline class="aplayer-list-control-btn-svg" />
</div>
<div
class="aplayer-list-control-btn aplayer-list-control-btn-reorder">
<ReorderFourOutline
class="aplayer-list-control-btn-svg" />
</div>
</div>
<span
class="aplayer-list-author"
v-show="element.artist !== 'Audio artist1'">
{{ element.artist }}
</span>
</li>
<!-- </miaoLazyDiv> -->
</VueDraggable>
</ol>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref } from 'vue'
import {
ChevronUpOutline,
ChevronDownOutline,
CloseOutline,
ReorderFourOutline,
PlayOutline,
HeartOutline,
VideocamOutline,
PauseOutline,
ListCircleOutline,
ListOutline,
HeadsetOutline,
LogoOctocat,
AddCircleOutline
} from '@vicons/ionicons5'
import { VueDraggable } from 'vue-draggable-plus'
// 与vue-draggable冲突
// import miaoLazyDiv from '@/components/miaoLazyDiv.vue';
import apType, { audioType } from '../../types/ap'
import { aplayerMiaoOption } from '../../hooks/aplayerMiao'
const props = defineProps<
{
ap: apType
} & aplayerMiaoOption
>()
const apRef = ref(props.ap)
const ol = ref<HTMLOListElement>()
const refreshKey = ref(Math.random())
const isDragging = ref(false)
const apTheme = computed(
() =>
props.ap.list.audios[activeAudioIndex.value].theme ??
props.ap.options.theme
)
const activeAudioIndex = computed(() => props.ap.list.index)
const isPaused = ref(false)
props.ap.on('play', () => {
nextTick(() => {
isPaused.value = false
})
})
props.ap.on('pause', () => {
isPaused.value = true
})
props.ap.on('listswitch', (...args: any) => {
// 手动触发更新,不然监听不到
props.ap.list.index = args[0].index as number
})
const refresh = () => {
const top = ol.value?.scrollTop
refreshKey.value = Math.random()
nextTick(() => {
ol.value?.scrollTo({
top
})
})
}
props.ap.on('listremove', refresh)
props.ap.on('listadd', refresh)
const switchAudio = (index: number) => {
if (index === activeAudioIndex.value) {
if (props.ap.paused) {
props.ap.play()
} else {
props.ap.pause()
}
props.ap.paused = props.ap.paused
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)
}
const { onDragStart, onDragEnd } = (() => {
let curr: audioType
return {
onDragStart: () => {
isDragging.value = true
curr = props.ap.list.audios[props.ap.list.index]
},
onDragEnd: () => {
listRefreshIndex(curr)
nextTick(() => {
isDragging.value = false
})
}
}
})()
const isVideo = (element: audioType) => {
for (let end of ['mp4']) {
if (element.name.endsWith(end)) {
return true
}
}
return false
}
const loopModeMap = {
0: { name: '列表循环', icon: ListCircleOutline },
1: { name: '单曲循环', icon: HeadsetOutline },
2: { name: '列表播放', icon: ListOutline },
3: { name: '随机播放', icon: LogoOctocat }
}
const loopMode = computed(() => {
if (apRef.value.options.order === 'list') {
switch (apRef.value.options.loop) {
case 'all':
return 0
case 'one':
return 1
case 'none':
return 2
}
}
return 3
})
const loopModeToggle = () => {
const ops = apRef.value.options
if (ops.order === 'random') {
ops.order = 'list'
} else {
switch (ops.loop) {
case 'all': {
ops.loop = 'one'
break
}
case 'one': {
ops.loop = 'none'
break
}
case 'none': {
ops.loop = 'all'
ops.order = 'random'
break
}
}
}
}
onMounted(() => {
console.log(props.ap)
})
</script>
<style lang="scss" scoped>
.aplayer-list-new {
height: fit-content !important;
cursor: default !important;
.aplayer-list-cur {
height: calc(100% - 8px) !important;
}
.aplayer-list-index {
display: inline-block;
width: 20px;
margin-right: 4px !important;
cursor: default !important;
}
}
.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;
cursor: pointer;
&-svg {
width: 20px;
aspect-ratio: 1;
transition: all 0.1s ease;
}
}
&-btn-hover {
.aplayer-list-control-btn-svg {
&:hover {
width: 24px;
}
}
}
}
.aplayer-list-top {
height: 30px;
// width: auto;
// max-width: 100%;
position: relative;
border-bottom: 1px solid #e9e9e9;
display: flex;
justify-items: baseline;
align-items: center;
padding: 0 10px;
.aplayer-list-top-loopMode {
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
float: left;
// background-color: #f9f9f9;
scale: 1;
transition: scale 0.15s ease;
&-icon {
width: 20px;
aspect-ratio: 1;
transform: translateY(1px);
fill: #000;
path {
fill: #000 !important;
}
}
&-name {
margin-left: 5px;
}
&:hover {
scale: 1.1;
}
}
.aplayer-list-top-buttons {
display: inline-flex;
position: absolute;
right: 10px;
align-items: center;
justify-content: center;
scale: 1;
transition: scale 0.15s ease;
&-icon {
width: 20px;
aspect-ratio: 1;
cursor: pointer;
}
&:hover {
scale: 1.1;
}
}
}
</style>
<style lang="scss">
.aplayer svg circle,
.aplayer .aplayer-list-new svg path {
fill: #ffffff00;
}
.iSDragging {
opacity: 0.5;
// background: #60e8a2 !important;
}
.aplayer-list-light {
background: unset !important;
.aplayer-list-title {
color: #60e8a2;
}
}
.aplayer .aplayer-list .miaoMusic-ol-isDragging li:hover {
background: #efefef !important;
}
.miaoMusicDrag-move,
.miaoMusicDrag-enter-active,
.miaoMusicDrag-leave-active {
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}
.miaoMusicDrag-enter-from,
.miaoMusicDrag-leave-to {
opacity: 0;
transform: scaleY(0.01) translate(30px, 0);
}
.miaoMusicDrag-leave-active {
position: absolute;
}
</style>
@@ -1,123 +0,0 @@
<template>
<div class="collection" @click="emit('onenter')">
<div class="collection-cover" @click.stop="emit('onPlay')">
<img
v-if="item.coverUrl"
class="collection-cover-img"
:src="item.coverUrl" />
<div class="collection-cover-play">
<Play></Play>
</div>
</div>
<div class="collection-info">
<div class="collection-info-name">
<span class="collection-info-name-ellipsis">
<span>
{{ item.name }}
</span>
</span>
</div>
<div class="collection-info-stats">
<span class="collection-info-stats-time">{{ dateStr }}</span>
<span class="collection-info-stats-count">
{{ `共${item.audios.length}首` }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import collectionType from '../../types/collection'
import dateFormatter from '@/hooks/dateFormatter'
import { Play } from '@vicons/ionicons5'
const props = defineProps<{
item: collectionType
}>()
const emit = defineEmits<{
onPlay: []
onenter: []
}>()
const dateStr = computed(() =>
dateFormatter(new Date(props.item.createTime), 'yyyy-MM-dd')
)
</script>
<style lang="scss" scoped>
.collection {
height: 50px;
position: relative;
display: flex;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
border-radius: 5px;
overflow: hidden;
transition: height 0.25s ease;
cursor: pointer;
user-select: none;
&:not(&:last-child) {
margin-bottom: 15px;
}
.collection-cover {
width: 50px;
height: 50px;
aspect-ratio: 1;
background-color: #60e8a2;
margin-right: 5px;
.collection-cover-play {
width: 25px;
height: 15px;
background-color: #d1d1d1b2;
position: absolute;
bottom: 5px;
left: 5px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 3px;
svg {
height: 15px;
color: #fff;
}
}
}
.collection-info {
position: relative;
width: calc(100% - 60px);
display: flex;
flex-direction: column;
justify-content: center;
.collection-info-name {
max-width: 100%;
overflow: hidden;
&-ellipsis {
overflow: hidden;
white-space: nowrap;
display: inline-block;
vertical-align: bottom;
max-width: 100%;
text-overflow: ellipsis;
span {
white-space: nowrap;
text-wrap: nowrap;
text-overflow: ellipsis;
}
}
}
.collection-info-stats {
text-wrap: nowrap;
span {
font-size: smaller;
}
.collection-info-stats-time {
margin-right: 10px;
}
}
}
}
</style>
-133
View File
@@ -1,133 +0,0 @@
<template>
<div class="miaoMusic-main">
<NScrollbar>
<div class="miaoMusic-main-collectionsList">
<div class="miaoMusic-main-collectionsList-title">自建歌单</div>
<VueDraggable
v-if="collections"
v-model="collections"
:animation="150"
:delay="100"
ghostClass="iSDragging">
<collection
v-for="item in collections"
:item="item"
@on-play="handleCollectionsPlay(item)"
@onenter="handleCollectionEnter(item)"></collection>
</VueDraggable>
<n-empty
description="空空如也~"
size="huge"
style="margin-top: 200px"
v-if="!data || data?.length === 0">
<template #icon>
<n-icon>
<HandLeftOutline />
</n-icon>
</template>
<template #extra>
<n-button
size="small"
type="primary"
@click="emit('newCollection')">
立刻创建
</n-button>
</template>
</n-empty>
</div>
</NScrollbar>
<Transition name="slide-fade">
<collectionInfo
class="miaoMusic-main-collection"
:collection="currCollection"
v-if="currCollection"
@exit="handleCollectionEnter(undefined)"
@delete-self="handleCollectionDelete()"
@play-all="handleCollectionsPlay"></collectionInfo>
</Transition>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import apType from '../types/ap'
import collection from './components/collection.vue'
import collectionInfo from './collectionInfo.vue'
import collectionType from '../types/collection'
import { VueDraggable } from 'vue-draggable-plus'
import { NScrollbar, NEmpty, NButton } from 'naive-ui'
import { HandLeftOutline } from '@vicons/ionicons5'
const props = defineProps<{
ap: apType
}>()
const emit = defineEmits<{
newCollection: []
}>()
const collections = defineModel<collectionType[]>('data')
const currCollection = ref<collectionType>()
const playCollection = (collection: collectionType, index?: number) => {
const ap = props.ap
console.log({ collection, ap })
// clear有bug
while (ap.list.audios.length !== 0) {
ap.list.remove(0)
}
ap.list.add(collection.audios)
index && ap.list.switch(index)
ap.play()
}
const handleCollectionsPlay = (item: collectionType, index?: number) => {
playCollection(item, index)
}
const handleCollectionEnter = (item: collectionType | undefined) => {
currCollection.value = item
}
const handleCollectionDelete = () => {
collections.value &&
collections.value.splice(
collections.value.findIndex((v) => v === currCollection.value),
1
)
currCollection.value = undefined
}
</script>
<style lang="scss" scoped>
.miaoMusic-main {
position: relative;
max-height: 100%;
height: 100%;
.miaoMusic-main-collectionsList {
padding: 15px 20px 88px;
&-title {
font-size: larger;
letter-spacing: 1px;
margin-bottom: 10px;
user-select: none;
}
}
}
.slide-fade-enter-active {
transition: all 0.15s ease-out;
}
.slide-fade-leave-active {
transition: all 0.15s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(200px);
opacity: 0;
}
.isDragging {
opacity: 0.5;
}
</style>
-45
View File
@@ -1,45 +0,0 @@
export type audioType = {
url: string
artist?: string
cover?: string
name: string
theme?: string
id?: number
index: number
}
type apType = {
paused: boolean
list: {
audios: audioType[]
index: number
switch: (index: number) => void
remove: (index: number) => void
add: (audios: audioType[]) => void
refreshAudioIndex: () => void
hide: () => void
clear: () => 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' | 'one' | 'none'
order: 'list' | 'random'
listMaxHeight: string
storageName: 'aplayer-setting'
}
destroy: () => {}
pause: () => void
play: () => void
}
export default apType
@@ -1,14 +0,0 @@
import type { audioType } from "./ap";
export type collectionItemType = {} & audioType;
export type collectionType = {
id: number;
name: string;
intro: string;
createTime: Date;
coverUrl?: string;
audios: collectionItemType[];
};
export default collectionType;
-25
View File
@@ -1,25 +0,0 @@
import { registerComponentOption } from '@/class/PluginCenter'
import DocumentText from '@vicons/ionicons5/es/DocumentText'
const pluginConfig: registerComponentOption = {
key: 'miaoPDF',
name: 'PDF浏览',
icon: DocumentText,
getComponent: async () =>
(await import('@/plugins/miaoPDF/miaoPDF.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('pdf')
}
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
-201
View File
@@ -1,201 +0,0 @@
<template>
<miao-message-provider ref="miaoMessageRef">
<div class="miao-container">
<div class="miao-container-topBar">
<div
class="miao-container-topBar-btn"
@click="() => handleZoom(10)"
title="快速缩小">
--
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleZoom()"
title="缩小">
-
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleResetMargin()"
title="还原">
还原缩放
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleShrink()"
title="放大">
+
</div>
<div
class="miao-container-topBar-btn"
@click="() => handleShrink(10)"
title="快速放大">
++
</div>
</div>
<NScrollbar>
<div
class="miao-container-pdf"
:style="{
alignItems: pdfMargin < 0 ? 'baseline' : 'center'
}">
<div
ref="pdfEl"
class="miao-container-pdf-content"
:style="{
width: `calc( 100% - ${pdfMargin}px)`
}"></div>
</div>
</NScrollbar>
</div>
</miao-message-provider>
</template>
<script setup lang="ts">
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
import { onMounted, ref, shallowReactive } from 'vue'
import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?worker'
import { VirtualFile } from '@/class/VirtualDirectory'
import { NScrollbar } from 'naive-ui'
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const pdfEl = ref<HTMLDivElement>()
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
// 通过margin来实现缩小功能
const pdfMargin = ref<number>(0)
const pdfPageCount = ref<number>(-1)
const canvasList = shallowReactive<HTMLCanvasElement[]>([])
const handleZoom = (index?: number) => {
const _index = index ?? 1
pdfMargin.value += 10 * _index
}
const handleShrink = (index?: number) => {
const _index = index ?? 1
// if (pdfMargin.value - 10 * _index <= 0) {
// return pdfMargin.value = 0
// }
pdfMargin.value -= 10 * _index
}
const handleResetMargin = () => {
pdfMargin.value = 0
}
onMounted(async () => {
const setLoadingMessage = miaoMessageRef.value?.alertTip('加载PDF插件中', {
type: 'info',
timeout: -1
})
const pdfSrc = currentFiles.value[0].url
// @ts-ignore
const PDFJS = await import('pdfjs-dist/build/pdf.mjs')
if (typeof window !== 'undefined' && 'Worker' in window) {
PDFJS.GlobalWorkerOptions.workerPort = new PdfjsWorker()
}
// 加载文档
let loadingTask = PDFJS.getDocument({ url: pdfSrc })
loadingTask.__PDFDocumentLoadingTask = true
const pdf = await loadingTask.promise // 使用await等待加载完毕
pdfPageCount.value = pdf.numPages
// 循环渲染每一页
for (let i = 1; i <= pdf.numPages; i++) {
setLoadingMessage && setLoadingMessage(`渲染进度:${i}/${pdf.numPages}`)
const page = await pdf.getPage(i)
let pixelRatio = 3
let viewport = page.getViewport({ scale: 1 })
// 使用canvas渲染
let canvas = window.document.createElement('canvas')
canvasList.push(canvas)
// @ts-ignore
pdfEl.value.appendChild(canvas)
canvas.width = viewport.width * pixelRatio // 计算宽度
canvas.height = viewport.height * pixelRatio
let renderContext = {
canvasContext: canvas.getContext('2d'),
viewport: viewport,
transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
}
await page.render(renderContext).promise // 一页一页的渲染
}
setLoadingMessage &&
setLoadingMessage(`渲染PDF完成`, {
type: 'success',
timeout: 3456
})
})
</script>
<style scoped lang="scss">
.miao-container {
width: 100%;
height: 100%;
background-color: rgb(201, 201, 201);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
.miao-container-topBar {
box-sizing: border-box;
padding: 0 30px;
height: 30px;
width: 100%;
background-color: rgb(170, 176, 182);
display: flex;
justify-content: space-around;
align-items: center;
&-btn {
cursor: pointer;
user-select: none;
height: 80%;
padding: 0 5px;
display: flex;
align-items: center;
justify-content: center;
font-size: larger;
letter-spacing: 1px;
color: black;
border-radius: 5px;
&:hover {
background-color: #eaeaea42;
}
}
}
.miao-container-pdf {
min-width: 0;
width: 100%;
display: flex;
flex-direction: column;
.miao-container-pdf-content {
display: flex;
flex-direction: column;
}
}
.miao-container-loading {
position: absolute;
top: 40px;
background-color: aliceblue;
border: 1px solid black;
height: 40px;
width: 200px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 15px;
letter-spacing: 5px;
user-select: none;
}
}
</style>
-26
View File
@@ -1,26 +0,0 @@
// @ts-nocheck
import { registerComponentOption } from '@/class/PluginCenter'
const pluginConfig: registerComponentOption = {
key: 'yourPluginKey',
name: 'yourPluginName',
// icon: LogoChrome,
getComponent: async () => null,
// (await import('yourPluginPath')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('?')
},
disable: true
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
@@ -1,19 +0,0 @@
<template>
<div class="template-container"></div>
</template>
<script setup lang="ts">
// import { VirtualFile } from '@/class/VirtualDirectory'
// const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
// required: true
// })
</script>
<style scoped lang="scss">
.template-container {
width: 100%;
height: 100%;
}
</style>
-33
View File
@@ -1,33 +0,0 @@
import { registerComponentOption } from '@/class/PluginCenter'
import PlayCircle from '@vicons/ionicons5/es/PlayCircle'
const pluginConfig: registerComponentOption = {
key: 'miaoVideoPlayer',
name: '视频',
icon: PlayCircle,
getComponent: async () =>
(await import('@/plugins/miaoVideoPlayer/miaoVideoPlayer.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length === 0) {
return false
}
for (let i of vFiles) {
for (let s of ['mp4', 'mkv']) {
if (i.name.endsWith(s)) {
return true
}
}
}
return false
},
priority: 6
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
@@ -1,356 +0,0 @@
<template>
<miao-drop-handler @on-virtual-files="handleDropVFiles">
<div class="player-container">
<div ref="xgPlayer" id="xgplayer"></div>
</div>
</miao-drop-handler>
</template>
<script lang="ts" setup>
import { VirtualFile } from '@/class/VirtualDirectory'
import miaoDropHandler from '@/components/miaoDropHandler.vue'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import XGPlayer from 'xgplayer'
import './style/xgplayer.scss'
import TextTrack from 'xgplayer/es/plugins/track'
import 'xgplayer/es/plugins/track/index.css'
import axios from 'axios'
import uniq from 'lodash/uniq'
import lockScreen from './playerPlugin/lockScreen/lockScreen'
import xgplayerVueApp from './src/XGPlayerVuePluginRoot.vue'
import useXGPlayerVueFrame from './playerPlugin/xgplayer-vue-frame/xgplayer-vue-frame'
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const xgPlayer = ref()
let _player: XGPlayer
const { setVideo } = (() => {
const initPlayer = (videos: VirtualFile[], texttrack: {
list: {
id: number; url: string
text: string
}[]
}) => {
const videoVFile = videos.shift() as VirtualFile
_player && _player.destroy()
_player = new XGPlayer({
plugins: [
TextTrack,
lockScreen,
useXGPlayerVueFrame(xgplayerVueApp),
],
el: xgPlayer.value,
url: videoVFile.url,
width: '100%',
height: '100%',
volume: 1,
// loop: true,
// pip: true,
videoFillMode: 'contain',
autoplay: true,
download: true,
ignores: [videos.length === 0 ? 'playNext' : undefined].filter(v => v) as string[],
lang: 'zh-cn',
playbackRate: [0.1, 0.3, 0.5, 0.75, 1, 1.5, 2, 3, 5, 10],
texttrack,
screenShot: {
saveImg: true,
quality: 1,
type: 'image/png',
format: '.png'
},
playNext: {
urlList: videos.map(v => v.url)
},
progress: {
isDragingSeek: true
},
// mobile: {
// disableGesture: true
// }
})
}
return {
setVideo: async (
videoVFiles: VirtualFile[],
textTrackFiles?: VirtualFile[]
) => {
// 天哪什么精神状态会写出这种代码
textTrackFiles &&
textTrackFiles.push(...findTextTrack(currentFiles.value))
textTrackFiles = uniq(textTrackFiles)
currentFiles.value = uniq([...currentFiles.value, ...videoVFiles, ...(textTrackFiles ?? [])])
const videos = findVideo(uniq([...currentFiles.value, ...videoVFiles]))
const _list = []
if (textTrackFiles) {
for (let i of textTrackFiles) {
if (i.name.endsWith('srt')) {
const response = await axios.get(i.url)
// console.log(srtToVtt(response.data))
const url = setUrl(new Blob([srtToVtt(response.data)]))
_list.push({
id: i.id,
url,
// 这个language会覆盖id的设置,导致无法切换字幕
// language: 'zh-cn',
text: i.name
})
} else if (i.name.endsWith('ass')) {
const response = await axios.get(i.url)
// console.log(assToVtt(response.data))
const url = setUrl(new Blob([assToVtt(response.data)]))
_list.push({
id: i.id,
url,
text: i.name
})
} else if (i.name.endsWith('ssa')) {
const response = await axios.get(i.url)
// console.log(ssaToVtt(response.data))
const url = setUrl(new Blob([ssaToVtt(response.data)]))
_list.push({
id: i.id,
url,
text: i.name
})
} else {
_list.push({
id: i.id,
url: i.url,
text: i.name
})
}
}
}
const texttrack = {
list: _list
}
initPlayer([...videos], texttrack)
}
}
})()
const { setUrl, destroyUrl } = (() => {
const urlList: string[] = []
return {
setUrl(blob: Blob) {
const url = URL.createObjectURL(blob)
urlList.push(url)
return url
},
destroyUrl() {
for (const url of urlList) {
URL.revokeObjectURL(url)
}
}
}
})()
const handleDropVFiles = (files: VirtualFile[]) => {
setVideo(findVideo(files.reverse()), findTextTrack(files))
}
function srtToVtt(srt: string) {
// 将 SRT 字幕按行分割
const lines = srt.split('\n')
let vtt = 'WEBVTT\n\n' // VTT 文件的开头
let index = 0
while (index < lines.length) {
// 跳过空行
if (lines[index].trim() === '') {
index++
continue
}
// SRT 中的序号
lines[index].trim()
index++
// 时间戳
const timeLine = lines[index].trim().replace(/,/g, '.') // 将逗号替换为点
index++
// 字幕文本
let text = ''
while (index < lines.length && lines[index].trim() !== '') {
text += lines[index].trim() + '\n'
index++
}
// 添加到 VTT 字幕中
vtt += `${timeLine}\n${text.trim()}\n\n`
}
return vtt
}
function assToVtt(assContent: string) {
// 分割 ASS 内容为行
const lines = assContent.split('\n')
let vttContent = 'WEBVTT\n\n' // VTT 文件头
// 遍历每一行,寻找字幕条目
for (let line of lines) {
// 只处理 [Events] 部分后的 Dialogue 行
if (line.startsWith('Dialogue:')) {
// 提取时间戳和文本
const parts = line.split(',')
if (parts.length >= 5) {
// 将时间戳格式化为 VTT 格式
const startTime = parts[1]
.trim()
.replace('.', ',')
.replace(',', '.') // 替换 . 为 ,,然后再替换为 .
const endTime = parts[2]
.trim()
.replace('.', ',')
.replace(',', '.') // 替换 . 为 ,,然后再替换为 .
// 确保时间戳是两位数格式
const formattedStartTime = startTime.replace(
/(\d+):(\d+):(\d+)\.(\d+)/,
(match: any, h: any, m: any, s: any, ms: any) => {
match
return `${String(h).padStart(2, '0')}:${String(
m
).padStart(2, '0')}:${String(s).padStart(
2,
'0'
)}.${String(ms).padStart(3, '0')}`
}
)
const formattedEndTime = endTime.replace(
/(\d+):(\d+):(\d+)\.(\d+)/,
(match: any, h: any, m: any, s: any, ms: any) => {
match
return `${String(h).padStart(2, '0')}:${String(
m
).padStart(2, '0')}:${String(s).padStart(
2,
'0'
)}.${String(ms).padStart(3, '0')}`
}
)
const rawText = parts.slice(9).join(',').trim() // 提取文本
// 清理文本中的样式信息
const cleanText = rawText
.replace(/{[^}]*}/g, '')
.replace(/\\N/g, '\n')
.trim()
// 添加到 VTT 内容
vttContent += `${formattedStartTime} --> ${formattedEndTime}\n${cleanText}\n\n`
}
}
}
return vttContent.trim() // 返回 VTT 内容,去掉末尾空行
}
function ssaToVtt(ssaContent: string): string {
function convertSsaTimeToVtt(ssaTime: string): string {
const timeParts = ssaTime.split(':')
const hours = timeParts[0].padStart(2, '0')
const minutes = timeParts[1].padStart(2, '0')
const secondsAndMilliseconds = timeParts[2].split('.')
const seconds = secondsAndMilliseconds[0].padStart(2, '0')
const milliseconds = (secondsAndMilliseconds[1] || '0').padEnd(3, '0')
return `${hours}:${minutes}:${seconds}.${milliseconds}`
}
const lines = ssaContent.split('\n')
let vttContent = 'WEBVTT\n\n' // VTT文件头
let inEvents = false
for (const line of lines) {
// 检查是否进入[Events]部分
if (line.startsWith('[Events]')) {
inEvents = true
continue
}
// 如果在[Events]部分,处理对话行
if (inEvents && line.startsWith('Dialogue:')) {
const parts = line.split(',')
if (parts.length >= 3) {
const startTime = convertSsaTimeToVtt(parts[1].trim())
const endTime = convertSsaTimeToVtt(parts[2].trim())
const text = parts.slice(9).join(',').trim() // 获取字幕文本
// 去除样式标签
const cleanText = text.replace(/{[^}]*}/g, '').trim()
// 添加到VTT内容
vttContent += `${startTime} --> ${endTime}\n${cleanText}\n\n`
}
}
}
return vttContent
}
const findVideo = (vFiles: VirtualFile[]) =>
uniq(vFiles.filter((v) => {
for (let s of ['mp4', 'mp3', 'mkv']) {
if (v.name.endsWith(s)) {
return true
}
}
return false
}))
const findTextTrack = (vFiles: VirtualFile[]) =>
vFiles.filter((v) => {
for (let s of ['vtt', 'srt', 'ass', 'ssa']) {
if (v.name.endsWith(s)) {
return true
}
}
return false
})
onMounted(() => {
if (currentFiles.value.length) {
currentFiles.value.reverse()
setVideo(
findVideo(currentFiles.value),
findTextTrack(currentFiles.value)
)
}
})
onBeforeUnmount(() => {
_player.destroy()
destroyUrl()
})
</script>
<style scoped lang="scss">
.player-container {
width: 100%;
height: 100%;
position: relative;
background-color: grey;
display: flex;
flex-direction: column;
justify-content: center;
#xgplayer {
width: 100%;
// height: 500px;
background-color: black;
}
}
.video-js {
width: 100%;
background-color: rgb(0, 0, 0);
}
</style>
@@ -1,51 +0,0 @@
.lock-div {
position: absolute;
z-index: 20;
left: 15px;
top: 50%;
transform: translateY(-50%);
width: 30px;
aspect-ratio: 1;
cursor: pointer;
color: #f2f2f2;
opacity: 1;
visibility: visible;
transition: opacity 0.5s ease, visibility 0.5s ease, width 0.25s ease, left 0.25s ease;
pointer-events: all !important;
svg {
width: inherit;
height: inherit;
}
&:hover {
width: 36px;
left: 12px;
}
}
.xgplayer.xgplayer-inactive .lock-div {
pointer-events: none;
cursor: default;
visibility: hidden;
opacity: 0;
}
.overlay-div {
position: absolute;
z-index: 10;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
display: none;
cursor: auto;
}
.xgplayer.xgplayer-inactive .overlay-div {
pointer-events: none;
cursor: none;
}
.xgplayer .xgplayer-locked {
pointer-events: none;
}
@@ -1,104 +0,0 @@
import { BasePlugin } from "xgplayer";
import "./lockScreen.scss";
import MobilePlugin from "xgplayer/es/plugins/mobile";
export default class LockPlugin extends BasePlugin {
static get pluginName() {
return "lockPlugin";
}
private locked: boolean;
private lockDiv: HTMLDivElement = document.createElement("div");
private overlayDiv: HTMLDivElement = document.createElement("div");
private lockOpenSVG: string = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M336 208v-95a80 80 0 0 0-160 0v95" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></path>
<rect x="96" y="208" width="320" height="272" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></rect>
</svg>`;
private lockClosedSVG: string = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M336 112a80 80 0 0 0-160 0v96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></path>
<rect x="96" y="208" width="320" height="272" rx="48" ry="48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></rect>
</svg>`;
constructor(args: any) {
super(args);
this.locked = false;
}
afterCreate() {
this.initLockDiv();
this.initOverlayDiv();
this.bindEvents();
}
private initLockDiv() {
this.lockDiv.innerHTML = this.lockClosedSVG;
this.lockDiv.className = "lock-div";
this.player.root !== null && this.player.root.appendChild(this.lockDiv);
}
private initOverlayDiv() {
this.overlayDiv.className = "overlay-div";
this.player.root !== null && this.player.root.appendChild(this.overlayDiv);
this.overlayDiv.addEventListener('mousedown', (e) => {
e.preventDefault();
});
this.overlayDiv.addEventListener('mousemove', (e) => {
e.preventDefault();
});
}
private bindEvents() {
this.lockDiv!.addEventListener("click", this.toggleLock.bind(this));
this.player.root !== null && this.player.root.addEventListener("click", this.preventClick.bind(this), true);
this.player.usePluginHooks("progress", "dragstart", () => {
if (this.locked) {
return false;
}
return true;
});
// 阻止进度条默认行为
for(const event of ['dragstart', 'drag', 'dragend']) {
this.player.usePluginHooks("progress", event, () => {
if (this.locked) {
return false;
}
return true;
});
}
}
private toggleLock() {
const mobile = this.getPlugin('mobile') as MobilePlugin | undefined
// mobile?.disableGesture()
if (this.lockDiv && this.overlayDiv) {
this.locked = !this.locked;
this.lockDiv.innerHTML = this.locked ? this.lockOpenSVG : this.lockClosedSVG;
this.overlayDiv.style.display = this.locked ? "block" : "none";
this.player.controls && (this.locked ? this.player.controls.hide() : this.player.controls.show());
this.player.root !== null && this.player.root.classList.toggle("xgplayer-locked", this.locked);
this.locked ? mobile?.disableGesture() : mobile?.enableGesture()
}
}
private preventClick(event: MouseEvent) {
if (this.locked && event.target instanceof HTMLElement && !event.target.classList.contains("lock-div")) {
event.stopPropagation();
event.preventDefault();
}
}
destroy() {
if (this.lockDiv) {
this.lockDiv.removeEventListener("click", this.toggleLock.bind(this));
this.player.root !== null && this.player.root.removeEventListener("click", this.preventClick.bind(this), true);
this.player.root !== null && this.player.root.removeChild(this.lockDiv);
}
if (this.overlayDiv) {
this.player.root !== null && this.player.root.removeChild(this.overlayDiv);
}
}
}
@@ -1,36 +0,0 @@
<template>
<Teleport :to="player.root">
<App v-bind="pick(toRaw(props), ['player', 'root'])"></App>
</Teleport>
</template>
<script setup lang="ts">
import { Component, toRaw } from "vue";
import Player from "xgplayer/es/player";
import pick from "lodash/pick";
const props = defineProps({
player: {
type: Player,
required: true
},
root: {
type: Element,
required: true
},
App: {
type: Object as () => Component,
required: true
}
})
</script>
<style lang="scss" scoped>
.test {
width: 50px;
height: 50px;
background-color: aliceblue;
position: absolute;
right: 0;
}
</style>
@@ -1,35 +0,0 @@
import { App, Component, createApp } from "vue";
import { BasePlugin } from "xgplayer";
import fakeApp from "./fakeRoot.vue";
const useXGPlayerVueFrame = (App: Component) => {
return class XGPlayerVueFrame extends BasePlugin {
static get pluginName() {
return "xgplayerVueFrame";
}
private root: HTMLDivElement | null = null;
private app: App<Element> | null = null;
constructor(args: any) {
super(args);
}
afterCreate() {
this.createVueFrame();
}
private createVueFrame() {
this.root = document.createElement("div");
this.app = createApp(fakeApp, { player: this.player, root: this.root, App })
this.app.mount(this.root);
// this.app.mount(this.player.root);
}
destroy() {
this.app?.unmount()
}
};
};
export default useXGPlayerVueFrame;
@@ -1,33 +0,0 @@
<template>
<!-- <div class="test"></div> -->
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import Player from "xgplayer/es/player";
const props = defineProps({
player: {
type: Player,
required: true
},
root: {
type: Element,
required: true
}
})
onMounted(() => {
props
})
</script>
<style lang="scss" scoped>
.test {
width: 50px;
height: 50px;
background-color: aliceblue;
position: absolute;
right: 0;
}
</style>
File diff suppressed because it is too large. Load diff
@@ -1,36 +0,0 @@
import type { Plugin, registerComponentOption } from "@/class/PluginCenter";
export function checkNormalPlugin(obj: any): obj is Plugin {
return (
typeof obj === "object" &&
obj!== null &&
typeof obj.name === "string" &&
(obj.icon === undefined || typeof obj.icon === "object") &&
typeof obj.disable === "boolean" &&
Array.isArray(obj.group) &&
obj.group.every((group: any) => typeof group === "string") &&
typeof obj.priority === "number" &&
typeof obj.filter === "function" &&
typeof obj.func === "function"
);
}
export function checkComponentPlugin(
obj: any
): obj is registerComponentOption {
return (
typeof obj === "object" &&
obj!== null &&
typeof obj.key === "string" &&
typeof obj.name === "string" &&
(obj.icon === undefined || typeof obj.icon === "object") &&
typeof obj.getComponent === "function" &&
((obj.group === undefined) || (Array.isArray(obj.group) && obj.group.every((item: any) => typeof item === "string"))) &&
typeof obj.filter === "function" &&
(obj.disable === undefined || typeof obj.disable === "boolean") &&
(obj.exitConfirm === undefined || typeof obj.exitConfirm === "boolean") &&
(obj.single === undefined || typeof obj.single === "boolean") &&
(obj.priority === undefined || typeof obj.priority === "number")
);
}
@@ -1,29 +0,0 @@
import { Plugin, PluginGroup } from "@/class/PluginCenter";
import { LogoGithub } from "@vicons/ionicons5";
import { PluginExportType } from "@/types/type";
import { shallowRef } from "vue";
import registerExternalPlugin from "./registerExternalPlugin";
const pluginConfig: Plugin = {
name: "导入插件",
icon: shallowRef(LogoGithub),
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false;
}
if (vFiles.length !== 1) {
return false;
}
return vFiles[0].name.endsWith('js')
},
disable: false,
group: [PluginGroup.default],
func: registerExternalPlugin,
priority: -1
};
export default pluginConfig;
export const type = PluginExportType.default;
export const key = 'registerExternalPlugin';
@@ -1,36 +0,0 @@
import VirtualDirectory, { VirtualFile } from "@/class/VirtualDirectory";
// import useMiaoFetchApi from "@/hooks/useMiaoFetchApi";
import { PluginExportType, usePluginHooksType } from "@/types/type";
import type { Plugin, registerComponentOption } from "@/class/PluginCenter";
import { checkComponentPlugin, checkNormalPlugin } from "./checkPluginAvailable";
// const miaoFetchApi = useMiaoFetchApi()
export default async function registerExternalPlugin(_VDirectories: VirtualDirectory[], VFiles: VirtualFile[], hooks: usePluginHooksType) {
try {
const targetPluginVFile = VFiles[0] as VirtualFile
const plugin = await import(/* @vite-ignore */ targetPluginVFile.url) as {
default: registerComponentOption | Plugin;
type: PluginExportType;
key?: string;
}
const pc = hooks.getPluginCenter()
if (plugin.type === PluginExportType.default) {
if (checkNormalPlugin(plugin.default) === false) {
throw('插件不合法')
}
pc.register(plugin.key ?? `${Math.random()}`, plugin.default as Plugin)
} else {
if (checkComponentPlugin(plugin.default) === false) {
throw('插件不合法')
}
pc.registerComponent(plugin.default as registerComponentOption)
}
} catch (e) {
console.error(e);
hooks.globalAlertTip('导入外部插件失败', {
type: 'error',
timeout: 3000
})
}
}
-4
View File
@@ -1,4 +0,0 @@
:root
body {
background-color: aliceblue;
}
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more