Compare commits

2 Commits
7 changed files with 893 additions and 654 deletions

No files matched your search

+326 -277
View File
@@ -1,150 +1,202 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, onMounted, reactive, ref, shallowRef } from 'vue'
import { h, onMounted, reactive, ref, shallowRef } from 'vue'; import { NIcon, NScrollbar, NDropdown, NQrCode } from 'naive-ui'
import { NIcon, NScrollbar, NDropdown, NQrCode } from 'naive-ui'; import {
import { EyeOffOutline, EyeOutline, CloseOutline, CopyOutline, EllipsisVertical, QrCodeOutline } from '@vicons/ionicons5' EyeOffOutline,
import miaoDirectory from './views/miaoDirectory.vue'; EyeOutline,
import VirtualPage from './class/VirtualPage'; CloseOutline,
import VirtualDirectory, { VirtualFile } from './class/VirtualDirectory'; CopyOutline,
import MiaoMask from './components/miaoMask.vue'; EllipsisVertical,
import config from './config'; QrCodeOutline
import useViretualPages from './hooks/useVirtualPages'; } from '@vicons/ionicons5'
import miaoDirectory from './views/miaoDirectory.vue'
import VirtualPage from './class/VirtualPage'
import VirtualDirectory, { VirtualFile } from './class/VirtualDirectory'
import MiaoMask from './components/miaoMask.vue'
import config from './config'
import useVirtualPages from './hooks/useVirtualPages'
const { baseUrl } = config const { baseUrl } = config
const showModal = ref<boolean>(false) const showModal = ref<boolean>(false)
const modalData = shallowRef<{ const modalData = shallowRef<{
component: any component: any
props: any props: any
}>() }>()
const views = useViretualPages() const views = useVirtualPages()
const rootDirectory = reactive(new VirtualDirectory({ const rootDirectory = reactive(
name: '根目录', new VirtualDirectory({
stats: { name: '根目录',
atimeMs: 0, stats: {
birthtimeMs: 0, atimeMs: 0,
ctimeMs: 0, birthtimeMs: 0,
mtimeMs: 0, ctimeMs: 0,
} mtimeMs: 0
})) }
})
)
// component就是一个vue组件,类似于miaoDirectory // component就是一个vue组件,类似于miaoDirectory
const createView = (component: any, currentObjects: (VirtualDirectory | VirtualFile)[], index?: number) => { const createView = (
index ?? (index = views.length) component: any,
views.splice(index, 0, reactive<VirtualPage>(new VirtualPage(component, [...currentObjects]))) currentObjects: (VirtualDirectory | VirtualFile)[],
index?: number
) => {
index ?? (index = views.length)
views.splice(
index,
0,
reactive<VirtualPage>(new VirtualPage(component, [...currentObjects]))
)
} }
const deleteView = (index: number) => { const deleteView = (index: number) => {
views.splice(index, 1) views.splice(index, 1)
if (views.length === 0) { if (views.length === 0) {
createView(miaoDirectory, [rootDirectory]) createView(miaoDirectory, [rootDirectory])
} }
} }
// 点击标题是隐藏其他标签或显示其他标签,还挺好用的 // 点击标题是隐藏其他标签或显示其他标签,还挺好用的
const handleClickTitle = (index: number) => { const handleClickTitle = (index: number) => {
let count = views.filter(v => v.visible).length let count = views.filter((v) => v.visible).length
let op = !views[index].visible || count > 1 ? 'hideOthers' : 'showOthers' let op = !views[index].visible || count > 1 ? 'hideOthers' : 'showOthers'
let _index = 0 let _index = 0
while (_index < views.length) { while (_index < views.length) {
if (_index !== index) { if (_index !== index) {
views[_index].visible = op === 'hideOthers' ? false : true views[_index].visible = op === 'hideOthers' ? false : true
} else { } else {
views[_index].visible = true views[_index].visible = true
}
_index += 1
} }
_index += 1
}
} }
// 顶部菜单相关 // 顶部菜单相关
function renderIcon(icon: any) { function renderIcon(icon: any) {
return () => { return () => {
return h(NIcon, null, { return h(NIcon, null, {
default: () => h(icon) default: () => h(icon)
}) })
} }
} }
const openMenuOption = [ const openMenuOption = [
{ {
label: '分享', label: '分享',
key: 'share', key: 'share',
icon: renderIcon(QrCodeOutline) icon: renderIcon(QrCodeOutline)
} }
] ]
const handleMenuSelect = (key: string) => { const handleMenuSelect = (key: string) => {
if (key === 'share') { if (key === 'share') {
showModal.value = true showModal.value = true
modalData.value = { modalData.value = {
component: NQrCode, component: NQrCode,
props: { props: {
value: baseUrl, value: baseUrl,
size: 300, size: 300,
errorCorrectionLevel: 'H' errorCorrectionLevel: 'H'
} }
}
} }
}
} }
onMounted(() => { onMounted(() => {
createView(miaoDirectory, [rootDirectory]) createView(miaoDirectory, [rootDirectory])
}) })
</script> </script>
<template> <template>
<div class="view"> <div class="view">
<div class="view-controler"> <div class="view-controler">
<n-scrollbar x-scrollable> <n-scrollbar x-scrollable>
<div class="tags-container"> <div class="tags-container">
<transition-group name="tab"> <transition-group name="tab">
<div class="tab" v-for="(view, index) of views" :key="view.uid"> <div
<div class="tab-title" @click="handleClickTitle(index)">{{ view.title }}</div> class="tab"
<div class="tab-point" :style="{ backgroundColor: view.color }"></div> v-for="(view, index) of views"
<div class="tab-control"> :key="view.uid">
<n-icon class="tag-control-icon icon" @click="view.switchShow()"> <div
<EyeOutline v-show="view.visible" class="icon-inner" /> class="tab-title"
<EyeOffOutline v-show="!view.visible" class="icon-inner" /> @click="handleClickTitle(index)">
</n-icon> {{ view.title }}
<n-icon class="tag-control-icon icon" </div>
@click="createView(view.component, view.currentObjects, index + 1)"> <div
<CopyOutline /> class="tab-point"
</n-icon> :style="{ backgroundColor: view.color }"></div>
<n-icon class="tag-control-icon icon" @click="deleteView(index)"> <div class="tab-control">
<CloseOutline class="icon-inner" /> <n-icon
</n-icon> class="tag-control-icon icon"
</div> @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.currentObjects,
index + 1
)
">
<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>
</transition-group>
</div> </div>
</n-scrollbar> <div class="view-container">
<div class="view-controler-menu"> <transition-group name="page">
<div class="view-controler-menu-item"> <div
<n-dropdown trigger="click" :options="openMenuOption" @select="handleMenuSelect"> class="view-container-item"
<n-icon class="icon"> v-for="(view, index) of views"
<EllipsisVertical class="icon-inner" /> v-show="view.visible"
</n-icon> :key="view.uid">
</n-dropdown> <component
:is="view.component"
v-model:current-objects="view.currentObjects"
:id="view.uid"
:color="view.color"
@exit="deleteView(index)"></component>
</div>
</transition-group>
</div> </div>
</div>
</div> </div>
<div class="view-container"> <!-- 模态框展示,用来显示分享二维码,设置菜单之类的东西 -->
<transition-group name="page"> <MiaoMask v-model:show="showModal" @click="showModal = false">
<div class="view-container-item" v-for="(view, index) of views" v-show="view.visible" :key="view.uid"> <component
<component :is="view.component" v-model:current-objects="view.currentObjects" :id="view.uid" :is="modalData?.component"
:color="view.color" @exit="deleteView(index)"></component> v-bind="modalData?.props"></component>
</div> </MiaoMask>
</transition-group>
</div>
</div>
<!-- 模态框展示,用来显示分享二维码,设置菜单之类的东西 -->
<MiaoMask v-model:show="showModal" @click="showModal = false">
<component :is="modalData?.component" v-bind="modalData?.props"></component>
</MiaoMask>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -152,197 +204,194 @@ $controler-height: 25px;
$tag-width: 170px; $tag-width: 170px;
.view { .view {
position: relative;
height: 100%;
width: 100%;
.view-controler {
display: flex;
height: $controler-height;
width: 100%;
background-color: #dadada;
// overflow: hidden;
.tags-container {
display: flex;
margin-left: 5px;
flex: 1;
// overflow-x: scroll;
.tab {
position: relative;
width: $tag-width;
margin: 0 5px;
// flex: 1;
display: flex;
border-radius: 5px 5px 0 0;
align-items: center;
.tab-title {
margin-left: 4px;
// width: 70%;
user-select: none;
cursor: pointer;
}
.tab-point {
height: 5px;
aspect-ratio: 1;
border-radius: 66px;
margin-left: 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($controler-height*0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
right: 0px;
}
}
}
.view-controler-menu {
position: relative;
display: flex;
align-items: center;
padding: 0 5px;
.view-controler-menu-item {
height: 100%;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
}
&::before {
content: '';
height: calc($controler-height*0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
left: 2px;
}
}
.icon {
height: calc($controler-height * 0.7);
width: calc($controler-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; position: relative;
display: flex; height: 100%;
// flex-direction: column; width: 100%;
height: calc(100% - $controler-height);
overflow-y: overlay;
overflow-x: overlay;
.view-container-item { .view-controler {
flex: 1; display: flex;
height: 100%; height: $controler-height;
width: 100%;
background-color: #dadada;
// overflow: hidden;
.tags-container {
display: flex;
margin-left: 5px;
flex: 1;
// overflow-x: scroll;
.tab {
position: relative;
width: $tag-width;
margin: 0 5px;
// flex: 1;
display: flex;
border-radius: 5px 5px 0 0;
align-items: center;
.tab-title {
margin-left: 4px;
// width: 70%;
user-select: none;
cursor: pointer;
}
.tab-point {
height: 5px;
aspect-ratio: 1;
border-radius: 66px;
margin-left: 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($controler-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($controler-height * 0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
left: 2px;
}
}
.icon {
height: calc($controler-height * 0.7);
width: calc($controler-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);
}
}
} }
&::-webkit-scrollbar { .view-container {
position: absolute; position: relative;
width: 4px; display: flex;
} // flex-direction: column;
height: calc(100% - $controler-height);
overflow-y: overlay;
overflow-x: overlay;
// 滚动条轨道 .view-container-item {
&::-webkit-scrollbar-track { flex: 1;
// background: rgb(239, 239, 239); height: 100%;
border-radius: 2px; }
}
// 小滑块 &::-webkit-scrollbar {
&::-webkit-scrollbar-thumb { position: absolute;
background: #e0e0e0; width: 4px;
// border-radius: 5px; }
}
&::-webkit-scrollbar-thumb:hover { // 滚动条轨道
background: #999999; &::-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动画相关 // transition动画相关
.view { .view {
.view-controler { .view-controler {
.tags-container { .tags-container {
.tab-move,
.tab-enter-active {
transition: all 0.2s ease;
}
.tab-move, .tab-leave-active {
.tab-enter-active { transition: none;
transition: all 0.2s ease; }
}
.tab-leave-active { .tab-enter-from {
transition: none; opacity: 0;
} transform: translateY(-30px);
}
.tab-enter-from { // .tab-leave-to {
opacity: 0; // opacity: 0;
transform: translateY(-30px); // transform: translateY(-80px) translateX(calc($tag-width + 10px));
} // }
// .tab-leave-to { .tab-leave-active {
// opacity: 0; position: absolute;
// transform: translateY(-80px) translateX(calc($tag-width + 10px)); }
// } }
.tab-leave-active {
position: absolute;
}
} }
}
.view-container { // .view-container {
// .page-move {
// transition: all 0.2s ease;
// }
// .page-move { // .page-enter-active {
// transition: all 0.2s ease;
// }
// .page-leave-active {
// transition: none;
// }
// .page-enter-from {
// opacity: 0;
// }
// .page-leave-active {
// position: absolute;
// }
// } // }
// .page-enter-active {
// }
// .page-leave-active {
// transition: none;
// }
// .page-enter-from {
// opacity: 0;
// }
// .page-leave-active {
// position: absolute;
// }
}
} }
</style> </style>
File renamed without changes.
File renamed without changes.
@@ -1,116 +1,169 @@
<template> <template>
<div class="miao-item" draggable="true" ref="itemRef" @dragstart="handleDragStart"> <div
<div class="item-main"> class="miao-item"
<div class="item-main-front" :style="{ backgroundColor: props.color }"></div> draggable="true"
<Icon class="item-main-icon"></Icon> ref="itemRef"
<div class="item-main-info"> @dragstart="handleDragStart">
<div class="item-main-info-name"> <div class="item-main">
{{ name }} <div
</div> class="item-main-front"
<div class="item-main-info-date" v-if="time"> :style="{ backgroundColor: props.color }"></div>
{{ time }} <Icon class="item-main-icon"></Icon>
</div> <div class="item-main-info">
</div> <div class="item-main-info-name">
<n-dropdown trigger="click" :options="dropDownOptions" @select="handleDropdownSelect"> {{ name }}
<div class="item-main-option" @click="e=>e.stopPropagation()"> </div>
<EllipsisVertical class="item-main-option-icon" /> <div class="item-main-info-date" v-if="time">
</div> {{ time }}
</n-dropdown> </div>
</div> </div>
</div> <n-dropdown
trigger="click"
:options="dropDownOptions"
@select="handleDropdownSelect">
<div
class="item-main-option"
@click="(e) => e.stopPropagation()">
<EllipsisVertical class="item-main-option-icon" />
</div>
</n-dropdown>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import {
DocumentText,
FileTray,
Help,
Image,
LogoChrome,
LogoCss3,
LogoJavascript,
LogoVue,
PlayCircle,
EllipsisVertical,
Disc
} from '@vicons/ionicons5'
import { computed, ref } from 'vue'
import dateFormatter from '@/hooks/dateFormatter'
// import useDataBus from '@/hooks/useDataBus'
import { NDropdown } from 'naive-ui'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'; // const dataBus = useDataBus()
import { DocumentText, FileTray, Help, Image, LogoChrome, LogoCss3, LogoJavascript, LogoVue, PlayCircle, EllipsisVertical, Disc } from '@vicons/ionicons5';
import { computed, ref } from 'vue';
import dateFormatter from '@/hooks/dateFormatter';
import useDataBus from '@/hooks/useDataBus';
import { NDropdown } from 'naive-ui';
const dataBus = useDataBus() const props = defineProps<{
item: VirtualDirectory | VirtualFile
name?: string
color: string
}>()
const emit = defineEmits<{ const emit = defineEmits<{
download: [] download: []
delete: [] delete: []
dragStart: [] dragStart: [event: DragEvent, item: VirtualDirectory | VirtualFile]
}>()
const props = defineProps<{
item: VirtualDirectory | VirtualFile
name?: string
color:string
}>() }>()
const itemType = ref<'file' | 'directory'>(props.item.type) const itemType = ref<'file' | 'directory'>(props.item.type)
const name = props.name ?? props.item.name const name = props.name ?? props.item.name
const time = computed(()=>{ const time = computed(() => {
return dateFormatter(new Date(props.item.stats.mtimeMs), "yyyy-MM-dd hh:mm:ss"); return dateFormatter(
new Date(props.item.stats.mtimeMs),
'yyyy-MM-dd hh:mm:ss'
)
}) })
const dropDownOptions = computed(()=> { const dropDownOptions = computed(() => {
const options = [{ const options = [
label: '删除', {
key: 'delete' label: '删除',
}] key: 'delete'
if(itemType.value === 'file'){ }
options.push(...[ ]
{ if (itemType.value === 'file') {
label: '下载', options.push(
key: 'download', ...[
} {
]) label: '下载',
key: 'download'
}
]
)
} }
return options return options
}) })
const handleDropdownSelect = (key: 'download' | 'delete') => { const handleDropdownSelect = (key: 'download' | 'delete') => {
if(key === 'download'){ if (key === 'download') {
emit('download') emit('download')
} else if (key === 'delete') { } else if (key === 'delete') {
emit('delete') emit('delete')
} }
} }
const Icon = computed(()=>{ const Icon = computed(() => {
try{ try {
if(props.item.type === 'directory') { if (props.item.type === 'directory') {
return FileTray return FileTray
} else if(props.item.type === 'file') { } else if (props.item.type === 'file') {
const aft = props.item.name.split('.').at(-1)?.toLowerCase() const aft = props.item.name.split('.').at(-1)?.toLowerCase()
if(!aft){ if (!aft) {
throw(1) throw 1
}else if(['txt', 'doc', 'docs', 'docx', 'xls', 'xlsx'].includes(aft)){ } else if (
['txt', 'doc', 'docs', 'docx', 'xls', 'xlsx'].includes(aft)
) {
return DocumentText return DocumentText
}else if(['mp3', 'wav', 'ape', 'acc', 'ogg', 'flac'].includes(aft)){ } else if (
['mp3', 'wav', 'ape', 'acc', 'ogg', 'flac'].includes(aft)
) {
return Disc return Disc
}else if(['.xbm','.tif','pjp','.svgz','jpg','jpeg','ico','tiff','.gif','svg','.jfif','.webp','.png','.bmp','pjpeg','.avif'].includes(aft)){ } else if (
[
'.xbm',
'.tif',
'pjp',
'.svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'.gif',
'svg',
'.jfif',
'.webp',
'.png',
'.bmp',
'pjpeg',
'.avif'
].includes(aft)
) {
return Image return Image
}else if(['mp4', 'm2v', 'mkv'].includes(aft)){ } else if (['mp4', 'm2v', 'mkv'].includes(aft)) {
return PlayCircle return PlayCircle
}else if(['js', 'jsx', 'ts', 'tsx'].includes(aft)){ } else if (['js', 'jsx', 'ts', 'tsx'].includes(aft)) {
return LogoJavascript return LogoJavascript
}else if(['html'].includes(aft)){ } else if (['html'].includes(aft)) {
return LogoChrome return LogoChrome
}else if(['css', 'scss', 'less'].includes(aft)){ } else if (['css', 'scss', 'less'].includes(aft)) {
return LogoCss3 return LogoCss3
}else if(['vue'].includes(aft)){ } else if (['vue'].includes(aft)) {
return LogoVue return LogoVue
} }
return DocumentText return DocumentText
} }
}catch{ } catch {
return Help return Help
} }
}) })
const itemRef = ref() const itemRef = ref()
const handleDragStart = () => { const handleDragStart = (event: DragEvent) => {
dataBus.set('dragData', [ props.item ]) emit('dragStart', event, props.item)
// dataBus.set('dragData', [props.item])
} }
</script> </script>
@@ -138,7 +191,7 @@ const handleDragStart = () => {
.item-main-front { .item-main-front {
height: 100%; height: 100%;
width: 10px; width: 10px;
border-radius: 7px 0 0 7px ; border-radius: 7px 0 0 7px;
background-color: rgb(0, 0, 0); background-color: rgb(0, 0, 0);
} }
.item-main-icon { .item-main-icon {
@@ -176,17 +229,18 @@ const handleDragStart = () => {
color: #000; color: #000;
transition: color 0.15s; transition: color 0.15s;
} }
&:hover, &:active { &:hover,
&:active {
background-color: #8b8b8b; background-color: #8b8b8b;
.item-main-option-icon { .item-main-option-icon {
color: #FFF; color: #fff;
} }
} }
} }
&:hover, &:active { &:hover,
&:active {
background-color: #d2d2d2; background-color: #d2d2d2;
} }
} }
} }
</style> </style>
+106
View File
@@ -0,0 +1,106 @@
<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 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 handleDragOver = async (event: DragEvent) => {
showMask.value += 1
event.preventDefault()
}
const handleDragLeave = async () => {
showMask.value = 0
}
const handleDrop = async (event: DragEvent) => {
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>
+1 -1
View File
@@ -2,7 +2,7 @@
<transition name="transition"> <transition name="transition">
<div class="mask-body" ref="maskBodyRef" v-show="showMask"> <div class="mask-body" ref="maskBodyRef" v-show="showMask">
<div @click="e=>e.stopPropagation()"> <div @click="e=>e.stopPropagation()">
<slot> {{}} </slot> <slot></slot>
</div> </div>
</div> </div>
</transition> </transition>
+336 -306
View File
@@ -1,240 +1,270 @@
<template> <template>
<div class="miao-directory-item-container" ref="rootDomRef"> <miao-drop-handler @on-virtual-directory="handleDropVDirectory" @on-files="handleDropFiles">
<div <div class="miao-directory-item-container" ref="rootDomRef">
class="container-top" <div
:style="{ backgroundColor: index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)' }" class="container-top"
> :style="{
<div class="container-top-colorbar" :style="{ backgroundColor: props.color }"></div> backgroundColor:
<div class="container-top-breadcrumb"> index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)'
<!-- <n-scrollbar x-scrollable> --> }">
<div class="container-top-breadcrumb-container"> <div
<n-breadcrumb separator=">" style="margin-left: 5px"> class="container-top-colorfulBar"
<n-breadcrumb-item :clickable="false"> :style="{ backgroundColor: props.color }"></div>
<n-icon size="18"> <div class="container-top-breadcrumb">
<CloudOutline /> <!-- <n-scrollbar x-scrollable> -->
</n-icon> <div class="container-top-breadcrumb-container">
</n-breadcrumb-item> <n-breadcrumb separator=">" style="margin-left: 5px">
<n-breadcrumb-item <n-breadcrumb-item :clickable="false">
v-for="dir in currentDirectory?.directoryArray" <n-icon size="18">
@click="setCurrentDirectory(dir)" <CloudOutline />
:clickable="true" </n-icon>
> </n-breadcrumb-item>
<div>{{ dir.name }}</div> <n-breadcrumb-item
</n-breadcrumb-item> v-for="dir in currentDirectory?.directoryArray"
</n-breadcrumb> @click="setCurrentDirectory(dir)"
:clickable="true">
<div>{{ dir.name }}</div>
</n-breadcrumb-item>
</n-breadcrumb>
</div>
<!-- </n-scrollbar> -->
</div>
<div class="container-top-tools">
<n-icon
class="container-top-tools-item"
size="15"
@click="handleBack">
<ChevronBackOutline />
</n-icon>
<n-icon
class="container-top-tools-item"
size="14"
@click="handleReload">
<ReloadOutline
class="container-top-tools-item-reload" />
</n-icon>
<n-icon
class="container-top-tools-item"
size="20"
@click="emit('exit')">
<CloseOutline />
</n-icon>
</div>
</div>
<div
class="container-items"
:style="{
backgroundColor:
index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)'
}">
<n-scrollbar>
<transition-group name="dirItem">
<miaoDirectoryItem
v-for="dir in showData_directory"
@click="setCurrentDirectory(dir)"
:item="dir"
:key="dir.uid"
:color="props.color"
@delete="handleItemDelete(dir)"
@drag-start="handleItemDragStart" />
</transition-group>
<transition-group name="dirItem">
<miaoDirectoryItem
v-for="file in showData_files"
:item="file"
:key="file.uid"
@click="openUrl(`${baseUrl}${api.get}${file.path}`)"
@download="
openUrl(`${baseUrl}${api.get}${file.path}`, {
download: file.name
})
"
@delete="handleItemDelete(file)"
@drag-start="handleItemDragStart"
:color="props.color" />
</transition-group>
</n-scrollbar>
</div>
<div
class="container-bottom"
:style="{
backgroundColor:
index % 2 === 1 ? 'rgb(61 61 61)' : 'rgb(162 162 162)'
}"></div>
<miao-mask v-model:show="showModel"></miao-mask>
<!-- <miao-popup-input ref="popupInput"></miao-popup-input> -->
</div> </div>
<!-- </n-scrollbar> --> </miao-drop-handler>
</div>
<div class="container-top-tools">
<n-icon class="container-top-tools-item" size="15" @click="handleBack">
<ChevronBackOutline />
</n-icon>
<n-icon class="container-top-tools-item" size="14" @click="handleReload">
<ReloadOutline class="container-top-tools-item-reload" />
</n-icon>
<n-icon class="container-top-tools-item" size="20" @click="emit('exit')">
<CloseOutline />
</n-icon>
</div>
</div>
<div
class="container-items"
:style="{ backgroundColor: index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)' }"
>
<n-scrollbar>
<transition-group name="dirItem">
<miaoDirectoryItem
v-for="dir in showData_directory"
@click="setCurrentDirectory(dir)"
:item="dir"
:key="dir.uid"
:color="props.color"
@delete="handleItemDelete(dir)"
/>
</transition-group>
<transition-group name="dirItem">
<miaoDirectoryItem
v-for="file in showData_files"
:item="file"
:key="file.uid"
@click="openUrl(`${baseUrl}${api.get}${file.path}`)"
@download="
openUrl(`${baseUrl}${api.get}${file.path}`, { download: file.name })
"
@delete="handleItemDelete(file)"
:color="props.color"
/>
</transition-group>
</n-scrollbar>
</div>
<div
class="container-bottom"
:style="{ backgroundColor: index % 2 === 1 ? 'rgb(61 61 61)' : 'rgb(162 162 162)' }"
></div>
<miao-mask v-model:show="showModel"></miao-mask>
<!-- <miao-popup-input ref="popupInput"></miao-popup-input> -->
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NBreadcrumbItem, NBreadcrumb, NIcon, NScrollbar } from "naive-ui"; import { NBreadcrumbItem, NBreadcrumb, NIcon, NScrollbar } from 'naive-ui'
import type { file, directory } from "@/types/type.ts"; import type { file, directory } from '@/types/type.ts'
import VirtualDirectory, { VirtualFile } from "@/class/VirtualDirectory"; import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from 'vue'
import Config from "@/config"; import Config from '@/config'
import dropHandler, { dropHandlerHooks } from "@/hooks/dropHandler"; import miaoMask from '@/components/miaoMask.vue'
import miaoMask from "@/components/miaoMask.vue"; import miaoDirectoryItem from '@/components/miaoDirectory/miaoDirectoryItem.vue'
import miaoPopupInput from "@/components/miaoPopupInput.vue"; // import miaoPopupInput from "@/components/miaoPopupInput.vue";
import miaoDirectoryItem from "@/components/miaoDirectory/miaoDirectoryItem.vue"; import miaoDropHandler from '@/components/miaoDropHandler.vue'
import useMiaoFetchApi from "@/hooks/useMiaoFetchApi"; import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import useDataBus from "@/hooks/useDataBus"; import useDataBus from '@/hooks/useDataBus'
import { import {
CloudOutline, CloudOutline,
ChevronBackOutline, ChevronBackOutline,
ReloadOutline, ReloadOutline,
CloseOutline, CloseOutline
} from "@vicons/ionicons5"; } from '@vicons/ionicons5'
import useUploadQueue from "@/hooks/useUploadQueue"; import useUploadQueue from '@/hooks/useUploadQueue'
import useViretualPages from "@/hooks/useVirtualPages"; import useVirtualPages from '@/hooks/useVirtualPages'
const { baseUrl, api } = Config; const { baseUrl, api } = Config
const dataBus = useDataBus(); const dataBus = useDataBus()
const views = useViretualPages(); const views = useVirtualPages()
const miaoFetchApi = useMiaoFetchApi(); const miaoFetchApi = useMiaoFetchApi()
const props = defineProps<{ const props = defineProps<{
color: string; color: string
id: number; id: number
}>(); }>()
const emit = defineEmits<{ const emit = defineEmits<{
exit: []; exit: []
}>(); }>()
const popupInput = ref() const popupInput = ref()
// 是否显示模态框 // 是否显示模态框
const showModel = ref(0); const showModel = ref(0)
const currentObjects = defineModel<VirtualDirectory[]>("currentObjects", { const currentObjects = defineModel<VirtualDirectory[]>('currentObjects', {
required: true, required: true
}); })
// 目前展示的Dir // 目前展示的Dir
const currentDirectory = computed(() => { const currentDirectory = computed(() => {
return currentObjects.value[0]; return currentObjects.value[0]
}); })
const index = computed(() => { const index = computed(() => {
for (let i in views) { for (let i in views) {
if (views[i].uid === props.id) { if (views[i].uid === props.id) {
return parseInt(i); return parseInt(i)
}
} }
} return 0
return 0; })
});
// 用于展示的数据,后续会增加排序等操作 // 用于展示的数据,后续会增加排序等操作
const showData_directory = computed<VirtualDirectory[]>(() => { const showData_directory = computed<VirtualDirectory[]>(() => {
const data = currentDirectory.value?.directorys const data = currentDirectory.value?.directorys
? [...currentDirectory.value?.directorys] ? [...currentDirectory.value?.directorys]
: []; : []
return data; return data
}); })
const showData_files = computed<VirtualFile[]>(() => { const showData_files = computed<VirtualFile[]>(() => {
const data = currentDirectory.value?.files ? [...currentDirectory.value?.files] : []; const data = currentDirectory.value?.files
return data; ? [...currentDirectory.value?.files]
}); : []
return data
})
// 用于监听拖拽文件事件 // 用于监听拖拽文件事件
const rootDomRef = ref<HTMLDivElement>(); const rootDomRef = ref<HTMLDivElement>()
const uploadQueue = useUploadQueue(); const uploadQueue = useUploadQueue()
// 设置显示的dir // 设置显示的dir
const setCurrentDirectory = async (virtualDirectory: VirtualDirectory) => { const setCurrentDirectory = async (virtualDirectory: VirtualDirectory) => {
// 若没有缓存数据则同步刷新,有缓存数据则异步刷新 // 若没有缓存数据则同步刷新,有缓存数据则异步刷新
if (virtualDirectory.files === undefined || virtualDirectory.directorys === undefined) { if (
const data: (file | directory)[] = await miaoFetchApi.get(virtualDirectory); virtualDirectory.files === undefined ||
virtualDirectory.updateContent(data); virtualDirectory.directorys === undefined
} else { ) {
(async () => { const data: (file | directory)[] = await miaoFetchApi.get(
const data: (file | directory)[] = await miaoFetchApi.get(virtualDirectory); virtualDirectory
virtualDirectory.updateContent(data); )
})(); virtualDirectory.updateContent(data)
} } else {
currentObjects.value[0] = virtualDirectory; ;(async () => {
}; const data: (file | directory)[] = await miaoFetchApi.get(
virtualDirectory
)
virtualDirectory.updateContent(data)
})()
}
currentObjects.value[0] = virtualDirectory
}
const reload = () => { const reload = () => {
setCurrentDirectory(currentDirectory.value); setCurrentDirectory(currentDirectory.value)
}; }
const openUrl = ( const openUrl = (
href: string, href: string,
config?: { config?: {
download?: string; download?: string
target?: string; target?: string
} }
) => { ) => {
let { target, download } = config ?? {}; let { target, download } = config ?? {}
const a = document.createElement("a"); const a = document.createElement('a')
a.setAttribute("href", href); a.setAttribute('href', href)
a.setAttribute("target", target ?? "_blank"); a.setAttribute('target', target ?? '_blank')
download && a.setAttribute("download", download); download && a.setAttribute('download', download)
a.click(); a.click()
}; }
const handleBack = () => { const handleBack = () => {
if (currentDirectory.value?.parent) { if (currentDirectory.value?.parent) {
setCurrentDirectory(currentDirectory.value.parent); setCurrentDirectory(currentDirectory.value.parent)
} }
}; }
const handleReload = () => { const handleReload = () => {
reload(); reload()
}; }
const handleItemDelete = async (item: VirtualDirectory | VirtualFile) => { const handleItemDelete = async (item: VirtualDirectory | VirtualFile) => {
const { response } = miaoFetchApi.delete(item, { const { response } = miaoFetchApi.delete(item, {
retry: 5, retry: 5
}); })
// todo: 将删除事件统一用一个事件管理中心管理 // todo: 将删除事件统一用一个事件管理中心管理
const id = (await response).eventId; const id = (await response).eventId
const { response: res } = miaoFetchApi.query(id); const { response: res } = miaoFetchApi.query(id)
const eventResult = await res; const eventResult = await res
if (eventResult.status === "success") { if (eventResult.status === 'success') {
reload(); reload()
} }
}; }
const handleItemDragStart = async (
_event: DragEvent,
item: VirtualDirectory | VirtualFile
) => {
if (item.type === 'file') {
dataBus.set('dragData_vFiles', [item])
} else {
dataBus.set('dragData_vDirectory', [item])
}
}
const handleDropVDirectory = async (vDirs: VirtualDirectory[]) => {
setCurrentDirectory(vDirs[0])
}
const handleDropFiles = async (files: File[]) => {
currentDirectory.value &&
uploadQueue.push(files, currentDirectory.value, {
onFinish() {
reload()
}
})
}
onMounted(async () => { onMounted(async () => {
await setCurrentDirectory(currentDirectory.value); await setCurrentDirectory(currentDirectory.value)
// 拖动事件处理 })
const dropFileOption: dropHandlerHooks = {
onDropFiles(files: File[]) {
const dragData = dataBus.pop("dragData");
console.log({ dragData, files });
if (files && files.length > 0) {
currentDirectory.value &&
uploadQueue.push(files, currentDirectory.value, {
onFinish() {
reload();
},
});
}
},
onEnter() {
showModel.value += 1;
},
onEnd() {
showModel.value -= 1;
},
};
rootDomRef.value && dropHandler(rootDomRef.value, dropFileOption);
console.log(await popupInput.value.popup())
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -242,137 +272,137 @@ $top-height: 30px;
$bottom-bar-height: 60px; $bottom-bar-height: 60px;
.miao-directory-item-container { .miao-directory-item-container {
position: relative;
height: 100%;
width: inherit;
min-width: 266px;
display: flex;
flex-direction: column;
overflow: hidden;
.container-top {
height: $top-height;
width: 100%;
user-select: none;
position: relative; position: relative;
height: 100%;
width: inherit;
min-width: 266px;
display: flex; display: flex;
align-items: center; flex-direction: column;
overflow: hidden; overflow: hidden;
background-color: #f1f1f1;
.container-top-colorbar { .container-top {
height: 70%; height: $top-height;
width: 5px; width: 100%;
margin-left: 10px; user-select: none;
border-radius: 3px 0 0 3px; position: relative;
}
.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; display: flex;
justify-content: flex-start;
align-items: center; 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;
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);
}
}
// &::after {
// content: '';
// height: 60%;
// position: absolute;
// right: -2px;
// width: 2px;
// background-color: #828282;
// }
}
} }
.container-top-tools { .container-items {
position: relative; width: 100%;
height: 100%; height: calc(100% - $top-height - $bottom-bar-height);
width: 70px; // flex: 1;
padding: 0 5px; }
display: flex;
justify-content: space-around;
align-items: center;
.container-top-tools-item { .container-bottom {
width: 20px; height: $bottom-bar-height;
height: 20px; background-color: bisque;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: #ffffff00; user-select: none;
border-radius: 5px; box-shadow: 0px 1px 10px #888888;
transition: background-color 0.3s; z-index: 2;
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);
}
}
// &::after {
// content: '';
// height: 60%;
// position: absolute;
// right: -2px;
// width: 2px;
// background-color: #828282;
// }
} }
}
.container-items {
width: 100%;
height: calc(100% - $top-height - $bottom-bar-height);
// flex: 1;
}
.container-bottom {
height: $bottom-bar-height;
background-color: bisque;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
box-shadow: 0px 1px 10px #888888;
z-index: 2;
}
} }
// transition动画相关 // transition动画相关
.miao-directory-item-container { // .miao-directory-item-container {
.container-items { // .container-items {
.dirItem-move { // .dirItem-move {
transition: all 0.2s ease; // transition: all 0.2s ease;
} // }
.dirItem-leave-active { // .dirItem-leave-active {
transition: none; // transition: none;
} // }
.dirItem-enter-from { // .dirItem-enter-from {
opacity: 0; // opacity: 0;
} // }
.dirItem-leave-active { // .dirItem-leave-active {
position: absolute; // position: absolute;
} // }
} // }
} // }
</style> </style>