add FileExplorerPage
This commit is contained in:
@@ -1,20 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-transition">
|
<div class="page-transition">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition v-if="hasTransition" :name="transitionName">
|
<transition :name="transitionName" :css="hasTransition">
|
||||||
<div class="pages" :key="route.fullPath">
|
<div class="pages" :key="pageKey">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<div v-else class="pages" :key="route.fullPath">
|
|
||||||
<component :is="Component" />
|
|
||||||
</div>
|
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from "vue-router";
|
||||||
|
import { viewDepthKey } from "vue-router";
|
||||||
import { useGlobalUIStore } from "@/store/globalUIStore";
|
import { useGlobalUIStore } from "@/store/globalUIStore";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -24,12 +22,26 @@ defineOptions({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const globalUIStore = useGlobalUIStore();
|
const globalUIStore = useGlobalUIStore();
|
||||||
|
const viewDepth = inject(viewDepthKey, 0);
|
||||||
|
|
||||||
const transitionName = ref("push-left");
|
const transitionName = ref("");
|
||||||
const hasTransition = computed(() => transitionName.value !== "");
|
const hasTransition = ref(false);
|
||||||
const pageBackground = computed(() => globalUIStore.bodyBackground);
|
const pageBackground = computed(() => globalUIStore.bodyBackground);
|
||||||
|
const currentDepth = computed(() => Number(unref(viewDepth)));
|
||||||
|
const pageKey = computed(() => {
|
||||||
|
const depth = currentDepth.value;
|
||||||
|
const matchedRecord = route.matched[depth];
|
||||||
|
|
||||||
// ---------- 路由深度计算 ----------
|
if (!matchedRecord) {
|
||||||
|
return route.fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth < route.matched.length - 1) {
|
||||||
|
return matchedRecord.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return route.fullPath;
|
||||||
|
});
|
||||||
|
|
||||||
const pathCache = new Map<string, number>();
|
const pathCache = new Map<string, number>();
|
||||||
|
|
||||||
@@ -51,17 +63,25 @@ function calcDepth(sourceRoute: RouteLocationNormalized): number {
|
|||||||
return depth;
|
return depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// beforeEach 返回注销函数,组件卸载时必须调用,否则守卫会一直存在。
|
|
||||||
const unregisterGuard = router.beforeEach((to, from) => {
|
const unregisterGuard = router.beforeEach((to, from) => {
|
||||||
|
if (to.meta.tabBarVisible && from.meta.tabBarVisible) {
|
||||||
|
transitionName.value = "";
|
||||||
|
hasTransition.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const toDepth = calcDepth(to);
|
const toDepth = calcDepth(to);
|
||||||
const fromDepth = calcDepth(from);
|
const fromDepth = calcDepth(from);
|
||||||
|
|
||||||
if (fromDepth < toDepth) {
|
if (fromDepth < toDepth) {
|
||||||
transitionName.value = "push-left";
|
transitionName.value = "push-left";
|
||||||
|
hasTransition.value = true;
|
||||||
} else if (toDepth < fromDepth) {
|
} else if (toDepth < fromDepth) {
|
||||||
transitionName.value = "push-right";
|
transitionName.value = "push-right";
|
||||||
|
hasTransition.value = true;
|
||||||
} else {
|
} else {
|
||||||
transitionName.value = "";
|
transitionName.value = "";
|
||||||
|
hasTransition.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,28 +104,35 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
// 这个 wrapper 必须独立,不能把 class=pages 直接挂在 <component> 上。
|
|
||||||
// 否则页面根节点会和动画容器变成同一个 DOM 节点,容易导致 fixed 布局溢出视口。
|
|
||||||
.pages {
|
.pages {
|
||||||
|
inset: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
background: v-bind(pageBackground);
|
background: v-bind(pageBackground);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- push-left / push-right 公共过渡状态 ----------
|
|
||||||
|
|
||||||
.push-left-enter-active,
|
.push-left-enter-active,
|
||||||
.push-left-leave-active,
|
.push-left-leave-active,
|
||||||
.push-right-enter-active,
|
.push-right-enter-active,
|
||||||
.push-right-leave-active {
|
.push-right-leave-active {
|
||||||
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
transition: transform @duration @easing, opacity .2s linear;
|
transition: transform @duration @easing, opacity .2s linear;
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 前进:下一页从右滑入,当前页退到左侧 ----------
|
.push-left-enter-active,
|
||||||
|
.push-right-leave-active {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.push-left-leave-active,
|
||||||
|
.push-right-enter-active {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.push-left-enter-from {
|
.push-left-enter-from {
|
||||||
transform: translate3d(100%, 0, 0);
|
transform: translate3d(100%, 0, 0);
|
||||||
@@ -125,10 +152,7 @@ onUnmounted(() => {
|
|||||||
transform: translate3d(-@page-offset, 0, 0);
|
transform: translate3d(-@page-offset, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 返回:当前页从右滑出,上一页回到原位 ----------
|
|
||||||
|
|
||||||
.push-right-enter-active {
|
.push-right-enter-active {
|
||||||
z-index: -1;
|
|
||||||
transition: transform @duration @easing, opacity .3s;
|
transition: transform @duration @easing, opacity .3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<route-placeholder title="文件详情页" description="这里保留为二级详情占位,用于验证从文件页进入和返回时的滑动方向。" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
</script>
|
|
||||||
463
src/pages/file/FileExplorerPage.vue
Normal file
463
src/pages/file/FileExplorerPage.vue
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
<template>
|
||||||
|
<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">
|
||||||
|
<div class="icon dir">
|
||||||
|
<t-icon name="rollback" />
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<p class="name">返回上一级</p>
|
||||||
|
<p class="desc">当前目录的父级目录</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section :class="['list', `is-${displayMode}`]">
|
||||||
|
<article
|
||||||
|
v-for="item in currentItems"
|
||||||
|
:key="item.path"
|
||||||
|
class="item glass-white"
|
||||||
|
@click="openItem(item)"
|
||||||
|
>
|
||||||
|
<div :class="['icon', item.type]">
|
||||||
|
<t-icon :name="item.type === 'dir' ? 'folder' : item.icon" />
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<p class="name" v-text="item.name"></p>
|
||||||
|
<p class="desc" v-text="formatItemDesc(item)"></p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<t-empty v-if="!currentItems.length" description="当前目录为空" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type DisplayMode = "list" | "grid";
|
||||||
|
type FileItemType = "dir" | "file";
|
||||||
|
|
||||||
|
interface FileEntry {
|
||||||
|
name: string;
|
||||||
|
type: FileItemType;
|
||||||
|
size?: string;
|
||||||
|
updatedAt: string;
|
||||||
|
icon: string;
|
||||||
|
children?: FileEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExplorerItem extends Omit<FileEntry, "children"> {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
mode?: DisplayMode;
|
||||||
|
path?: string | string[];
|
||||||
|
useRoutePath?: boolean;
|
||||||
|
}>(), {
|
||||||
|
mode: "list",
|
||||||
|
path: "",
|
||||||
|
useRoutePath: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:path": [value: string[]];
|
||||||
|
"open-folder": [value: string[]];
|
||||||
|
"open-file": [value: ExplorerItem];
|
||||||
|
"update:mode": [value: DisplayMode];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const fileTree: FileEntry[] = [
|
||||||
|
{
|
||||||
|
name: "文档",
|
||||||
|
type: "dir",
|
||||||
|
icon: "folder",
|
||||||
|
updatedAt: "今天 09:24",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "项目说明.md",
|
||||||
|
type: "file",
|
||||||
|
size: "18 KB",
|
||||||
|
icon: "file-text",
|
||||||
|
updatedAt: "今天 09:20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "设计稿",
|
||||||
|
type: "dir",
|
||||||
|
icon: "folder",
|
||||||
|
updatedAt: "昨天 18:10",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "home.png",
|
||||||
|
type: "file",
|
||||||
|
size: "1.2 MB",
|
||||||
|
icon: "image",
|
||||||
|
updatedAt: "昨天 18:08"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file.png",
|
||||||
|
type: "file",
|
||||||
|
size: "984 KB",
|
||||||
|
icon: "image",
|
||||||
|
updatedAt: "昨天 18:03"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "媒体",
|
||||||
|
type: "dir",
|
||||||
|
icon: "folder",
|
||||||
|
updatedAt: "今天 08:16",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "音乐",
|
||||||
|
type: "dir",
|
||||||
|
icon: "folder",
|
||||||
|
updatedAt: "今天 08:11",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "demo.mp3",
|
||||||
|
type: "file",
|
||||||
|
size: "6.8 MB",
|
||||||
|
icon: "music",
|
||||||
|
updatedAt: "今天 08:10"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "封面.jpg",
|
||||||
|
type: "file",
|
||||||
|
size: "2.4 MB",
|
||||||
|
icon: "image",
|
||||||
|
updatedAt: "昨天 22:40"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "系统日志.log",
|
||||||
|
type: "file",
|
||||||
|
size: "428 KB",
|
||||||
|
icon: "file-text",
|
||||||
|
updatedAt: "今天 07:55"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "backup.zip",
|
||||||
|
type: "file",
|
||||||
|
size: "126 MB",
|
||||||
|
icon: "file-zip",
|
||||||
|
updatedAt: "昨天 23:18"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayMode = ref<DisplayMode>(props.mode);
|
||||||
|
const localSegments = ref<string[]>(normalizePath(props.path));
|
||||||
|
|
||||||
|
const shouldUseRoutePath = computed(() => {
|
||||||
|
if (!props.useRoutePath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return route.name === "FilePage" || route.name === "FileExplorerPage";
|
||||||
|
});
|
||||||
|
|
||||||
|
const routeSegments = computed(() => normalizePath(route.params.pathMatch));
|
||||||
|
const currentSegments = computed(() => {
|
||||||
|
if (shouldUseRoutePath.value) {
|
||||||
|
return routeSegments.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return localSegments.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentItems = computed<ExplorerItem[]>(() => {
|
||||||
|
const target = resolveEntries(currentSegments.value);
|
||||||
|
return target.map((item) => ({
|
||||||
|
...item,
|
||||||
|
path: buildItemPath(currentSegments.value, item.name)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.mode,
|
||||||
|
(value) => {
|
||||||
|
displayMode.value = value;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.path,
|
||||||
|
(value) => {
|
||||||
|
if (shouldUseRoutePath.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localSegments.value = normalizePath(value);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
function normalizePath(source?: string | string[]): string[] {
|
||||||
|
if (Array.isArray(source)) {
|
||||||
|
return source.filter((segment) => !!segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.split("/").filter((segment) => !!segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEntries(pathSegments: string[]): FileEntry[] {
|
||||||
|
let entries = fileTree;
|
||||||
|
|
||||||
|
for (const segment of pathSegments) {
|
||||||
|
const next = entries.find((item) => item.type === "dir" && item.name === segment);
|
||||||
|
if (!next || !next.children) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = next.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildItemPath(pathSegments: string[], name: string): string {
|
||||||
|
return [...pathSegments, name].join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDisplayMode(mode: DisplayMode): void {
|
||||||
|
displayMode.value = mode;
|
||||||
|
emit("update:mode", mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openRoot(): Promise<void> {
|
||||||
|
await syncPath([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openParent(): Promise<void> {
|
||||||
|
if (shouldUseRoutePath.value && route.name === "FileExplorerPage" && currentSegments.value.length === 1) {
|
||||||
|
await router.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
if (item.type === "dir") {
|
||||||
|
const nextSegments = [...currentSegments.value, item.name];
|
||||||
|
emit("open-folder", nextSegments);
|
||||||
|
await syncPath(nextSegments);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("open-file", item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncPath(nextSegments: string[]): Promise<void> {
|
||||||
|
if (shouldUseRoutePath.value) {
|
||||||
|
if (!nextSegments.length) {
|
||||||
|
await router.push("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.push({
|
||||||
|
name: "FileExplorerPage",
|
||||||
|
params: {
|
||||||
|
pathMatch: nextSegments
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localSegments.value = nextSegments;
|
||||||
|
emit("update:path", nextSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatItemDesc(item: ExplorerItem): string {
|
||||||
|
if (item.type === "dir") {
|
||||||
|
return `文件夹 · ${item.updatedAt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${item.size || "--"} · ${item.updatedAt}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.page {
|
||||||
|
gap: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.toolbar,
|
||||||
|
.go-up,
|
||||||
|
.item {
|
||||||
|
border: 1px solid var(--app-line);
|
||||||
|
background: var(--app-card);
|
||||||
|
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
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;
|
||||||
|
display: flex;
|
||||||
|
padding: .875rem 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
gap: .75rem;
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
&.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 {
|
||||||
|
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: 1 1 auto;
|
||||||
|
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) .page {
|
||||||
|
.toolbar,
|
||||||
|
.go-up,
|
||||||
|
.item {
|
||||||
|
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .22);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="page">
|
|
||||||
<route-placeholder title="LoginPage" description="登录页占位,当前仅保留独立路由入口,不引入业务表单。" />
|
|
||||||
<t-button block theme="primary" @click="router.replace('/')">
|
|
||||||
进入应用首页
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const router = useRouter();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.page {
|
|
||||||
gap: 1rem;
|
|
||||||
display: flex;
|
|
||||||
padding: 1.2rem;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,25 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<file-explorer-page />
|
||||||
<route-placeholder title="文件页" description="这里保留为文件管理入口,用于验证首页标签、布局容器和二级详情页切换。" />
|
|
||||||
<t-button block theme="primary" @click="openFileDetail">
|
|
||||||
打开文件详情页
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const router = useRouter();
|
import FileExplorerPage from "@/pages/file/FileExplorerPage.vue";
|
||||||
|
|
||||||
async function openFileDetail(): Promise<void> {
|
|
||||||
await router.push("/files/detail/demo-file");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.page {
|
|
||||||
gap: 1rem;
|
|
||||||
display: flex;
|
|
||||||
padding: 1.2rem;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<route-placeholder title="阅读页" description="该页面暂时不在 tab 中展示,只保留占位,后续可按需恢复。" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
</script>
|
|
||||||
@@ -4,10 +4,9 @@ import MainLayout from "@/layout/MainLayout.vue";
|
|||||||
import tabs from "@/router/tabs";
|
import tabs from "@/router/tabs";
|
||||||
import { DEFAULT_BODY_BACKGROUND, useGlobalUIStore } from "@/store/globalUIStore";
|
import { DEFAULT_BODY_BACKGROUND, useGlobalUIStore } from "@/store/globalUIStore";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import LoginPage from "@/pages/system/LoginPage.vue";
|
|
||||||
import NotFoundPage from "@/pages/system/NotFoundPage.vue";
|
import NotFoundPage from "@/pages/system/NotFoundPage.vue";
|
||||||
import ServerIndexPage from "@/pages/system/ServerIndexPage.vue";
|
import ServerIndexPage from "@/pages/system/ServerIndexPage.vue";
|
||||||
import FileDetailPage from "@/pages/detail/FileDetailPage.vue";
|
import FileExplorerPage from "@/pages/file/FileExplorerPage.vue";
|
||||||
import ServerLogPage from "@/pages/detail/ServerLogPage.vue";
|
import ServerLogPage from "@/pages/detail/ServerLogPage.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -28,18 +27,6 @@ const router = createRouter({
|
|||||||
name: "RootLayout",
|
name: "RootLayout",
|
||||||
component: RootLayout,
|
component: RootLayout,
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
path: "/login",
|
|
||||||
name: "LoginPage",
|
|
||||||
meta: {
|
|
||||||
depth: 1,
|
|
||||||
ignoreConnectCheck: true,
|
|
||||||
navBarVisible: false,
|
|
||||||
tabBarVisible: false,
|
|
||||||
bodyBackground: "#FFF"
|
|
||||||
},
|
|
||||||
component: LoginPage
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/server-index",
|
path: "/server-index",
|
||||||
name: "ServerIndexPage",
|
name: "ServerIndexPage",
|
||||||
@@ -63,16 +50,17 @@ const router = createRouter({
|
|||||||
children: [
|
children: [
|
||||||
...tabs,
|
...tabs,
|
||||||
{
|
{
|
||||||
path: "/files/detail/:id",
|
path: "/files/:pathMatch(.*)+",
|
||||||
name: "FileDetailPage",
|
name: "FileExplorerPage",
|
||||||
meta: {
|
meta: {
|
||||||
depth: 3,
|
dynamicDepth: true,
|
||||||
|
baseDepth: 2,
|
||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarCanBack: true,
|
navBarCanBack: true,
|
||||||
navBarTitle: "文件详情",
|
navBarTitle: "文件",
|
||||||
tabBarVisible: false
|
tabBarVisible: false
|
||||||
},
|
},
|
||||||
component: FileDetailPage
|
component: FileExplorerPage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/server/logs",
|
path: "/server/logs",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ const tabs: RouteRecordRaw[] = [
|
|||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarTitle: "文件",
|
navBarTitle: "文件",
|
||||||
tabBarVisible: true,
|
tabBarVisible: true,
|
||||||
tabBarPadding: true
|
tabBarPadding: true,
|
||||||
|
bodyBackground: "#FFF"
|
||||||
},
|
},
|
||||||
component: FilePage
|
component: FilePage
|
||||||
},
|
},
|
||||||
@@ -24,7 +25,8 @@ const tabs: RouteRecordRaw[] = [
|
|||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
navBarTitle: "状态",
|
navBarTitle: "状态",
|
||||||
tabBarVisible: true,
|
tabBarVisible: true,
|
||||||
tabBarPadding: true
|
tabBarPadding: true,
|
||||||
|
bodyBackground: "#FFF"
|
||||||
},
|
},
|
||||||
component: ServerStatusPage
|
component: ServerStatusPage
|
||||||
},
|
},
|
||||||
@@ -37,7 +39,7 @@ const tabs: RouteRecordRaw[] = [
|
|||||||
navBarTitle: "设置",
|
navBarTitle: "设置",
|
||||||
tabBarVisible: true,
|
tabBarVisible: true,
|
||||||
tabBarPadding: true,
|
tabBarPadding: true,
|
||||||
bodyBackground: "var(--app-bg)"
|
bodyBackground: "#FFF"
|
||||||
},
|
},
|
||||||
component: SettingsPage
|
component: SettingsPage
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user