236 lines
5.0 KiB
Plaintext
236 lines
5.0 KiB
Plaintext
<template>
|
|
<view
|
|
v-if="node.type === 'folder'"
|
|
class="folder-item"
|
|
:style="`margin-left: ${level * 16}rpx`"
|
|
>
|
|
<view
|
|
class="folder-header"
|
|
@tap="handleToggleFolder"
|
|
>
|
|
<!-- 文件夹展开/收起图标 -->
|
|
<text class="folder-caret iconfont icon-a-Chevronright"
|
|
:class="{ expanded: isExpanded }"></text>
|
|
<view class="folder-name">{{ node.name }}</view>
|
|
</view>
|
|
<view v-if="isExpanded && node.children && !!node.children.length" class="file-list">
|
|
<file-tree-node
|
|
v-for="child in node.children"
|
|
:key="child.id"
|
|
:node="child"
|
|
:level="level + 1"
|
|
:expanded-folders="expandedFolders"
|
|
:selected-file-id="selectedFileId"
|
|
@toggle-folder="handleChildToggleFolder"
|
|
@select-file="handleChildSelectFile"
|
|
/>
|
|
</view>
|
|
</view>
|
|
<view
|
|
v-else
|
|
class="file-item"
|
|
:class="{ 'active-file': isSelected, 'hidden-file': node.name.startsWith('.') }"
|
|
:style="`margin-left: ${level * 8}rpx`"
|
|
@tap.stop="handleSelectFile"
|
|
>
|
|
<view class="file-icon-wrapper">
|
|
<image
|
|
class="file-icon"
|
|
:src="fileIcon"
|
|
mode="aspectFit"
|
|
alt=""
|
|
@error="handleImageError"
|
|
/>
|
|
</view>
|
|
<view class="file-name">{{ node.name }}</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { computed } from 'vue'
|
|
import type { FileNode } from '@/types/interfaces/agent'
|
|
import FileTreeNode from './file-tree-node.uvue'
|
|
import { getFileIcon } from './fileIconHelper.uts'
|
|
import iconDefault from '@/static/filetree/icon_default.svg'
|
|
|
|
interface Props {
|
|
node: FileNode
|
|
level: number
|
|
expandedFolders: Set<string>
|
|
selectedFileId: string
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const emit = defineEmits<{
|
|
toggleFolder: [folderId: string]
|
|
selectFile: [fileNode: FileNode]
|
|
}>()
|
|
|
|
/**
|
|
* 是否展开
|
|
*/
|
|
const isExpanded = computed(() => {
|
|
return props.expandedFolders.has(props.node.id)
|
|
})
|
|
|
|
/**
|
|
* 是否选中
|
|
*/
|
|
const isSelected = computed(() => {
|
|
return props.selectedFileId === props.node.id
|
|
})
|
|
|
|
/**
|
|
* 文件图标路径
|
|
*/
|
|
const fileIcon = computed<string>(() => {
|
|
if (props.node?.type === 'file' || props.node?.isLink) {
|
|
return getFileIcon(props.node.name)
|
|
}
|
|
return iconDefault
|
|
})
|
|
|
|
/**
|
|
* 图片加载错误处理
|
|
*/
|
|
const handleImageError = (e: any) => {
|
|
console.error('Image load error:', fileIcon.value, e)
|
|
}
|
|
|
|
/**
|
|
* 切换文件夹展开/收起
|
|
*/
|
|
const handleToggleFolder = () => {
|
|
if (props.node.type === 'folder') {
|
|
emit('toggleFolder', props.node.id)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 选择文件
|
|
*/
|
|
const handleSelectFile = () => {
|
|
if (props.node.type === 'file' && !props.node.name.startsWith('.')) {
|
|
emit('selectFile', props.node)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 处理子组件的切换文件夹事件
|
|
*/
|
|
const handleChildToggleFolder = (folderId: string) => {
|
|
emit('toggleFolder', folderId)
|
|
}
|
|
|
|
/**
|
|
* 处理子组件的选择文件事件
|
|
*/
|
|
const handleChildSelectFile = (fileNode: FileNode) => {
|
|
emit('selectFile', fileNode)
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import '@/static/iconfont/iconfont.css';
|
|
|
|
.folder-item {
|
|
padding: 0rpx 0 0 32rpx;
|
|
white-space: nowrap;
|
|
display: block;
|
|
|
|
.folder-header {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-direction: row;
|
|
padding: 16rpx 0;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
min-height: 40rpx;
|
|
white-space: nowrap;
|
|
|
|
.folder-caret {
|
|
font-size: 18rpx;
|
|
color: #5e5e5b;
|
|
margin-right: 12rpx;
|
|
transition: transform 0.2s;
|
|
flex-shrink: 0;
|
|
// display: inline-block;
|
|
// width: 18rpx;
|
|
// text-align: center;
|
|
// line-height: 1;
|
|
|
|
&.expanded {
|
|
transform: rotate(90deg);
|
|
}
|
|
}
|
|
|
|
// .folder-icon {
|
|
// font-size: 28rpx;
|
|
// color: #8b9dc3;
|
|
// margin-right: 6rpx;
|
|
// display: inline-block;
|
|
// line-height: 1;
|
|
// }
|
|
|
|
.folder-name {
|
|
font-size: 26rpx;
|
|
color: #5e5e5b;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
|
|
.file-list {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
|
|
.file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-direction: row;
|
|
padding: 16rpx 32rpx;
|
|
color: #5e5e5b;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
min-height: 40rpx;
|
|
white-space: nowrap;
|
|
|
|
&.active-file {
|
|
background-color: #f0f0f0;
|
|
border-radius: 4rpx;
|
|
}
|
|
|
|
&.hidden-file {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.file-icon-wrapper {
|
|
width: 28rpx;
|
|
height: 28rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 12rpx;
|
|
flex-shrink: 0;
|
|
|
|
.file-icon {
|
|
width: 28rpx;
|
|
height: 28rpx;
|
|
display: block;
|
|
color: #5e5e5b;
|
|
}
|
|
}
|
|
|
|
.file-name {
|
|
font-size: 26rpx;
|
|
color: #5e5e5b;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
</style>
|