<script lang="ts" setup>
|
import type { Component } from 'vue';
|
|
import type { DmsPermissionResource } from '#/api/x/dms/permission';
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { formatToDateTime } from '@jnpf/utils';
|
|
import {
|
AppstoreOutlined,
|
FileExcelOutlined,
|
FileImageOutlined,
|
FileOutlined,
|
FilePdfOutlined,
|
FileTextOutlined,
|
FileWordOutlined,
|
FolderFilled,
|
FolderOutlined,
|
MenuFoldOutlined,
|
MenuUnfoldOutlined,
|
RightOutlined,
|
SearchOutlined,
|
UnorderedListOutlined,
|
} from '@ant-design/icons-vue';
|
import { Tree as ATree, Empty } from 'ant-design-vue';
|
|
import { getPermissionResourceTree } from '#/api/x/dms/permission';
|
|
import { dmsPreviewErrorMessage, openDmsFilePreview } from '../shared/dmsFilePreview';
|
|
interface ExplorerLocation {
|
folder: DmsPermissionResource;
|
path: DmsPermissionResource[];
|
}
|
|
interface FolderTreeNode {
|
children?: FolderTreeNode[];
|
isLeaf?: boolean;
|
key: string;
|
title: string;
|
}
|
|
interface FolderTreeLoadNode {
|
key: number | string;
|
}
|
|
const emit = defineEmits<{
|
folderChange: [folder: DmsPermissionResource];
|
select: [resource: DmsPermissionResource];
|
treeCollapse: [collapsed: boolean];
|
}>();
|
|
const { createMessage } = useMessage();
|
const loading = ref(false);
|
const roots = ref<DmsPermissionResource[]>([]);
|
const entries = ref<DmsPermissionResource[]>([]);
|
const currentFolder = ref<DmsPermissionResource>();
|
const currentPath = ref<DmsPermissionResource[]>([]);
|
const selectedId = ref('');
|
const keyword = ref('');
|
const viewMode = ref<'details' | 'tiles'>('details');
|
const folderTreeData = ref<FolderTreeNode[]>([]);
|
const expandedFolderKeys = ref<string[]>([]);
|
const treeCollapsed = ref(false);
|
const folderById = new Map<string, DmsPermissionResource>();
|
const folderPathById = new Map<string, DmsPermissionResource[]>();
|
let selectTimer: number | undefined;
|
|
const selectedFolderKeys = computed(() => (currentFolder.value ? [currentFolder.value.id] : []));
|
const emptyDescription = computed(() => {
|
if (keyword.value) return '没有匹配的项目';
|
return '此文件夹为空';
|
});
|
const visibleEntries = computed(() => {
|
const search = keyword.value.trim().toLowerCase();
|
return [...entries.value]
|
.filter((entry) => !search || displayName(entry).toLowerCase().includes(search))
|
.sort((left, right) => {
|
const typeDiff = resourceRank(left) - resourceRank(right);
|
return typeDiff || displayName(left).localeCompare(displayName(right), 'zh-CN') || left.id.localeCompare(right.id);
|
});
|
});
|
const selectedEntry = computed(() => entries.value.find((entry) => entry.id === selectedId.value));
|
|
function resourceRank(resource: DmsPermissionResource) {
|
if (resource.resourceType === 'FOLDER') return 0;
|
if (resource.resourceType === 'DOCUMENT') return 1;
|
return 2;
|
}
|
|
function displayName(resource: DmsPermissionResource) {
|
return resource.fileName || resource.documentName || resource.name;
|
}
|
|
function resourceTypeLabel(resource: DmsPermissionResource) {
|
if (resource.resourceType === 'FOLDER') return '文件夹';
|
if (resource.resourceType === 'FILE_VERSION') return `文件版本 v${resource.versionNo ?? '-'}`;
|
const suffix = displayName(resource).split('.').pop()?.toUpperCase();
|
return suffix && suffix !== displayName(resource).toUpperCase() ? `${suffix} 文件` : '文件';
|
}
|
|
function resourceIcon(resource: DmsPermissionResource): Component {
|
if (resource.resourceType === 'FOLDER') return FolderFilled;
|
const name = displayName(resource).toLowerCase();
|
if (name.endsWith('.pdf')) return FilePdfOutlined;
|
if (/\.(?:doc|docx)$/.test(name)) return FileWordOutlined;
|
if (/\.(?:xls|xlsx|csv)$/.test(name)) return FileExcelOutlined;
|
if (/\.(?:gif|jpe?g|png|webp)$/.test(name)) return FileImageOutlined;
|
if (/\.(?:md|rtf|txt)$/.test(name)) return FileTextOutlined;
|
return FileOutlined;
|
}
|
|
function resourceIconTone(resource: DmsPermissionResource) {
|
if (resource.resourceType === 'FOLDER') return 'folder';
|
const name = displayName(resource).toLowerCase();
|
if (name.endsWith('.pdf')) return 'pdf';
|
if (/\.(?:doc|docx)$/.test(name)) return 'word';
|
if (/\.(?:xls|xlsx|csv)$/.test(name)) return 'excel';
|
if (/\.(?:gif|jpe?g|png|webp)$/.test(name)) return 'image';
|
return 'file';
|
}
|
|
function displaySize(resource: DmsPermissionResource) {
|
if (resource.resourceType === 'FOLDER') return '—';
|
const bytes = resource.sizeBytes;
|
if (bytes === undefined || bytes === null) return '—';
|
if (bytes < 1024) return `${bytes} B`;
|
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`;
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
}
|
|
function displayDate(value?: string) {
|
return value ? formatToDateTime(value, 'YYYY-MM-DD') : '—';
|
}
|
|
function fileValue(resource: DmsPermissionResource, value: string) {
|
return resource.resourceType === 'FOLDER' ? '—' : value;
|
}
|
|
function validUntil(resource: DmsPermissionResource) {
|
if (resource.resourceType === 'FOLDER') return '—';
|
if (resource.effectiveTo) return displayDate(resource.effectiveTo);
|
return resource.lifecycleStatus === 'EFFECTIVE' ? '长期有效' : '—';
|
}
|
|
async function fetchEntries(folder: DmsPermissionResource) {
|
const response = await getPermissionResourceTree({ parentId: folder.id, pageSize: 100 });
|
return response.data || [];
|
}
|
|
function toFolderTreeNode(folder: DmsPermissionResource, current?: FolderTreeNode): FolderTreeNode {
|
return {
|
children: current?.children,
|
isLeaf: current?.isLeaf ?? false,
|
key: folder.id,
|
title: displayName(folder),
|
};
|
}
|
|
function findFolderTreeNode(nodes: FolderTreeNode[], key: string): FolderTreeNode | undefined {
|
for (const node of nodes) {
|
if (node.key === key) return node;
|
const child = node.children ? findFolderTreeNode(node.children, key) : undefined;
|
if (child) return child;
|
}
|
}
|
|
function replaceFolderTreeNode(nodes: FolderTreeNode[], key: string, replacement: FolderTreeNode): FolderTreeNode[] {
|
return nodes.map((node) => {
|
if (node.key === key) return replacement;
|
if (!node.children) return node;
|
return { ...node, children: replaceFolderTreeNode(node.children, key, replacement) };
|
});
|
}
|
|
function syncFolderTreeChildren(parent: DmsPermissionResource, resources: DmsPermissionResource[], path: DmsPermissionResource[]) {
|
folderById.set(parent.id, parent);
|
folderPathById.set(parent.id, path);
|
|
const parentNode = findFolderTreeNode(folderTreeData.value, parent.id);
|
if (!parentNode) return;
|
const currentChildren = new Map((parentNode.children || []).map((node) => [node.key, node]));
|
const folderChildren = resources
|
.filter((resource) => resource.resourceType === 'FOLDER')
|
.sort((left, right) => displayName(left).localeCompare(displayName(right), 'zh-CN') || left.id.localeCompare(right.id));
|
const children = folderChildren.map((folder) => {
|
folderById.set(folder.id, folder);
|
folderPathById.set(folder.id, [...path, folder]);
|
return toFolderTreeNode(folder, currentChildren.get(folder.id));
|
});
|
|
folderTreeData.value = replaceFolderTreeNode(folderTreeData.value, parent.id, {
|
...parentNode,
|
children,
|
isLeaf: children.length === 0,
|
});
|
}
|
|
function expandFolderPath(path: DmsPermissionResource[]) {
|
expandedFolderKeys.value = [...new Set([...expandedFolderKeys.value, ...path.map((folder) => folder.id)])];
|
}
|
|
async function applyLocation(location: ExplorerLocation) {
|
loading.value = true;
|
try {
|
const loadedEntries = await fetchEntries(location.folder);
|
entries.value = loadedEntries;
|
syncFolderTreeChildren(location.folder, loadedEntries, location.path);
|
expandFolderPath(location.path);
|
currentFolder.value = location.folder;
|
currentPath.value = location.path;
|
selectedId.value = location.folder.id;
|
emit('folderChange', location.folder);
|
emit('select', location.folder);
|
} catch {
|
entries.value = [];
|
createMessage.error('目录内容加载失败');
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function navigate(location: ExplorerLocation) {
|
await applyLocation(location);
|
}
|
|
async function loadRoots() {
|
loading.value = true;
|
try {
|
const response = await getPermissionResourceTree({ pageSize: 100 });
|
roots.value = (response.data || []).filter((resource) => resource.resourceType === 'FOLDER');
|
folderById.clear();
|
folderPathById.clear();
|
folderTreeData.value = roots.value.map((folder) => {
|
folderById.set(folder.id, folder);
|
folderPathById.set(folder.id, [folder]);
|
return toFolderTreeNode(folder);
|
});
|
expandedFolderKeys.value = [];
|
const first = roots.value[0];
|
if (first) await navigate({ folder: first, path: [first] });
|
} catch {
|
roots.value = [];
|
entries.value = [];
|
createMessage.error('文件根目录加载失败');
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function loadFolderTreeChildren(node: FolderTreeLoadNode) {
|
const folder = folderById.get(String(node.key));
|
if (!folder) return;
|
try {
|
const loadedEntries = await fetchEntries(folder);
|
syncFolderTreeChildren(folder, loadedEntries, folderPathById.get(folder.id) || [folder]);
|
} catch {
|
createMessage.error('子目录加载失败');
|
}
|
}
|
|
function handleFolderTreeExpand(keys: (number | string)[]) {
|
expandedFolderKeys.value = keys.map(String);
|
}
|
|
function handleFolderTreeSelect(keys: (number | string)[]) {
|
const key = keys[0];
|
if (key === undefined) return;
|
const folder = folderById.get(String(key));
|
const path = folder ? folderPathById.get(folder.id) : undefined;
|
if (!folder || !path) return;
|
if (folder.id === currentFolder.value?.id) {
|
selectEntry(folder);
|
return;
|
}
|
void navigate({ folder, path });
|
}
|
|
function toggleFolderTree() {
|
treeCollapsed.value = !treeCollapsed.value;
|
emit('treeCollapse', treeCollapsed.value);
|
}
|
|
async function openEntry(resource: DmsPermissionResource) {
|
if (selectedId.value !== resource.id) selectEntry(resource);
|
if (resource.resourceType === 'FOLDER') {
|
await navigate({ folder: resource, path: [...currentPath.value, resource] });
|
return;
|
}
|
try {
|
await openDmsFilePreview({
|
fileName: displayName(resource),
|
resourceId: resource.id,
|
resourceType: resource.resourceType,
|
});
|
} catch (error) {
|
createMessage.error(dmsPreviewErrorMessage(error));
|
}
|
}
|
|
function selectEntry(resource: DmsPermissionResource) {
|
selectedId.value = resource.id;
|
emit('select', resource);
|
}
|
|
function handleEntryClick(resource: DmsPermissionResource) {
|
clearSelectTimer();
|
selectTimer = window.setTimeout(() => {
|
selectTimer = undefined;
|
selectEntry(resource);
|
}, 250);
|
}
|
|
function handleEntryDoubleClick(resource: DmsPermissionResource) {
|
clearSelectTimer();
|
void openEntry(resource);
|
}
|
|
function clearSelectTimer() {
|
if (selectTimer === undefined) return;
|
window.clearTimeout(selectTimer);
|
selectTimer = undefined;
|
}
|
|
async function openBreadcrumb(index: number) {
|
const folder = currentPath.value[index];
|
if (!folder || folder.id === currentFolder.value?.id) return;
|
await navigate({ folder, path: currentPath.value.slice(0, index + 1) });
|
}
|
|
async function refresh() {
|
if (!currentFolder.value) return loadRoots();
|
await applyLocation({ folder: currentFolder.value, path: currentPath.value });
|
}
|
|
onMounted(loadRoots);
|
onBeforeUnmount(clearSelectTimer);
|
|
defineExpose({ refresh, reload: loadRoots });
|
</script>
|
|
<template>
|
<section class="resource-explorer">
|
<aside class="folder-navigation" :class="{ collapsed: treeCollapsed }">
|
<div class="folder-navigation-header">
|
<strong v-if="!treeCollapsed">目录</strong>
|
<a-tooltip :title="treeCollapsed ? '展开目录树' : '收起目录树'">
|
<a-button class="folder-navigation-toggle" :aria-label="treeCollapsed ? '展开目录树' : '收起目录树'" type="text" @click="toggleFolderTree">
|
<MenuUnfoldOutlined v-if="treeCollapsed" />
|
<MenuFoldOutlined v-else />
|
</a-button>
|
</a-tooltip>
|
</div>
|
<div v-if="!treeCollapsed" class="folder-tree-scroll">
|
<ATree
|
block-node
|
class="folder-tree"
|
:expanded-keys="expandedFolderKeys"
|
:load-data="loadFolderTreeChildren"
|
:selected-keys="selectedFolderKeys"
|
:tree-data="folderTreeData"
|
@expand="handleFolderTreeExpand"
|
@select="handleFolderTreeSelect">
|
<template #title="{ title }">
|
<span class="folder-tree-title" :title="title">
|
<FolderOutlined />
|
<span>{{ title }}</span>
|
</span>
|
</template>
|
</ATree>
|
</div>
|
</aside>
|
|
<div class="explorer-main">
|
<div class="navigation-bar">
|
<div class="address-bar" aria-label="当前位置">
|
<template v-for="(folder, index) in currentPath" :key="folder.id">
|
<button type="button" :title="displayName(folder)" @click="openBreadcrumb(index)">{{ displayName(folder) }}</button>
|
<RightOutlined v-if="index < currentPath.length - 1" />
|
</template>
|
</div>
|
|
<a-input v-model:value="keyword" class="entry-search" allow-clear :placeholder="`搜索 ${currentFolder?.name || '当前目录'}`">
|
<template #prefix><SearchOutlined /></template>
|
</a-input>
|
|
<div class="navigation-actions"><slot name="actions"></slot></div>
|
</div>
|
|
<div class="file-pane">
|
<div class="view-command-bar">
|
<strong>{{ currentFolder?.name || '文件' }}</strong>
|
<div class="view-command-actions">
|
<span>{{ visibleEntries.length }} 个项目</span>
|
<a-button-group size="small">
|
<a-tooltip title="详细信息">
|
<a-button aria-label="详细信息视图" :type="viewMode === 'details' ? 'primary' : 'default'" @click="viewMode = 'details'">
|
<UnorderedListOutlined />
|
</a-button>
|
</a-tooltip>
|
<a-tooltip title="大图标">
|
<a-button aria-label="大图标视图" :type="viewMode === 'tiles' ? 'primary' : 'default'" @click="viewMode = 'tiles'">
|
<AppstoreOutlined />
|
</a-button>
|
</a-tooltip>
|
</a-button-group>
|
</div>
|
</div>
|
|
<div class="entry-viewport" :class="`view-${viewMode}`" v-loading="loading">
|
<template v-if="visibleEntries.length">
|
<div v-if="viewMode === 'details'" class="detail-list" role="listbox" aria-label="目录内容">
|
<div class="detail-header" aria-hidden="true">
|
<span>标题</span>
|
<span>文件编号</span>
|
<span>版本号</span>
|
<span>上传日期</span>
|
<span>有效期至</span>
|
<span>文件大小</span>
|
<span>生效日期</span>
|
<span>操作记录</span>
|
</div>
|
<div
|
v-for="entry in visibleEntries"
|
:key="entry.id"
|
class="detail-row"
|
:class="{ selected: selectedId === entry.id }"
|
role="option"
|
:aria-selected="selectedId === entry.id"
|
tabindex="0"
|
@click="handleEntryClick(entry)"
|
@dblclick.stop.prevent="handleEntryDoubleClick(entry)"
|
@keydown.enter="openEntry(entry)">
|
<span class="entry-name" :title="displayName(entry)">
|
<span class="entry-icon" :class="resourceIconTone(entry)"><component :is="resourceIcon(entry)" /></span>
|
<span>{{ displayName(entry) }}</span>
|
</span>
|
<span :title="entry.documentNo">{{ fileValue(entry, entry.documentNo || '—') }}</span>
|
<span>{{ fileValue(entry, entry.versionLabel || `v${entry.versionNo ?? '-'}`) }}</span>
|
<span>{{ fileValue(entry, displayDate(entry.createdAt)) }}</span>
|
<span>{{ validUntil(entry) }}</span>
|
<span>{{ fileValue(entry, displaySize(entry)) }}</span>
|
<span>{{ fileValue(entry, displayDate(entry.effectiveFrom)) }}</span>
|
<span>—</span>
|
</div>
|
</div>
|
|
<div v-else class="tile-grid" role="listbox" aria-label="目录内容">
|
<button
|
v-for="entry in visibleEntries"
|
:key="entry.id"
|
class="entry-tile"
|
:class="{ selected: selectedId === entry.id }"
|
type="button"
|
:aria-selected="selectedId === entry.id"
|
:title="displayName(entry)"
|
@click="handleEntryClick(entry)"
|
@dblclick.stop.prevent="handleEntryDoubleClick(entry)"
|
@keydown.enter="openEntry(entry)">
|
<span class="tile-icon" :class="resourceIconTone(entry)"><component :is="resourceIcon(entry)" /></span>
|
<span class="tile-name">{{ displayName(entry) }}</span>
|
<span class="tile-meta">{{ resourceTypeLabel(entry) }}</span>
|
</button>
|
</div>
|
</template>
|
<Empty v-else-if="!loading" class="entry-empty" :description="emptyDescription" />
|
</div>
|
|
<footer class="explorer-status-bar">
|
<span>{{ entries.length }} 个项目</span>
|
<span v-if="selectedEntry" :title="displayName(selectedEntry)">已选择:{{ displayName(selectedEntry) }}</span>
|
</footer>
|
</div>
|
</div>
|
</section>
|
</template>
|
|
<style lang="scss" scoped>
|
.resource-explorer {
|
display: contents;
|
color: var(--text-color);
|
}
|
|
.explorer-main {
|
display: flex;
|
flex-direction: column;
|
grid-row: 1;
|
grid-column: 2;
|
min-width: 0;
|
min-height: 0;
|
overflow: hidden;
|
background: var(--component-background);
|
}
|
|
.folder-navigation {
|
display: flex;
|
flex-direction: column;
|
grid-row: 1;
|
grid-column: 1;
|
min-width: 0;
|
min-height: 0;
|
overflow: hidden;
|
background: color-mix(in srgb, var(--body-background) 62%, var(--component-background));
|
border-right: 1px solid var(--border-color-base1);
|
}
|
|
.folder-navigation-header {
|
display: flex;
|
flex: 0 0 42px;
|
align-items: center;
|
justify-content: space-between;
|
min-width: 0;
|
padding: 0 6px 0 12px;
|
border-bottom: 1px solid var(--border-color-base1);
|
|
strong {
|
overflow: hidden;
|
text-overflow: ellipsis;
|
font-size: 13px;
|
white-space: nowrap;
|
}
|
|
.collapsed & {
|
justify-content: center;
|
padding: 0;
|
}
|
}
|
|
.folder-navigation-toggle {
|
width: 30px;
|
min-width: 30px;
|
height: 30px;
|
padding: 0;
|
}
|
|
.folder-tree-scroll {
|
flex: 1;
|
min-height: 0;
|
padding: 6px;
|
overflow: auto;
|
}
|
|
.folder-tree {
|
min-width: 100%;
|
background: transparent;
|
}
|
|
.folder-tree-title {
|
display: flex;
|
gap: 7px;
|
align-items: center;
|
min-width: 0;
|
|
svg {
|
flex: 0 0 auto;
|
color: #d99c00;
|
}
|
|
span {
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
}
|
|
.folder-tree :deep(.ant-tree-treenode) {
|
width: 100%;
|
padding-block: 1px;
|
}
|
|
.folder-tree :deep(.ant-tree-node-content-wrapper) {
|
min-width: 0;
|
overflow: hidden;
|
}
|
|
.file-pane {
|
display: flex;
|
flex: 1;
|
flex-direction: column;
|
min-width: 0;
|
min-height: 0;
|
}
|
|
.navigation-bar {
|
display: grid;
|
flex: 0 0 auto;
|
grid-template-columns: minmax(180px, 1fr) minmax(180px, 240px) auto;
|
gap: 10px;
|
align-items: center;
|
min-height: 52px;
|
padding: 8px 12px;
|
border-bottom: 1px solid var(--border-color-base1);
|
}
|
|
.address-bar {
|
display: flex;
|
align-items: center;
|
min-width: 0;
|
height: 34px;
|
padding: 0 8px;
|
overflow: hidden;
|
background: var(--body-background);
|
border: 1px solid var(--border-color-base1);
|
border-radius: 4px;
|
|
button {
|
min-width: 0;
|
max-width: 180px;
|
padding: 3px 7px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
color: inherit;
|
white-space: nowrap;
|
cursor: pointer;
|
background: transparent;
|
border: 0;
|
border-radius: 3px;
|
|
&:hover {
|
background: var(--item-hover-bg);
|
}
|
}
|
|
svg {
|
flex: 0 0 auto;
|
font-size: 10px;
|
color: var(--text-color-secondary);
|
}
|
}
|
|
.entry-search {
|
width: 100%;
|
}
|
|
.navigation-actions {
|
display: flex;
|
justify-content: flex-end;
|
min-width: 0;
|
}
|
|
.view-command-bar {
|
display: flex;
|
flex: 0 0 auto;
|
align-items: center;
|
justify-content: space-between;
|
min-height: 42px;
|
padding: 6px 14px;
|
border-bottom: 1px solid var(--border-color-base1);
|
|
strong {
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
font-size: 14px;
|
white-space: nowrap;
|
}
|
}
|
|
.view-command-actions {
|
display: flex;
|
flex: 0 0 auto;
|
gap: 4px;
|
align-items: center;
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
}
|
|
.entry-viewport {
|
flex: 1;
|
min-height: 0;
|
padding: 8px 12px;
|
overflow: auto;
|
}
|
|
.detail-list {
|
min-width: 1240px;
|
}
|
|
.detail-header,
|
.detail-row {
|
display: grid;
|
grid-template-columns: minmax(280px, 1fr) minmax(130px, 0.55fr) 90px 112px 112px 100px 112px 100px;
|
gap: 8px;
|
align-items: center;
|
min-height: 38px;
|
padding: 0 10px;
|
}
|
|
.detail-header {
|
position: sticky;
|
top: -8px;
|
z-index: 2;
|
min-height: 34px;
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
background: var(--component-background);
|
border-bottom: 1px solid var(--border-color-base1);
|
}
|
|
.detail-row {
|
cursor: default;
|
border-bottom: 1px solid color-mix(in srgb, var(--border-color-base1) 65%, transparent);
|
border-radius: 3px;
|
|
> span:not(.entry-name) {
|
overflow: hidden;
|
text-overflow: ellipsis;
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
white-space: nowrap;
|
}
|
|
&:hover {
|
background: var(--item-hover-bg);
|
}
|
|
&.selected {
|
outline: 1px solid color-mix(in srgb, var(--primary-color) 38%, transparent);
|
background: color-mix(in srgb, var(--primary-color) 13%, transparent);
|
}
|
|
&:focus-visible {
|
outline: 2px solid var(--primary-color);
|
}
|
}
|
|
.entry-name {
|
display: flex;
|
gap: 10px;
|
align-items: center;
|
min-width: 0;
|
|
> span:last-child {
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
}
|
|
.entry-icon {
|
display: inline-flex;
|
flex: 0 0 24px;
|
align-items: center;
|
justify-content: center;
|
font-size: 20px;
|
}
|
|
.folder {
|
color: #e6a700;
|
}
|
|
.pdf {
|
color: #d14343;
|
}
|
|
.word {
|
color: #2468c9;
|
}
|
|
.excel {
|
color: #20845b;
|
}
|
|
.image {
|
color: #8b5bb7;
|
}
|
|
.file {
|
color: #6b7280;
|
}
|
|
.tile-grid {
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
gap: 8px;
|
align-content: start;
|
}
|
|
.entry-tile {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
min-width: 0;
|
height: 132px;
|
padding: 10px;
|
color: inherit;
|
cursor: default;
|
background: transparent;
|
border: 1px solid transparent;
|
border-radius: 6px;
|
|
&:hover {
|
background: var(--item-hover-bg);
|
border-color: var(--border-color-base1);
|
}
|
|
&.selected {
|
background: color-mix(in srgb, var(--primary-color) 13%, transparent);
|
border-color: color-mix(in srgb, var(--primary-color) 45%, transparent);
|
}
|
|
&:focus-visible {
|
outline: 2px solid var(--primary-color);
|
}
|
}
|
|
.tile-icon {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 58px;
|
height: 58px;
|
font-size: 46px;
|
}
|
|
.tile-name {
|
width: 100%;
|
margin-top: 6px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
font-size: 13px;
|
white-space: nowrap;
|
}
|
|
.tile-meta {
|
margin-top: 2px;
|
font-size: 11px;
|
color: var(--text-color-secondary);
|
}
|
|
.entry-empty {
|
margin-top: min(20vh, 140px);
|
}
|
|
.explorer-status-bar {
|
display: flex;
|
flex: 0 0 30px;
|
align-items: center;
|
justify-content: space-between;
|
min-width: 0;
|
padding: 0 12px;
|
font-size: 12px;
|
color: var(--text-color-secondary);
|
border-top: 1px solid var(--border-color-base1);
|
|
span {
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
}
|
|
@media (max-width: 1400px) {
|
.navigation-bar {
|
grid-template-columns: minmax(120px, 1fr) 220px;
|
}
|
|
.entry-search {
|
grid-row: 2;
|
grid-column: 1 / 3;
|
}
|
|
.navigation-actions {
|
width: 220px;
|
}
|
}
|
|
@media (max-width: 760px) {
|
.navigation-bar {
|
grid-template-columns: minmax(0, 1fr);
|
}
|
|
.navigation-actions {
|
grid-row: 1;
|
grid-column: 1;
|
width: auto;
|
}
|
|
.address-bar {
|
grid-row: 2;
|
grid-column: 1;
|
}
|
|
.entry-search {
|
grid-row: 3;
|
grid-column: 1;
|
}
|
}
|
</style>
|