update:文件后缀、icon添加

This commit is contained in:
xiaozhu
2026-08-31 17:27:50 +08:00
parent 20eadb953b
commit 4ff0f344af
3 changed files with 83 additions and 29 deletions
@@ -7,6 +7,7 @@ import {
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import { fileVisualFromName } from "./WorkspaceTree";
import {
formFieldClass,
formHintClass,
@@ -52,6 +53,8 @@ export function DataResourceUploadModal({
const finalPath = finalTarget
? `${finalTarget}/${file?.name ?? ""}`
: file?.name ?? "";
const fileVisual = fileVisualFromName(file?.name ?? resourceName);
const FileIcon = fileVisual.Icon;
return (
<AppFormDialog
@@ -65,8 +68,13 @@ export function DataResourceUploadModal({
<form className={modalFormClass} onSubmit={onSubmit}>
<label className={formFieldClass}>
<span></span>
<div className="font-normal text-[#283a4e]">
{file ? file.name : "未选择文件"}
<div className="flex items-center gap-2.5 font-normal text-[#283a4e]">
<span
className={`grid size-[25px] shrink-0 place-items-center rounded-[5px] ${fileVisual.tone}`}
>
<FileIcon size={17} />
</span>
<span className="min-w-0 truncate">{file ? file.name : "未选择文件"}</span>
</div>
</label>
<label className={formFieldClass}>
@@ -75,7 +83,7 @@ export function DataResourceUploadModal({
className={formInputClass}
autoFocus
maxLength={255}
placeholder="例如:训练数据"
placeholder="例如:训练数据.csv"
value={resourceName}
onChange={(event) => onNameChange(event.target.value)}
required
@@ -5,8 +5,13 @@ import {
ChevronRight,
Database,
FileCode,
FileJson,
FileSpreadsheet,
FileText,
FileType,
Folder,
Lock,
Table2,
type LucideIcon,
} from "lucide-react";
import type {
@@ -50,6 +55,11 @@ type WorkspaceTreeItemsProps = Omit<WorkspaceTreeProps, "title" | "readOnly"> &
onCopyResourcePath?: (jupyterPath: string) => void;
};
export type FileVisual = {
Icon: LucideIcon;
tone: string;
};
function formatTime(value: string) {
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
@@ -64,6 +74,36 @@ export function scriptIcon(item: Pick<ScriptItem, "script_type">): LucideIcon {
return item.script_type === "notebook" ? BookOpen : FileCode;
}
/** 按文件名后缀返回树节点图标与配色(脚本 + 数据资源共用)。 */
export function fileVisualFromName(
name: string,
scriptType?: ScriptItem["script_type"],
): FileVisual {
const lower = name.toLowerCase();
if (scriptType === "notebook" || lower.endsWith(".ipynb")) {
return { Icon: BookOpen, tone: "text-[#e15e50] bg-[#fff0ed]" };
}
if (scriptType === "python" || lower.endsWith(".py")) {
return { Icon: FileCode, tone: "text-[#2e73c6] bg-[#eaf3ff]" };
}
if (lower.endsWith(".csv") || lower.endsWith(".tsv")) {
return { Icon: Table2, tone: "text-[#2f8f7b] bg-[#eaf8f4]" };
}
if (lower.endsWith(".xlsx") || lower.endsWith(".xls")) {
return { Icon: FileSpreadsheet, tone: "text-[#3d8f5a] bg-[#eef8f1]" };
}
if (lower.endsWith(".json")) {
return { Icon: FileJson, tone: "text-[#b07a1a] bg-[#fff8e8]" };
}
if (lower.endsWith(".parquet")) {
return { Icon: Database, tone: "text-[#4a6fa5] bg-[#eef3fa]" };
}
if (lower.endsWith(".txt")) {
return { Icon: FileText, tone: "text-[#6b7c8f] bg-[#f2f5f8]" };
}
return { Icon: FileType, tone: "text-[#5a8f6a] bg-[#eef8f1]" };
}
function ownedScriptPath(item: ScriptItem) {
// 数据资源的 relative_path 已经是 jupyter 路径(无 ULID 前缀);脚本相对路径形如
// `{ulid}/{ws_id}/{user_id}/...`,需要剥前两层。
@@ -98,25 +138,33 @@ export function WorkspaceTreeGroup({
const open = expandedPaths.has(groupKey);
const dataResourceScripts = useMemo(() => {
if (!dataResources) return [];
return dataResources.map((r) => ({
script_id: `data:${r.resource_id}`,
workspace_id: r.workspace_id,
current_object_id: r.storage_object_id,
owner_user_id: r.owner_user_id,
owner_display_name: null,
script_name: r.resource_name,
script_type: "python" as const,
visibility: r.visibility,
status: r.status,
is_locked: false,
// relative_path 直接是 jupyter 路径(与 Python/Notebook 同层渲染,不再带虚拟前缀)
relative_path: r.jupyter_accessible_path,
jupyter_path: r.jupyter_accessible_path,
content_hash: r.file.content_hash ?? "",
size_bytes: r.file.size_bytes,
created_at: r.created_at,
updated_at: r.updated_at,
} as unknown as ScriptItem));
return dataResources.map((r) => {
const ext = r.file.file_extension;
const name = r.resource_name;
const displayName =
ext && !name.toLowerCase().endsWith(ext.toLowerCase())
? `${name}${ext}`
: name;
return {
script_id: `data:${r.resource_id}`,
workspace_id: r.workspace_id,
current_object_id: r.storage_object_id,
owner_user_id: r.owner_user_id,
owner_display_name: null,
script_name: displayName,
script_type: "python" as const,
visibility: r.visibility,
status: r.status,
is_locked: false,
// relative_path 直接是 jupyter 路径(与 Python/Notebook 同层渲染,不再带虚拟前缀)
relative_path: r.jupyter_accessible_path,
jupyter_path: r.jupyter_accessible_path,
content_hash: r.file.content_hash ?? "",
size_bytes: r.file.size_bytes,
created_at: r.created_at,
updated_at: r.updated_at,
} as unknown as ScriptItem;
});
}, [dataResources]);
const dataResourceDirectories = useMemo(() => {
@@ -250,12 +298,10 @@ function WorkspaceTreeItems({
{childScripts.map((item) => {
const isActive = selectedId === item.script_id;
const isData = item.script_id.startsWith("data:");
const ItemIcon = isData ? Database : scriptIcon(item);
const iconTone = isData
? "text-[#5a8f6a] bg-[#eef8f1]"
: item.script_type === "notebook"
? "text-[#e15e50] bg-[#fff0ed]"
: "text-[#2e73c6] bg-[#eaf3ff]";
const { Icon: ItemIcon, tone: iconTone } = fileVisualFromName(
item.script_name,
isData ? undefined : item.script_type,
);
return (
<button
className={`flex h-[47px] w-full cursor-pointer items-center gap-2 rounded-md border pr-[9px] text-left ${
@@ -251,7 +251,7 @@ export const useUiStore = create<UiState>((set) => ({
open: true,
file,
parentPath,
resourceName: file.name.replace(/\.[^/.]+$/, ""),
resourceName: file.name,
visibility: "workspace",
description: "",
uploading: false,