frontend: FE-030 目录树与文件列表精致化,图标体系+骨架屏+空状态+行级联动效+圆点徽标

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
fenghp
2026-06-11 17:27:50 +08:00
co-authored by Cursor
parent b6447443c5
commit be055673f1
5 changed files with 203 additions and 51 deletions
@@ -0,0 +1,111 @@
import {
Database,
File,
FileArchive,
FileCode,
FileImage,
FileJson,
FileSpreadsheet,
FileTerminal,
FileText,
FolderClosed,
FolderOpen,
FolderSymlink,
type LucideIcon,
} from "lucide-react"
import { cn } from "~/lib/utils"
// 扩展名 -> 图标与配色的映射规则,未命中走默认文件图标。
const FILE_ICON_RULES: { exts: string[]; icon: LucideIcon; className: string }[] = [
{
exts: ["ts", "tsx", "js", "jsx", "java", "cs", "py", "go", "rs", "c", "cpp", "h", "vue", "css", "scss", "html"],
icon: FileCode,
className: "text-blue-600 dark:text-blue-400",
},
{
exts: ["json", "xml", "yml", "yaml", "toml", "config", "properties", "gradle"],
icon: FileJson,
className: "text-amber-600 dark:text-amber-400",
},
{
exts: ["md", "txt", "doc", "docx", "pdf", "rtf"],
icon: FileText,
className: "text-zinc-500 dark:text-zinc-400",
},
{
exts: ["sql", "db"],
icon: Database,
className: "text-violet-600 dark:text-violet-400",
},
{
exts: ["png", "jpg", "jpeg", "gif", "svg", "webp", "ico", "icns"],
icon: FileImage,
className: "text-emerald-600 dark:text-emerald-400",
},
{
exts: ["zip", "rar", "7z", "tar", "gz", "jar"],
icon: FileArchive,
className: "text-orange-600 dark:text-orange-400",
},
{
exts: ["xls", "xlsx", "csv"],
icon: FileSpreadsheet,
className: "text-green-600 dark:text-green-400",
},
{
exts: ["sh", "bat", "cmd", "ps1", "zsh"],
icon: FileTerminal,
className: "text-zinc-600 dark:text-zinc-300",
},
]
/**
* 取文件名扩展名(小写,无点);无扩展名返回空串。
*/
function extOf(name: string): string {
const index = name.lastIndexOf(".")
return index > 0 ? name.slice(index + 1).toLowerCase() : ""
}
/**
* 文件 / 文件夹图标:文件按扩展名区分图标与配色,
* 文件夹区分普通(蓝色)与已映射(FolderSymlink 主色),展开态用 FolderOpen。
*/
export function FileIcon({
name,
folder,
mapped = false,
expanded = false,
className,
}: {
name: string
folder: boolean
mapped?: boolean
expanded?: boolean
className?: string
}) {
if (folder) {
const Icon = mapped ? FolderSymlink : expanded ? FolderOpen : FolderClosed
return (
<Icon
className={cn(
"size-3.5 shrink-0",
mapped ? "text-primary" : "text-sky-600 dark:text-sky-400",
className,
)}
/>
)
}
const rule = FILE_ICON_RULES.find((item) => item.exts.includes(extOf(name)))
const Icon = rule?.icon ?? File
return (
<Icon
className={cn(
"size-3.5 shrink-0",
rule?.className ?? "text-muted-foreground",
className,
)}
/>
)
}