A:剩余代码提交

This commit is contained in:
jimmy committed 2025-08-16 13:17:31 +08:00
1 parent 3d63040636
commit 5813015407
8 files changed
+1326 -101

No files matched your search

+79 -29
View File
@@ -1,4 +1,4 @@
/* ú@7ÍnŒ;˜M */
/* 基础样式重置和主题适配 */
*,
*::before,
*::after {
@@ -11,9 +11,11 @@ body {
min-height: 100vh;
color: var(--text-color);
background: var(--bg-color);
transition: color 0.3s, background-color 0.3s;
line-height: 1.6;
transition: color var(--transition-normal), background-color var(--transition-normal);
line-height: var(--line-height-normal);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-size: var(--font-size-base);
font-weight: var(--font-weight-normal);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
@@ -30,14 +32,14 @@ body {
a {
color: var(--text-color);
text-decoration: none;
transition: color 0.3s;
transition: color var(--transition-normal), opacity var(--transition-normal);
}
a:hover {
opacity: 0.8;
opacity: var(--opacity-hover);
}
/* nÝ@ ¹hý(;˜ØÏ */
/* 确保所有容器都使用主题变量 */
.n-layout,
.n-layout-header,
.n-layout-sider,
@@ -45,43 +47,91 @@ a:hover {
.n-layout-footer {
background-color: var(--bg-color) !important;
color: var(--text-color) !important;
transition: background-color 0.3s, color 0.3s;
transition: background-color var(--transition-normal), color var(--transition-normal);
}
/* ñr! „Ú¨a7 */
html.dark ::-webkit-scrollbar {
width: 8px;
height: 8px;
/* 确保所有文本元素都使用主题颜色 */
h1, h2, h3, h4, h5, h6, p, span, div {
color: inherit;
}
html.dark ::-webkit-scrollbar-track {
background: var(--bg-color-secondary);
/* 输入框样式适配 */
input, textarea, select {
background-color: var(--input-bg);
color: var(--text-color);
border-color: var(--border-color);
transition: all var(--transition-normal);
}
html.dark ::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
/* 按钮基础样式适配 */
button {
background-color: var(--button-bg);
color: var(--text-color);
border-color: var(--border-color);
transition: all var(--transition-normal);
}
html.dark ::-webkit-scrollbar-thumb:hover {
background: var(--text-color-secondary);
/* 通用滚动条样式 */
::-webkit-scrollbar {
width: var(--spacing-sm);
height: var(--spacing-sm);
}
/* Er! „Ú¨a7 */
html.light ::-webkit-scrollbar {
width: 8px;
height: 8px;
::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
html.light ::-webkit-scrollbar-track {
background: #f1f1f1;
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: var(--radius-sm);
transition: background-color var(--transition-fast);
}
html.light ::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
html.light ::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
/* 文本选择样式 */
::selection {
background-color: var(--selection-bg);
color: var(--selection-color);
}
/* 焦点样式 */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* 禁用元素样式 */
:disabled {
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
/* 通用过渡效果 */
.transition-colors {
transition: color var(--transition-normal), background-color var(--transition-normal), border-color var(--transition-normal);
}
.transition-all {
transition: all var(--transition-normal);
}
.transition-fast {
transition: all var(--transition-fast);
}
/* 通用阴影类 */
.shadow-sm {
box-shadow: var(--shadow-sm);
}
.shadow-md {
box-shadow: var(--shadow-md);
}
.shadow-lg {
box-shadow: var(--shadow-lg);
}
+196
View File
@@ -0,0 +1,196 @@
/* 统一颜色管理系统 */
:root {
/* ============================================
* 基础颜色调色板
* ============================================ */
/* 中性色调色板 */
--neutral-50: #fafafa;
--neutral-100: #f5f5f5;
--neutral-200: #e5e5e5;
--neutral-300: #d4d4d4;
--neutral-400: #a3a3a3;
--neutral-500: #737373;
--neutral-600: #525252;
--neutral-700: #404040;
--neutral-800: #262626;
--neutral-900: #171717;
--neutral-950: #0a0a0a;
/* 蓝色调色板 */
--blue-50: #eff6ff;
--blue-100: #dbeafe;
--blue-500: #3b82f6;
--blue-600: #2563eb;
--blue-700: #1d4ed8;
/* 红色调色板 */
--red-50: #fef2f2;
--red-100: #fee2e2;
--red-400: #f87171;
--red-500: #ef4444;
--red-600: #dc2626;
--red-700: #b91c1c;
--red-danger: #d03050;
/* 绿色调色板 */
--green-50: #f0fdf4;
--green-100: #dcfce7;
--green-500: #22c55e;
--green-600: #16a34a;
/* 黄色调色板 */
--yellow-50: #fefce8;
--yellow-100: #fef3c7;
--yellow-500: #eab308;
--yellow-600: #ca8a04;
/* ============================================
* 语义化颜色变量
* ============================================ */
/* 状态颜色 */
--color-success: var(--green-600);
--color-warning: var(--yellow-500);
--color-error: var(--red-500);
--color-danger: var(--red-danger);
--color-info: var(--blue-500);
/* 交互颜色 */
--color-primary: var(--blue-600);
--color-primary-hover: var(--blue-700);
--color-secondary: var(--neutral-600);
--color-secondary-hover: var(--neutral-700);
/* ============================================
* 透明度变量
* ============================================ */
--opacity-disabled: 0.5;
--opacity-hover: 0.8;
--opacity-focus: 0.9;
--opacity-overlay: 0.6;
/* ============================================
* 阴影颜色变量
* ============================================ */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
}
/* ============================================
* 浅色主题颜色映射
* ============================================ */
:root,
html.light {
/* 背景色 */
--bg-color: #ffffff;
--bg-color-secondary: var(--neutral-100);
--bg-color-tertiary: var(--neutral-50);
--bg-elevated: #ffffff;
/* 文字颜色 */
--text-color: var(--neutral-900);
--text-color-secondary: var(--neutral-600);
--text-color-tertiary: var(--neutral-500);
--text-color-disabled: var(--neutral-400);
--text-color-inverse: #ffffff;
/* 边框颜色 */
--border-color: var(--neutral-300);
--border-color-light: var(--neutral-200);
--border-color-strong: var(--neutral-400);
/* 交互状态颜色 */
--hover-bg: rgba(0, 0, 0, 0.04);
--hover-bg-secondary: rgba(0, 0, 0, 0.08);
--active-bg: rgba(0, 0, 0, 0.12);
--focus-ring: rgba(59, 130, 246, 0.3);
/* 组件背景色 */
--header-bg: #ffffff;
--sidebar-bg: #ffffff;
--content-bg: var(--neutral-100);
--card-bg: #ffffff;
--input-bg: #ffffff;
--button-bg: #ffffff;
/* 阴影颜色 */
--shadow-color: rgba(0, 0, 0, 0.08);
--shadow-color-light: rgba(0, 0, 0, 0.04);
--shadow-color-strong: rgba(0, 0, 0, 0.12);
}
/* ============================================
* 深色主题颜色映射
* ============================================ */
html.dark {
/* 背景色 */
--bg-color: var(--neutral-950);
--bg-color-secondary: var(--neutral-900);
--bg-color-tertiary: var(--neutral-800);
--bg-elevated: var(--neutral-900);
/* 文字颜色 */
--text-color: var(--neutral-50);
--text-color-secondary: var(--neutral-300);
--text-color-tertiary: var(--neutral-400);
--text-color-disabled: var(--neutral-600);
--text-color-inverse: var(--neutral-900);
/* 边框颜色 */
--border-color: var(--neutral-700);
--border-color-light: var(--neutral-800);
--border-color-strong: var(--neutral-600);
/* 交互状态颜色 */
--hover-bg: rgba(255, 255, 255, 0.08);
--hover-bg-secondary: rgba(255, 255, 255, 0.12);
--active-bg: rgba(255, 255, 255, 0.16);
--focus-ring: rgba(59, 130, 246, 0.4);
/* 组件背景色 */
--header-bg: var(--neutral-950);
--sidebar-bg: var(--neutral-950);
--content-bg: var(--neutral-900);
--card-bg: var(--neutral-900);
--input-bg: var(--neutral-900);
--button-bg: var(--neutral-900);
/* 阴影颜色 */
--shadow-color: rgba(0, 0, 0, 0.3);
--shadow-color-light: rgba(0, 0, 0, 0.2);
--shadow-color-strong: rgba(0, 0, 0, 0.4);
/* 深色模式特有颜色调整 */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.5);
}
/* ============================================
* 特殊用途颜色变量
* ============================================ */
:root {
/* 分割线颜色 */
--divider-color: var(--border-color-light);
--divider-color-strong: var(--border-color);
/* 遮罩颜色 */
--overlay-color: rgba(0, 0, 0, var(--opacity-overlay));
--modal-backdrop: rgba(0, 0, 0, 0.5);
/* 滚动条颜色 */
--scrollbar-track: var(--bg-color-secondary);
--scrollbar-thumb: var(--border-color);
--scrollbar-thumb-hover: var(--text-color-secondary);
/* 选择颜色 */
--selection-bg: rgba(59, 130, 246, 0.2);
--selection-color: var(--text-color);
}
+25 -19
View File
@@ -1,7 +1,10 @@
@import "colors.css";
@import "var.css";
@import "base.css";
@import "theme-override.css";
/* 应用特定样式 */
/* 禁止选中 */
.no-select {
user-select: none;
@@ -10,30 +13,33 @@
-ms-user-select: none; /* IE10+/Edge */
}
/* 全局容器样式,确保主题一致性 */
html, body {
margin: 0;
/* 工具类 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* 确保所有文本元素都使用主题颜色 */
h1, h2, h3, h4, h5, h6, p, span, div {
color: inherit;
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 输入框样式适配 */
input, textarea, select {
background-color: var(--bg-color);
color: var(--text-color);
border-color: var(--border-color);
.text-center {
text-align: center;
}
/* 按钮基础样式适配 */
button {
background-color: var(--bg-color-secondary);
color: var(--text-color);
border-color: var(--border-color);
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
+58 -39
View File
@@ -1,42 +1,61 @@
/* 应用程序特定变量 */
:root {
/* 布局尺寸变量 */
--header-height: 64px;
}
/* 浅色主题变量 */
:root,
html.light {
--bg-color: #ffffff;
--bg-color-secondary: #f5f5f5;
--bg-color-tertiary: #fafafa;
--text-color: #333333;
--text-color-secondary: #666666;
--text-color-tertiary: #999999;
--border-color: #e0e0e6;
--border-color-light: #f0f0f0;
--shadow-color: rgba(0, 0, 0, 0.08);
--shadow-color-light: rgba(0, 0, 0, 0.04);
--header-bg: #ffffff;
--sidebar-bg: #ffffff;
--content-bg: #f5f5f5;
--card-bg: #ffffff;
--hover-bg: rgba(0, 0, 0, 0.04);
}
/* 深色主题变量 */
html.dark {
--bg-color: #1a1a1a;
--bg-color-secondary: #2d2d2d;
--bg-color-tertiary: #3a3a3a;
--text-color: #ffffff;
--text-color-secondary: #cccccc;
--text-color-tertiary: #999999;
--border-color: #3d3d3d;
--border-color-light: #4a4a4a;
--shadow-color: rgba(0, 0, 0, 0.3);
--shadow-color-light: rgba(0, 0, 0, 0.2);
--header-bg: #1a1a1a;
--sidebar-bg: #1a1a1a;
--content-bg: #2d2d2d;
--card-bg: #2d2d2d;
--hover-bg: rgba(255, 255, 255, 0.08);
--sidebar-width: 290px;
--sidebar-collapsed-width: 64px;
/* 间距变量 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* 圆角变量 */
--radius-none: 0px;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* 过渡动画变量 */
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
/* 字体变量 */
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 1.875rem; /* 30px */
/* 字重变量 */
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* 行高变量 */
--line-height-none: 1;
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.625;
--line-height-loose: 2;
/* z-index层级管理 */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
--z-toast: 1080;
}
@@ -221,7 +221,7 @@ const handleMenuSelect = (key: string) => {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
padding: 0 var(--spacing-lg);
max-width: 100%;
}
@@ -239,7 +239,7 @@ const handleMenuSelect = (key: string) => {
}
.logo-icon {
margin-right: 12px;
margin-right: var(--spacing-md);
}
.logo-icon img {
@@ -249,8 +249,8 @@ const handleMenuSelect = (key: string) => {
}
.site-title {
font-size: 20px;
font-weight: 600;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-semibold);
color: var(--text-color);
white-space: nowrap;
}
@@ -374,15 +374,36 @@ const handleMenuSelect = (key: string) => {
:deep(.n-menu-item) {
color: var(--text-color) !important;
margin-right: 12px !important;
}
/* 折叠状态下的菜单项样式 */
:deep(.n-layout-sider--collapsed .n-menu-item) {
margin-right: 0 !important;
margin-left: 0 !important;
}
:deep(.n-layout-sider--collapsed .n-menu-item:hover) {
margin-right: 0 !important;
margin-left: 0 !important;
}
:deep(.n-layout-sider--collapsed .n-menu-item.n-menu-item--selected) {
margin-right: 0 !important;
margin-left: 0 !important;
}
:deep(.n-menu-item:hover) {
background-color: var(--hover-bg) !important;
margin-right: 12px !important;
border-radius: 6px !important;
}
:deep(.n-menu-item.n-menu-item--selected) {
background-color: var(--hover-bg) !important;
color: var(--text-color) !important;
margin-right: 12px !important;
border-radius: 6px !important;
}
:deep(.n-input) {
+940 -7
View File
@@ -1,11 +1,944 @@
<script setup lang="ts">
</script>
<template>
文件夹
<div class="file-explorer">
<!-- 左侧目录树 -->
<div class="sidebar" :class="{ collapsed: sidebarCollapsed }">
<div class="sidebar-header">
<n-button quaternary circle size="small" @click="toggleSidebar">
<template #icon>
<n-icon><menu-outline /></n-icon>
</template>
</n-button>
<span v-if="!sidebarCollapsed" class="sidebar-title">文件夹</span>
</div>
<div v-if="!sidebarCollapsed" class="directory-tree">
<n-tree
:data="directoryTree"
:selected-keys="selectedKeys"
:expanded-keys="expandedKeys"
selectable
expand-on-click
@update:selected-keys="handleDirectorySelect"
@update:expanded-keys="handleDirectoryExpand"
>
<template #prefix="{ option }">
<n-icon :color="option.type === 'folder' ? 'var(--color-warning)' : 'var(--text-color-secondary)'">
<folder-outline v-if="option.type === 'folder'" />
<image-outline v-else-if="option.type === 'images'" />
<document-outline v-else />
</n-icon>
</template>
</n-tree>
</div>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 顶部工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<!-- 面包屑导航 -->
<n-breadcrumb class="breadcrumb">
<n-breadcrumb-item
v-for="(item, index) in breadcrumbItems"
:key="index"
@click="navigateTo(item.path)"
>
<template #icon>
<n-icon><home-outline v-if="index === 0" /><folder-outline v-else /></n-icon>
</template>
{{ item.label }}
</n-breadcrumb-item>
</n-breadcrumb>
</div>
<div class="toolbar-center">
<!-- 搜索框 -->
<n-input
v-model:value="searchQuery"
placeholder="搜索文件和文件夹..."
class="search-input"
clearable
>
<template #prefix>
<n-icon><search-outline /></n-icon>
</template>
</n-input>
</div>
<div class="toolbar-right">
<!-- 视图模式切换 -->
<n-button-group>
<n-button
:type="viewMode === 'grid' ? 'primary' : 'default'"
quaternary
@click="setViewMode('grid')"
>
<template #icon>
<n-icon><grid-outline /></n-icon>
</template>
</n-button>
<n-button
:type="viewMode === 'list' ? 'primary' : 'default'"
quaternary
@click="setViewMode('list')"
>
<template #icon>
<n-icon><list-outline /></n-icon>
</template>
</n-button>
</n-button-group>
<!-- 排序方式 -->
<n-dropdown :options="sortOptions" @select="handleSortSelect">
<n-button quaternary>
<template #icon>
<n-icon><funnel-outline /></n-icon>
</template>
排序
</n-button>
</n-dropdown>
<!-- 缩放控制 -->
<div class="zoom-controls">
<n-button quaternary size="small" @click="zoomOut" :disabled="zoomLevel <= 1">
<template #icon>
<n-icon><remove-outline /></n-icon>
</template>
</n-button>
<span class="zoom-level">{{ Math.round(zoomLevel * 100) }}%</span>
<n-button quaternary size="small" @click="zoomIn" :disabled="zoomLevel >= 3">
<template #icon>
<n-icon><add-outline /></n-icon>
</template>
</n-button>
</div>
<!-- 更多操作 -->
<n-dropdown :options="moreOptions" @select="handleMoreAction">
<n-button quaternary>
<template #icon>
<n-icon><ellipsis-vertical-outline /></n-icon>
</template>
</n-button>
</n-dropdown>
</div>
</div>
<!-- 文件显示区域 -->
<div class="file-area" @wheel="handleWheel" @click="clearSelection">
<!-- 网格视图 -->
<div v-if="viewMode === 'grid'" class="file-grid" :style="{ '--zoom-level': zoomLevel }">
<div
v-for="file in filteredFiles"
:key="file.id"
class="file-item"
:class="{
selected: selectedFiles.includes(file.id),
'is-image': file.type === 'image'
}"
@click="handleFileClick(file, $event)"
@dblclick="handleFileDoubleClick(file)"
>
<!-- 图片预览 -->
<div v-if="file.type === 'image'" class="file-preview image-preview">
<img :src="file.thumbnail" :alt="file.name" @load="handleImageLoad" />
<div class="image-overlay">
<div class="image-info">
<span class="image-resolution">{{ file.resolution }}</span>
</div>
</div>
</div>
<!-- 非图片文件图标 -->
<div v-else class="file-preview icon-preview" :class="`file-type-${file.type}`">
<n-icon size="48" :color="getFileTypeColor(file.type)">
<component :is="getFileIcon(file.type)" />
</n-icon>
</div>
<div class="file-info">
<div class="file-name" :title="file.name">{{ file.name }}</div>
<div class="file-meta">
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<span class="file-date">{{ formatDate(file.modifiedAt) }}</span>
</div>
</div>
</div>
</div>
<!-- 列表视图 -->
<div v-else class="file-list">
<n-data-table
:columns="listColumns"
:data="filteredFiles"
:row-key="(row) => row.id"
:checked-row-keys="selectedFiles"
@update:checked-row-keys="handleRowSelection"
>
<template #name="{ row }">
<div class="list-file-name">
<n-icon :color="getFileTypeColor(row.type)">
<component :is="getFileIcon(row.type)" />
</n-icon>
<span>{{ row.name }}</span>
</div>
</template>
<template #size="{ row }">
{{ formatFileSize(row.size) }}
</template>
<template #modifiedAt="{ row }">
{{ formatDate(row.modifiedAt) }}
</template>
</n-data-table>
</div>
<!-- 空状态 -->
<div v-if="filteredFiles.length === 0" class="empty-state">
<n-empty description="此文件夹为空">
<template #icon>
<n-icon size="64" color="var(--text-color-tertiary)">
<folder-outline />
</n-icon>
</template>
</n-empty>
</div>
</div>
</div>
<!-- 预览模态框 -->
<n-modal
v-model:show="previewVisible"
preset="card"
:title="currentPreviewFile?.name"
size="huge"
:bordered="false"
class="preview-modal"
>
<div v-if="currentPreviewFile" class="preview-content">
<img
v-if="currentPreviewFile.type === 'image'"
:src="currentPreviewFile.url"
:alt="currentPreviewFile.name"
class="preview-image"
/>
<div v-else class="preview-placeholder">
<n-icon size="64" :color="getFileTypeColor(currentPreviewFile.type)">
<component :is="getFileIcon(currentPreviewFile.type)" />
</n-icon>
<p>无法预览此文件类型</p>
</div>
</div>
<div class="preview-controls">
<n-button @click="previousFile" :disabled="currentFileIndex <= 0">
<template #icon>
<n-icon><chevron-back-outline /></n-icon>
</template>
上一个
</n-button>
<span>{{ currentFileIndex + 1 }} / {{ filteredFiles.length }}</span>
<n-button @click="nextFile" :disabled="currentFileIndex >= filteredFiles.length - 1">
下一个
<template #icon>
<n-icon><chevron-forward-outline /></n-icon>
</template>
</n-button>
</div>
</n-modal>
<!-- 右键菜单 -->
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="contextMenuX"
:y="contextMenuY"
:options="contextMenuOptions"
:show="showContextMenu"
@select="handleContextMenuSelect"
@clickoutside="showContextMenu = false"
/>
</div>
</template>
<style scoped>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import {
NButton,
NButtonGroup,
NInput,
NIcon,
NTree,
NBreadcrumb,
NBreadcrumbItem,
NDropdown,
NDataTable,
NEmpty,
NModal
} from 'naive-ui'
import {
MenuOutline,
FolderOutline,
ImageOutline,
DocumentOutline,
HomeOutline,
SearchOutline,
GridOutline,
ListOutline,
FunnelOutline,
AddOutline,
RemoveOutline,
EllipsisVerticalOutline,
ChevronBackOutline,
ChevronForwardOutline,
VideocamOutline,
MusicalNoteOutline,
ArchiveOutline,
CodeSlashOutline
} from '@vicons/ionicons5'
</style>
// 状态管理
const sidebarCollapsed = ref(false)
const viewMode = ref<'grid' | 'list'>('grid')
const zoomLevel = ref(1.2)
const searchQuery = ref('')
const currentPath = ref('/Photos')
const selectedFiles = ref<string[]>([])
const selectedKeys = ref<string[]>(['photos'])
const expandedKeys = ref<string[]>(['root'])
const previewVisible = ref(false)
const currentPreviewFile = ref<any>(null)
const showContextMenu = ref(false)
const contextMenuX = ref(0)
const contextMenuY = ref(0)
// 假数据
const directoryTree = ref([
{
key: 'root',
label: '我的照片',
type: 'folder',
children: [
{
key: 'photos',
label: '照片',
type: 'images',
children: [
{ key: 'photos-2024', label: '2024', type: 'folder' },
{ key: 'photos-2023', label: '2023', type: 'folder' }
]
},
{ key: 'videos', label: '视频', type: 'folder' },
{ key: 'documents', label: '文档', type: 'folder' },
{ key: 'downloads', label: '下载', type: 'folder' }
]
}
])
const allFiles = ref([
{
id: '1',
name: 'sunset-beach.jpg',
type: 'image',
size: 2048576,
modifiedAt: '2024-01-15T10:30:00Z',
thumbnail: 'https://picsum.photos/300/200?random=1',
url: 'https://picsum.photos/1920/1080?random=1',
resolution: '1920×1080'
},
{
id: '2',
name: 'mountain-view.jpg',
type: 'image',
size: 3145728,
modifiedAt: '2024-01-14T15:45:00Z',
thumbnail: 'https://picsum.photos/300/200?random=2',
url: 'https://picsum.photos/1920/1080?random=2',
resolution: '1920×1080'
},
{
id: '3',
name: 'presentation.pdf',
type: 'document',
size: 1048576,
modifiedAt: '2024-01-13T09:15:00Z'
},
{
id: '4',
name: 'holiday-video.mp4',
type: 'video',
size: 52428800,
modifiedAt: '2024-01-12T20:00:00Z'
},
{
id: '5',
name: 'project-files.zip',
type: 'archive',
size: 10485760,
modifiedAt: '2024-01-11T11:30:00Z'
},
{
id: '6',
name: 'script.js',
type: 'code',
size: 4096,
modifiedAt: '2024-01-10T16:20:00Z'
}
])
// 计算属性
const filteredFiles = computed(() => {
let files = allFiles.value
if (searchQuery.value) {
files = files.filter(file =>
file.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
return files
})
const breadcrumbItems = computed(() => {
const parts = currentPath.value.split('/').filter(Boolean)
const items = [{ label: '主页', path: '/' }]
let currentFullPath = ''
for (const part of parts) {
currentFullPath += '/' + part
items.push({ label: part, path: currentFullPath })
}
return items
})
const currentFileIndex = computed(() => {
if (!currentPreviewFile.value) return -1
return filteredFiles.value.findIndex(file => file.id === currentPreviewFile.value.id)
})
// 排序和更多操作选项
const sortOptions = ref([
{ label: '按名称排序', key: 'name' },
{ label: '按大小排序', key: 'size' },
{ label: '按修改时间排序', key: 'modifiedAt' },
{ label: '按类型排序', key: 'type' }
])
const moreOptions = ref([
{ label: '新建文件夹', key: 'new-folder' },
{ label: '上传文件', key: 'upload' },
{ label: '刷新', key: 'refresh' },
{ label: '显示隐藏文件', key: 'show-hidden' }
])
const contextMenuOptions = computed(() => [
{ label: '打开', key: 'open' },
{ label: '重命名', key: 'rename' },
{ label: '删除', key: 'delete', disabled: selectedFiles.value.length === 0 },
{ type: 'divider' },
{ label: '复制', key: 'copy' },
{ label: '剪切', key: 'cut' },
{ label: '粘贴', key: 'paste' },
{ type: 'divider' },
{ label: '属性', key: 'properties' }
])
// 列表视图的表格列定义
const listColumns = [
{ type: 'selection' },
{ title: '名称', key: 'name', render: 'name' },
{ title: '大小', key: 'size', render: 'size', width: 120 },
{ title: '修改时间', key: 'modifiedAt', render: 'modifiedAt', width: 180 }
]
// 方法
const toggleSidebar = () => {
sidebarCollapsed.value = !sidebarCollapsed.value
}
const setViewMode = (mode: 'grid' | 'list') => {
viewMode.value = mode
}
const zoomIn = () => {
if (zoomLevel.value < 3) {
zoomLevel.value += 0.2
}
}
const zoomOut = () => {
if (zoomLevel.value > 0.6) {
zoomLevel.value -= 0.2
}
}
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault()
if (e.deltaY < 0) {
zoomIn()
} else {
zoomOut()
}
}
}
const handleDirectorySelect = (keys: string[]) => {
selectedKeys.value = keys
if (keys.length > 0) {
// 这里可以根据选中的目录加载相应的文件
console.log('Selected directory:', keys[0])
}
}
const handleDirectoryExpand = (keys: string[]) => {
expandedKeys.value = keys
}
const navigateTo = (path: string) => {
currentPath.value = path
// 加载对应路径的文件
}
const handleFileClick = (file: any, event: MouseEvent) => {
event.stopPropagation()
if (event.ctrlKey || event.metaKey) {
// Ctrl+点击多选
if (selectedFiles.value.includes(file.id)) {
selectedFiles.value = selectedFiles.value.filter(id => id !== file.id)
} else {
selectedFiles.value.push(file.id)
}
} else if (event.shiftKey && selectedFiles.value.length > 0) {
// Shift+点击范围选择
const lastSelected = selectedFiles.value[selectedFiles.value.length - 1]
const lastIndex = filteredFiles.value.findIndex(f => f.id === lastSelected)
const currentIndex = filteredFiles.value.findIndex(f => f.id === file.id)
const start = Math.min(lastIndex, currentIndex)
const end = Math.max(lastIndex, currentIndex)
selectedFiles.value = filteredFiles.value.slice(start, end + 1).map(f => f.id)
} else {
// 单选
selectedFiles.value = [file.id]
}
}
const handleFileDoubleClick = (file: any) => {
if (file.type === 'image') {
currentPreviewFile.value = file
previewVisible.value = true
} else {
// 其他文件类型的打开逻辑
console.log('Opening file:', file.name)
}
}
const clearSelection = () => {
selectedFiles.value = []
}
const handleRowSelection = (keys: string[]) => {
selectedFiles.value = keys
}
const previousFile = () => {
if (currentFileIndex.value > 0) {
currentPreviewFile.value = filteredFiles.value[currentFileIndex.value - 1]
}
}
const nextFile = () => {
if (currentFileIndex.value < filteredFiles.value.length - 1) {
currentPreviewFile.value = filteredFiles.value[currentFileIndex.value + 1]
}
}
const handleSortSelect = (key: string) => {
console.log('Sort by:', key)
}
const handleMoreAction = (key: string) => {
console.log('More action:', key)
}
const handleContextMenuSelect = (key: string) => {
console.log('Context menu:', key)
showContextMenu.value = false
}
const handleImageLoad = (event: Event) => {
// 图片加载完成的处理
}
// 工具函数
const getFileIcon = (type: string) => {
const iconMap: { [key: string]: any } = {
image: ImageOutline,
video: VideocamOutline,
audio: MusicalNoteOutline,
document: DocumentOutline,
archive: ArchiveOutline,
code: CodeSlashOutline,
folder: FolderOutline
}
return iconMap[type] || DocumentOutline
}
const getFileTypeColor = (type: string) => {
const colorMap: { [key: string]: string } = {
image: 'var(--color-success)',
video: 'var(--color-info)',
audio: 'var(--color-secondary)',
document: 'var(--text-color-secondary)',
archive: 'var(--color-warning)',
code: 'var(--color-primary)',
folder: 'var(--color-warning)'
}
return colorMap[type] || 'var(--text-color-secondary)'
}
const formatFileSize = (bytes: number) => {
const sizes = ['B', 'KB', 'MB', 'GB']
if (bytes === 0) return '0 B'
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
onMounted(() => {
// 组件挂载后的初始化
})
</script>
<style scoped>
.file-explorer {
display: flex;
height: 100vh;
background: var(--bg-color);
overflow: hidden;
}
/* 左侧边栏 */
.sidebar {
width: 280px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: width var(--transition-normal);
}
.sidebar.collapsed {
width: 60px;
}
.sidebar-header {
display: flex;
align-items: center;
padding: var(--spacing-md);
border-bottom: 1px solid var(--border-color-light);
gap: var(--spacing-sm);
}
.sidebar-title {
font-weight: var(--font-weight-semibold);
color: var(--text-color);
}
.directory-tree {
flex: 1;
padding: var(--spacing-sm);
overflow: auto;
}
/* 主内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 工具栏 */
.toolbar {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--header-bg);
border-bottom: 1px solid var(--border-color-light);
min-height: 60px;
}
.toolbar-left {
min-width: 200px;
}
.toolbar-center {
flex: 1;
max-width: 400px;
}
.toolbar-right {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.search-input {
width: 100%;
}
.breadcrumb :deep(.n-breadcrumb-item) {
cursor: pointer;
}
.zoom-controls {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: 0 var(--spacing-sm);
}
.zoom-level {
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
min-width: 40px;
text-align: center;
}
/* 文件区域 */
.file-area {
flex: 1;
padding: var(--spacing-lg);
overflow: auto;
background: var(--content-bg);
}
/* 网格视图 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(calc(160px * var(--zoom-level)), 1fr));
gap: calc(var(--spacing-md) * var(--zoom-level));
padding: var(--spacing-sm);
}
.file-item {
background: var(--card-bg);
border: 2px solid transparent;
border-radius: var(--radius-md);
padding: var(--spacing-sm);
cursor: pointer;
transition: all var(--transition-fast);
user-select: none;
}
.file-item:hover {
border-color: var(--border-color);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.file-item.selected {
border-color: var(--color-primary);
background: rgba(59, 130, 246, 0.08);
}
.file-item.is-image .file-preview {
aspect-ratio: 4/3;
}
.file-preview {
position: relative;
border-radius: var(--radius-sm);
overflow: hidden;
margin-bottom: var(--spacing-sm);
background: var(--bg-color-secondary);
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.7));
opacity: 0;
transition: opacity var(--transition-fast);
display: flex;
align-items: flex-end;
}
.file-item:hover .image-overlay {
opacity: 1;
}
.image-info {
padding: var(--spacing-sm);
color: white;
font-size: var(--font-size-xs);
}
.icon-preview {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 4/3;
background: var(--bg-color-tertiary);
}
.file-info {
text-align: center;
}
.file-name {
font-weight: var(--font-weight-medium);
color: var(--text-color);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-xs);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
display: flex;
justify-content: space-between;
font-size: var(--font-size-xs);
color: var(--text-color-secondary);
gap: var(--spacing-xs);
}
/* 列表视图 */
.file-list {
background: var(--card-bg);
border-radius: var(--radius-md);
overflow: hidden;
}
.list-file-name {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* 空状态 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
}
/* 预览模态框 */
.preview-modal {
max-width: 90vw;
max-height: 90vh;
}
.preview-content {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
background: var(--bg-color-secondary);
border-radius: var(--radius-md);
overflow: hidden;
}
.preview-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
color: var(--text-color-secondary);
}
.preview-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color-light);
}
/* 文件类型颜色 */
.file-type-document {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.file-type-video {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
}
.file-type-audio {
background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%);
}
.file-type-archive {
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
}
.file-type-code {
background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%);
}
/* 响应式设计 */
@media (max-width: 768px) {
.file-explorer {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
max-height: 200px;
}
.toolbar {
flex-wrap: wrap;
gap: var(--spacing-sm);
}
.toolbar-left,
.toolbar-center {
min-width: auto;
flex: 1;
}
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
}
@media (max-width: 480px) {
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
}
</style>
@@ -245,7 +245,7 @@ onMounted(() => {
.library-tag:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: var(--shadow-md);
}
.path-text {
@@ -272,7 +272,7 @@ onMounted(() => {
}
.close-icon:hover {
color: #d03050;
color: var(--color-danger);
transform: scale(1.1);
}
+1 -1
View File
@@ -31,7 +31,7 @@ export default defineConfig({
},
},
server: {
port: 3049, // 设置你想要的端口号
port: 0, // 设置你想要的端口号
host: false, // 可选:允许外部访问
},
})