update FileExplorer
This commit is contained in:
@@ -8,6 +8,11 @@
|
|||||||
:left-arrow="!!navBarStore.canBack"
|
:left-arrow="!!navBarStore.canBack"
|
||||||
@left-click="doBack"
|
@left-click="doBack"
|
||||||
>
|
>
|
||||||
|
<template #right>
|
||||||
|
<div class="nav-extra">
|
||||||
|
<component :is="navBarStore.rightRenderer" v-if="navBarStore.rightRenderer" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</t-navbar>
|
</t-navbar>
|
||||||
<div class="router-view">
|
<div class="router-view">
|
||||||
<page-transition />
|
<page-transition />
|
||||||
@@ -143,22 +148,11 @@ const bodyHeight = computed(() => {
|
|||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-btn {
|
.nav-extra {
|
||||||
padding: 0;
|
gap: .35rem;
|
||||||
border: none;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
color: var(--td-text-color-primary, #333);
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-text {
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Root from "@/Root.vue";
|
|||||||
import "tdesign-mobile-vue/es/style/index.css";
|
import "tdesign-mobile-vue/es/style/index.css";
|
||||||
import "timi-web/style.css";
|
import "timi-web/style.css";
|
||||||
import "timi-tdesign-mobile/style.css";
|
import "timi-tdesign-mobile/style.css";
|
||||||
|
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||||
|
|
||||||
export const pinia = createPinia();
|
export const pinia = createPinia();
|
||||||
|
|
||||||
|
|||||||
136
src/pages/file/FileExplorerGrid.vue
Normal file
136
src/pages/file/FileExplorerGrid.vue
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<recycle-scroller
|
||||||
|
class="scroller"
|
||||||
|
:items="rows"
|
||||||
|
:item-size="158"
|
||||||
|
key-field="key"
|
||||||
|
v-slot="{ item }"
|
||||||
|
>
|
||||||
|
<div class="grid-row">
|
||||||
|
<article
|
||||||
|
v-for="entry in item.items"
|
||||||
|
:key="entry.path"
|
||||||
|
class="card glass-white"
|
||||||
|
@click="emit('open', entry)"
|
||||||
|
>
|
||||||
|
<div :class="['icon', entry.type]">
|
||||||
|
<t-icon :name="entry.type === 'dir' ? 'folder' : entry.icon" />
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<p class="name" v-text="entry.name"></p>
|
||||||
|
<p class="desc" v-text="formatItemDesc(entry)"></p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<div v-if="item.items.length < columns" class="card ghost"></div>
|
||||||
|
</div>
|
||||||
|
</recycle-scroller>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { RecycleScroller } from "vue-virtual-scroller";
|
||||||
|
import { formatItemDesc, type ExplorerItem } from "./fileExplorer.shared";
|
||||||
|
|
||||||
|
interface GridRow {
|
||||||
|
key: string;
|
||||||
|
items: ExplorerItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = 2;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
items: ExplorerItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
open: [item: ExplorerItem];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rows = computed<GridRow[]>(() => {
|
||||||
|
const nextRows: GridRow[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < props.items.length; index += columns) {
|
||||||
|
const chunk = props.items.slice(index, index + columns);
|
||||||
|
nextRows.push({
|
||||||
|
key: chunk.map((item) => item.path).join("|"),
|
||||||
|
items: chunk
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextRows;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.scroller {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-row {
|
||||||
|
gap: .75rem;
|
||||||
|
display: grid;
|
||||||
|
padding-bottom: .75rem;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem .875rem;
|
||||||
|
border: 1px solid var(--app-line);
|
||||||
|
border-radius: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--app-card);
|
||||||
|
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.ghost {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
display: flex;
|
||||||
|
border-radius: .9rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #4d8dff, #76a9ff);
|
||||||
|
|
||||||
|
&.dir {
|
||||||
|
background: linear-gradient(135deg, #ff9c3d, #ffbf69);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
gap: .3rem;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name,
|
||||||
|
.desc {
|
||||||
|
margin: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: var(--app-text);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
color: var(--app-sub);
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.theme-dark) {
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
src/pages/file/FileExplorerList.vue
Normal file
51
src/pages/file/FileExplorerList.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<t-cell-group theme="card" class="list-view">
|
||||||
|
<recycle-scroller
|
||||||
|
class="scroller"
|
||||||
|
:items="items"
|
||||||
|
:item-size="56"
|
||||||
|
key-field="path"
|
||||||
|
v-slot="{ item }"
|
||||||
|
>
|
||||||
|
<t-swipe-cell class="swipe">
|
||||||
|
<t-cell
|
||||||
|
:title="item.name"
|
||||||
|
:arrow="item.type === 'dir'"
|
||||||
|
:note="item.size"
|
||||||
|
@click="emit('open', item)"
|
||||||
|
>
|
||||||
|
<template #left-icon>
|
||||||
|
<t-icon :name="item.type === 'dir' ? 'folder' : item.icon" />
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-swipe-cell>
|
||||||
|
</recycle-scroller>
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RecycleScroller } from "vue-virtual-scroller";
|
||||||
|
import { type ExplorerItem } from "./fileExplorer.shared";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
items: ExplorerItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
open: [item: ExplorerItem];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.list-view {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
:deep(.t-cell-group) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,50 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<section class="toolbar glass-white">
|
|
||||||
<div class="path">
|
|
||||||
<t-button
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
theme="primary"
|
|
||||||
:disabled="!currentSegments.length"
|
|
||||||
@click="openRoot"
|
|
||||||
>
|
|
||||||
根目录
|
|
||||||
</t-button>
|
|
||||||
<span v-for="(segment, index) in currentSegments" :key="`${index}-${segment}`" class="crumb-wrap">
|
|
||||||
<span class="sep">/</span>
|
|
||||||
<t-button
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
theme="primary"
|
|
||||||
class="crumb"
|
|
||||||
@click="openByIndex(index)"
|
|
||||||
>
|
|
||||||
<span v-text="segment"></span>
|
|
||||||
</t-button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<t-button
|
|
||||||
size="small"
|
|
||||||
theme="primary"
|
|
||||||
:variant="displayMode === 'list' ? 'base' : 'outline'"
|
|
||||||
@click="setDisplayMode('list')"
|
|
||||||
>
|
|
||||||
列表
|
|
||||||
</t-button>
|
|
||||||
<t-button
|
|
||||||
size="small"
|
|
||||||
theme="primary"
|
|
||||||
:variant="displayMode === 'grid' ? 'base' : 'outline'"
|
|
||||||
@click="setDisplayMode('grid')"
|
|
||||||
>
|
|
||||||
平铺
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-if="currentSegments.length" class="go-up" @click="openParent">
|
<section v-if="currentSegments.length" class="go-up" @click="openParent">
|
||||||
<div class="icon dir">
|
<div class="icon dir">
|
||||||
<t-icon name="rollback" />
|
<t-icon name="rollback" />
|
||||||
@@ -55,43 +10,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section :class="['list', `is-${displayMode}`]">
|
<section v-if="currentItems.length" class="content">
|
||||||
<article
|
<file-explorer-list
|
||||||
v-for="item in currentItems"
|
v-if="displayMode === 'list'"
|
||||||
:key="item.path"
|
:items="currentItems"
|
||||||
class="item glass-white"
|
@open="openItem"
|
||||||
@click="openItem(item)"
|
/>
|
||||||
>
|
<file-explorer-grid
|
||||||
<div :class="['icon', item.type]">
|
v-else
|
||||||
<t-icon :name="item.type === 'dir' ? 'folder' : item.icon" />
|
:items="currentItems"
|
||||||
</div>
|
@open="openItem"
|
||||||
<div class="meta">
|
/>
|
||||||
<p class="name" v-text="item.name"></p>
|
|
||||||
<p class="desc" v-text="formatItemDesc(item)"></p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<t-empty v-if="!currentItems.length" description="当前目录为空" />
|
<t-empty v-else description="当前目录为空" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
type DisplayMode = "list" | "grid";
|
import FileExplorerGrid from "./FileExplorerGrid.vue";
|
||||||
type FileItemType = "dir" | "file";
|
import FileExplorerList from "./FileExplorerList.vue";
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
interface FileEntry {
|
import {
|
||||||
name: string;
|
type DisplayMode,
|
||||||
type: FileItemType;
|
type ExplorerItem,
|
||||||
size?: string;
|
type FileEntry
|
||||||
updatedAt: string;
|
} from "./fileExplorer.shared";
|
||||||
icon: string;
|
|
||||||
children?: FileEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExplorerItem extends Omit<FileEntry, "children"> {
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
mode?: DisplayMode;
|
mode?: DisplayMode;
|
||||||
@@ -112,6 +56,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
|
const navBarRightOwner = "file-explorer-page";
|
||||||
|
|
||||||
const fileTree: FileEntry[] = [
|
const fileTree: FileEntry[] = [
|
||||||
{
|
{
|
||||||
@@ -185,7 +131,7 @@ const fileTree: FileEntry[] = [
|
|||||||
name: "系统日志.log",
|
name: "系统日志.log",
|
||||||
type: "file",
|
type: "file",
|
||||||
size: "428 KB",
|
size: "428 KB",
|
||||||
icon: "file-text",
|
icon: "file",
|
||||||
updatedAt: "今天 07:55"
|
updatedAt: "今天 07:55"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -224,6 +170,32 @@ const currentItems = computed<ExplorerItem[]>(() => {
|
|||||||
path: buildItemPath(currentSegments.value, item.name)
|
path: buildItemPath(currentSegments.value, item.name)
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
const currentTitle = computed(() => {
|
||||||
|
if (!currentSegments.value.length) {
|
||||||
|
return "文件";
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSegments.value[currentSegments.value.length - 1];
|
||||||
|
});
|
||||||
|
const displayModeActionText = computed(() => {
|
||||||
|
return displayMode.value === "list" ? "平铺" : "列表";
|
||||||
|
});
|
||||||
|
const navBarRightRenderer = defineComponent({
|
||||||
|
name: "file-explorer-nav-right",
|
||||||
|
setup() {
|
||||||
|
const buttonComponent = resolveComponent("t-button");
|
||||||
|
|
||||||
|
return () => h(buttonComponent, {
|
||||||
|
size: "small",
|
||||||
|
variant: "text",
|
||||||
|
theme: "primary",
|
||||||
|
class: "nav-switch",
|
||||||
|
onClick: toggleDisplayMode
|
||||||
|
}, {
|
||||||
|
default: () => displayModeActionText.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.mode,
|
() => props.mode,
|
||||||
@@ -233,6 +205,22 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
currentTitle,
|
||||||
|
(value) => {
|
||||||
|
navBarStore.setTitle(value);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
displayModeActionText,
|
||||||
|
() => {
|
||||||
|
navBarStore.setRightRenderer(navBarRightRenderer, navBarRightOwner);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.path,
|
() => props.path,
|
||||||
(value) => {
|
(value) => {
|
||||||
@@ -277,14 +265,22 @@ function buildItemPath(pathSegments: string[], name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setDisplayMode(mode: DisplayMode): void {
|
function setDisplayMode(mode: DisplayMode): void {
|
||||||
|
if (displayMode.value === mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
displayMode.value = mode;
|
displayMode.value = mode;
|
||||||
emit("update:mode", mode);
|
emit("update:mode", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openRoot(): Promise<void> {
|
function toggleDisplayMode(): void {
|
||||||
await syncPath([]);
|
setDisplayMode(displayMode.value === "list" ? "grid" : "list");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
navBarStore.clearRight(navBarRightOwner);
|
||||||
|
});
|
||||||
|
|
||||||
async function openParent(): Promise<void> {
|
async function openParent(): Promise<void> {
|
||||||
if (shouldUseRoutePath.value && route.name === "FileExplorerPage" && currentSegments.value.length === 1) {
|
if (shouldUseRoutePath.value && route.name === "FileExplorerPage" && currentSegments.value.length === 1) {
|
||||||
await router.back();
|
await router.back();
|
||||||
@@ -294,10 +290,6 @@ async function openParent(): Promise<void> {
|
|||||||
await syncPath(currentSegments.value.slice(0, -1));
|
await syncPath(currentSegments.value.slice(0, -1));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openByIndex(index: number): Promise<void> {
|
|
||||||
await syncPath(currentSegments.value.slice(0, index + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openItem(item: ExplorerItem): Promise<void> {
|
async function openItem(item: ExplorerItem): Promise<void> {
|
||||||
if (item.type === "dir") {
|
if (item.type === "dir") {
|
||||||
const nextSegments = [...currentSegments.value, item.name];
|
const nextSegments = [...currentSegments.value, item.name];
|
||||||
@@ -328,14 +320,6 @@ async function syncPath(nextSegments: string[]): Promise<void> {
|
|||||||
localSegments.value = nextSegments;
|
localSegments.value = nextSegments;
|
||||||
emit("update:path", nextSegments);
|
emit("update:path", nextSegments);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatItemDesc(item: ExplorerItem): string {
|
|
||||||
if (item.type === "dir") {
|
|
||||||
return `文件夹 · ${item.updatedAt}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${item.size || "--"} · ${item.updatedAt}`;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@@ -344,48 +328,20 @@ function formatItemDesc(item: ExplorerItem): string {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.toolbar,
|
.nav-switch {
|
||||||
.go-up,
|
padding: 0;
|
||||||
.item {
|
}
|
||||||
|
|
||||||
|
.go-up {
|
||||||
border: 1px solid var(--app-line);
|
border: 1px solid var(--app-line);
|
||||||
background: var(--app-card);
|
background: var(--app-card);
|
||||||
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.go-up {
|
||||||
gap: .75rem;
|
|
||||||
display: flex;
|
|
||||||
padding: .875rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.path,
|
|
||||||
.actions {
|
|
||||||
gap: .25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumb-wrap {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sep {
|
|
||||||
color: var(--app-sub);
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumb {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.go-up,
|
|
||||||
.item {
|
|
||||||
gap: .875rem;
|
gap: .875rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: .875rem 1rem;
|
padding: .875rem 1rem;
|
||||||
@@ -394,23 +350,9 @@ function formatItemDesc(item: ExplorerItem): string {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.content {
|
||||||
gap: .75rem;
|
min-height: 0;
|
||||||
display: grid;
|
flex: 1 1 auto;
|
||||||
|
|
||||||
&.is-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
|
|
||||||
.item {
|
|
||||||
padding: 1rem .875rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@@ -420,11 +362,11 @@ function formatItemDesc(item: ExplorerItem): string {
|
|||||||
border-radius: .9rem;
|
border-radius: .9rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
background: linear-gradient(135deg, #4D8DFF, #76A9FF);
|
background: linear-gradient(135deg, #4d8dff, #76a9ff);
|
||||||
|
|
||||||
&.dir {
|
&.dir {
|
||||||
background: linear-gradient(135deg, #FF9C3D, #FFBF69);
|
background: linear-gradient(135deg, #ff9c3d, #ffbf69);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,9 +396,7 @@ function formatItemDesc(item: ExplorerItem): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(.theme-dark) .page {
|
:global(.theme-dark) .page {
|
||||||
.toolbar,
|
.go-up {
|
||||||
.go-up,
|
|
||||||
.item {
|
|
||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22);
|
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/pages/file/fileExplorer.shared.ts
Normal file
23
src/pages/file/fileExplorer.shared.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export type DisplayMode = "list" | "grid";
|
||||||
|
export type FileItemType = "dir" | "file";
|
||||||
|
|
||||||
|
export interface FileEntry {
|
||||||
|
name: string;
|
||||||
|
type: FileItemType;
|
||||||
|
size?: string;
|
||||||
|
updatedAt: string;
|
||||||
|
icon: string;
|
||||||
|
children?: FileEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExplorerItem extends Omit<FileEntry, "children"> {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatItemDesc(item: ExplorerItem): string {
|
||||||
|
if (item.type === "dir") {
|
||||||
|
return `文件夹 · ${item.updatedAt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${item.size || "--"} · ${item.updatedAt}`;
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ const tabs: RouteRecordRaw[] = [
|
|||||||
navBarTitle: "文件",
|
navBarTitle: "文件",
|
||||||
tabBarVisible: true,
|
tabBarVisible: true,
|
||||||
tabBarPadding: true,
|
tabBarPadding: true,
|
||||||
bodyBackground: "#FFF"
|
bodyBackground: "#F4F4F4"
|
||||||
},
|
},
|
||||||
component: FilePage
|
component: FilePage
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Component } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useNavBarStore = defineStore("nav-bar", () => {
|
export const useNavBarStore = defineStore("nav-bar", () => {
|
||||||
@@ -7,8 +8,8 @@ export const useNavBarStore = defineStore("nav-bar", () => {
|
|||||||
const height = ref(0);
|
const height = ref(0);
|
||||||
const title = ref("");
|
const title = ref("");
|
||||||
const backTo = ref<string>();
|
const backTo = ref<string>();
|
||||||
const rightText = ref<string>();
|
const rightRenderer = shallowRef<Component>();
|
||||||
const rightAction = ref<(() => void) | undefined>();
|
const rightOwner = ref<string>();
|
||||||
|
|
||||||
const isShowing = computed(() => !!router.currentRoute.value.meta.navBarVisible);
|
const isShowing = computed(() => !!router.currentRoute.value.meta.navBarVisible);
|
||||||
const canBack = computed(() => !!router.currentRoute.value.meta.navBarCanBack);
|
const canBack = computed(() => !!router.currentRoute.value.meta.navBarCanBack);
|
||||||
@@ -33,32 +34,31 @@ export const useNavBarStore = defineStore("nav-bar", () => {
|
|||||||
title.value = value || "";
|
title.value = value || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRightText(value?: string): void {
|
function setRightRenderer(renderer?: Component, owner?: string): void {
|
||||||
rightText.value = value;
|
rightRenderer.value = renderer;
|
||||||
|
rightOwner.value = owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRightAction(action?: () => void): void {
|
function clearRight(owner?: string): void {
|
||||||
rightAction.value = action;
|
if (owner && rightOwner.value !== owner) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function clearRight(): void {
|
rightRenderer.value = undefined;
|
||||||
rightText.value = undefined;
|
rightOwner.value = undefined;
|
||||||
rightAction.value = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
height,
|
height,
|
||||||
title,
|
title,
|
||||||
backTo,
|
backTo,
|
||||||
rightText,
|
rightRenderer,
|
||||||
rightAction,
|
|
||||||
isShowing,
|
isShowing,
|
||||||
canBack,
|
canBack,
|
||||||
setHeight,
|
setHeight,
|
||||||
setBackTo,
|
setBackTo,
|
||||||
setTitle,
|
setTitle,
|
||||||
setRightText,
|
setRightRenderer,
|
||||||
setRightAction,
|
|
||||||
clearRight
|
clearRight
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
12
src/types/vue-virtual-scroller.d.ts
vendored
Normal file
12
src/types/vue-virtual-scroller.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
declare module "vue-virtual-scroller" {
|
||||||
|
import type { App, DefineComponent, Plugin } from "vue";
|
||||||
|
|
||||||
|
export const RecycleScroller: DefineComponent;
|
||||||
|
export const DynamicScroller: DefineComponent;
|
||||||
|
export const DynamicScrollerItem: DefineComponent;
|
||||||
|
|
||||||
|
export function install(app: App): void;
|
||||||
|
|
||||||
|
const VueVirtualScroller: Plugin;
|
||||||
|
export default VueVirtualScroller;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user