2025-11-19 14:54:03 +08:00
|
|
|
import React, { useState, useRef, useCallback } from 'react';
|
|
|
|
|
import { FileText, Search, X, FolderOpen, ArrowRight, Package } from 'lucide-react';
|
2025-11-23 14:49:37 +08:00
|
|
|
import { AssetPickerDialog } from '../../../components/dialogs/AssetPickerDialog';
|
2025-11-19 14:54:03 +08:00
|
|
|
import './AssetField.css';
|
|
|
|
|
|
|
|
|
|
interface AssetFieldProps {
|
2025-11-23 14:49:37 +08:00
|
|
|
label?: string;
|
2025-11-19 14:54:03 +08:00
|
|
|
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);
|
2025-11-23 14:49:37 +08:00
|
|
|
const [showPicker, setShowPicker] = useState(false);
|
2025-11-19 14:54:03 +08:00
|
|
|
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);
|
2025-11-23 14:49:37 +08:00
|
|
|
const file = files.find((f) =>
|
2025-11-19 14:54:03 +08:00
|
|
|
!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]);
|
|
|
|
|
|
2025-11-23 14:49:37 +08:00
|
|
|
const handleBrowse = useCallback(() => {
|
2025-11-19 14:54:03 +08:00
|
|
|
if (readonly) return;
|
2025-11-23 14:49:37 +08:00
|
|
|
setShowPicker(true);
|
|
|
|
|
}, [readonly]);
|
2025-11-19 14:54:03 +08:00
|
|
|
|
2025-11-23 14:49:37 +08:00
|
|
|
const handlePickerSelect = useCallback((path: string) => {
|
|
|
|
|
onChange(path);
|
|
|
|
|
setShowPicker(false);
|
|
|
|
|
}, [onChange]);
|
2025-11-19 14:54:03 +08:00
|
|
|
|
|
|
|
|
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">
|
2025-11-23 14:49:37 +08:00
|
|
|
{label && <label className="asset-field__label">{label}</label>}
|
2025-11-19 14:54:03 +08:00
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-11-23 14:49:37 +08:00
|
|
|
{/* 导航/定位按钮 */}
|
|
|
|
|
{onNavigate && (
|
2025-11-19 14:54:03 +08:00
|
|
|
<button
|
|
|
|
|
className="asset-field__button"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
2025-11-23 14:49:37 +08:00
|
|
|
if (value) {
|
|
|
|
|
onNavigate(value);
|
|
|
|
|
} else {
|
|
|
|
|
handleBrowse();
|
|
|
|
|
}
|
2025-11-19 14:54:03 +08:00
|
|
|
}}
|
2025-11-23 14:49:37 +08:00
|
|
|
title={value ? '在资产浏览器中显示' : '选择资产'}
|
2025-11-19 14:54:03 +08:00
|
|
|
>
|
2025-11-23 14:49:37 +08:00
|
|
|
{value ? <ArrowRight size={12} /> : <FolderOpen size={12} />}
|
2025-11-19 14:54:03 +08:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 清除按钮 */}
|
|
|
|
|
{value && !readonly && (
|
|
|
|
|
<button
|
|
|
|
|
className="asset-field__button asset-field__button--clear"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
handleClear();
|
|
|
|
|
}}
|
|
|
|
|
title="清除"
|
|
|
|
|
>
|
|
|
|
|
<X size={12} />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-23 14:49:37 +08:00
|
|
|
|
|
|
|
|
<AssetPickerDialog
|
|
|
|
|
isOpen={showPicker}
|
|
|
|
|
onClose={() => setShowPicker(false)}
|
|
|
|
|
onSelect={handlePickerSelect}
|
|
|
|
|
title="Select Asset"
|
|
|
|
|
fileExtensions={fileExtension ? [fileExtension] : []}
|
|
|
|
|
/>
|
2025-11-19 14:54:03 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
2025-11-23 14:49:37 +08:00
|
|
|
}
|