feat: 实现可扩展的字段编辑器系统与专业资产选择器 (#227)

This commit is contained in:
YHH
2025-11-19 14:54:03 +08:00
committed by GitHub
parent caed5428d5
commit ecfef727c8
18 changed files with 1330 additions and 11 deletions

View File

@@ -0,0 +1,194 @@
import React, { useState, useRef, useCallback } from 'react';
import { FileText, Search, X, FolderOpen, ArrowRight, Package } from 'lucide-react';
import { open } from '@tauri-apps/plugin-dialog';
import './AssetField.css';
interface AssetFieldProps {
label: string;
value: string | null;
onChange: (value: string | null) => void;
fileExtension?: string; // 例如: '.btree'
placeholder?: string;
readonly?: boolean;
onNavigate?: (path: string) => void; // 导航到资产
}
export function AssetField({
label,
value,
onChange,
fileExtension = '',
placeholder = 'None',
readonly = false,
onNavigate
}: AssetFieldProps) {
const [isDragging, setIsDragging] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const inputRef = useRef<HTMLDivElement>(null);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!readonly) {
setIsDragging(true);
}
}, [readonly]);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
}, []);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
if (readonly) return;
// 处理从文件系统拖入的文件
const files = Array.from(e.dataTransfer.files);
const file = files.find(f =>
!fileExtension || f.name.endsWith(fileExtension)
);
if (file) {
// Web File API 没有 path 属性,使用 name
onChange(file.name);
return;
}
// 处理从资产面板拖入的文件路径
const assetPath = e.dataTransfer.getData('asset-path');
if (assetPath && (!fileExtension || assetPath.endsWith(fileExtension))) {
onChange(assetPath);
return;
}
// 兼容纯文本拖拽
const text = e.dataTransfer.getData('text/plain');
if (text && (!fileExtension || text.endsWith(fileExtension))) {
onChange(text);
}
}, [onChange, fileExtension, readonly]);
const handleBrowse = useCallback(async () => {
if (readonly) return;
try {
const selected = await open({
multiple: false,
filters: fileExtension ? [{
name: `${fileExtension} Files`,
extensions: [fileExtension.replace('.', '')]
}] : []
});
if (selected) {
onChange(selected as string);
}
} catch (error) {
console.error('Failed to open file dialog:', error);
}
}, [onChange, fileExtension, readonly]);
const handleClear = useCallback(() => {
if (!readonly) {
onChange(null);
}
}, [onChange, readonly]);
const getFileName = (path: string) => {
const parts = path.split(/[\\/]/);
return parts[parts.length - 1];
};
return (
<div className="asset-field">
<label className="asset-field__label">{label}</label>
<div
className={`asset-field__container ${isDragging ? 'dragging' : ''} ${isHovered ? 'hovered' : ''}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* 资产图标 */}
<div className="asset-field__icon">
{value ? (
fileExtension === '.btree' ?
<FileText size={14} /> :
<Package size={14} />
) : (
<Package size={14} style={{ opacity: 0.5 }} />
)}
</div>
{/* 资产选择框 */}
<div
ref={inputRef}
className={`asset-field__input ${value ? 'has-value' : 'empty'}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={!readonly ? handleBrowse : undefined}
title={value || placeholder}
>
<span className="asset-field__value">
{value ? getFileName(value) : placeholder}
</span>
</div>
{/* 操作按钮组 */}
<div className="asset-field__actions">
{/* 浏览按钮 */}
{!readonly && (
<button
className="asset-field__button"
onClick={(e) => {
e.stopPropagation();
handleBrowse();
}}
title="浏览..."
>
<Search size={12} />
</button>
)}
{/* 导航按钮 */}
{value && onNavigate && (
<button
className="asset-field__button"
onClick={(e) => {
e.stopPropagation();
onNavigate(value);
}}
title="在资产浏览器中显示"
>
<ArrowRight size={12} />
</button>
)}
{/* 清除按钮 */}
{value && !readonly && (
<button
className="asset-field__button asset-field__button--clear"
onClick={(e) => {
e.stopPropagation();
handleClear();
}}
title="清除"
>
<X size={12} />
</button>
)}
</div>
</div>
</div>
);
}