feat: Add SkillModule and WeaponModule for managing skills and weapons

- Implemented SkillModule to manage FormSkillSO assets with a detailed UI for editing and displaying skill properties.
- Implemented WeaponModule to manage WeaponSO assets with a detailed UI for editing and displaying weapon properties.
- Created AssetOperations class for centralized CRUD operations on ScriptableObject assets, including create, rename, delete, and clone functionalities.
- Added DetailHeader for displaying and renaming asset names in the UI.
- Introduced SoListPane for a reusable ScriptableObject list panel with search functionality and context menus.
- Added meta files for all new scripts to ensure proper asset management in Unity.
This commit is contained in:
2026-05-21 07:09:53 +08:00
parent f096105caf
commit bb3afd130f
41 changed files with 2417 additions and 1618 deletions

View File

@@ -0,0 +1,158 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace BaseGames.Editor
{
/// <summary>
/// 集中管理 ScriptableObject 资产的 CRUD 操作(含 Undo 支持)。
/// </summary>
public static class AssetOperations
{
// ── 创建 ──────────────────────────────────────────────────────────────
/// <summary>
/// 弹出 SaveFilePanel 让用户选择路径,创建并返回新资产。
/// 创建失败或用户取消时返回 null。
/// </summary>
public static T Create<T>(string defaultFolder, string defaultName) where T : ScriptableObject
{
if (!Directory.Exists(defaultFolder))
Directory.CreateDirectory(defaultFolder);
string path = EditorUtility.SaveFilePanelInProject(
"新建 " + typeof(T).Name,
defaultName + ".asset",
"asset",
"选择保存路径",
defaultFolder);
if (string.IsNullOrEmpty(path))
return null;
var asset = ScriptableObject.CreateInstance<T>();
asset.name = Path.GetFileNameWithoutExtension(path);
AssetDatabase.CreateAsset(asset, path);
Undo.RegisterCreatedObjectUndo(asset, "Create " + typeof(T).Name);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return asset;
}
// ── 重命名 ───────────────────────────────────────────────────────────
/// <summary>
/// 重命名资产(同时更新磁盘文件名和 asset.name
/// 返回 (true, null) 成功;(false, errorMsg) 失败。
/// </summary>
public static (bool ok, string error) Rename(UnityEngine.Object asset, string newName)
{
if (asset == null) return (false, "资产为 null");
if (string.IsNullOrWhiteSpace(newName)) return (false, "名称不能为空");
newName = newName.Trim();
string path = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(path)) return (false, "资产不在 AssetDatabase 中");
// 先更新序列化内部名称
string oldName = asset.name;
Undo.RecordObject(asset, "Rename " + oldName);
asset.name = newName;
EditorUtility.SetDirty(asset);
// 再重命名磁盘文件
string err = AssetDatabase.RenameAsset(path, newName);
if (!string.IsNullOrEmpty(err))
{
asset.name = oldName;
EditorUtility.SetDirty(asset);
return (false, err);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return (true, null);
}
// ── 删除 ─────────────────────────────────────────────────────────────
/// <summary>弹出确认对话框,确认后删除资产文件。返回是否已删除。</summary>
public static bool Delete(UnityEngine.Object asset)
{
if (asset == null) return false;
string path = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(path)) return false;
if (!EditorUtility.DisplayDialog(
"确认删除",
$"删除资产:{asset.name}\n路径{path}\n\n此操作不可撤销。",
"删除", "取消"))
return false;
AssetDatabase.DeleteAsset(path);
AssetDatabase.Refresh();
return true;
}
// ── 克隆 ─────────────────────────────────────────────────────────────
/// <summary>复制资产文件并返回克隆的资产;用户取消或失败时返回 null。</summary>
public static T Clone<T>(T source, string defaultFolder) where T : ScriptableObject
{
if (source == null) return null;
string srcPath = AssetDatabase.GetAssetPath(source);
string path = EditorUtility.SaveFilePanelInProject(
"克隆 " + source.name,
source.name + "_Copy.asset",
"asset",
"选择保存路径",
defaultFolder);
if (string.IsNullOrEmpty(path)) return null;
if (!AssetDatabase.CopyAsset(srcPath, path))
{
Debug.LogError($"[AssetOperations] 克隆失败:{srcPath} → {path}");
return null;
}
AssetDatabase.Refresh();
var clone = AssetDatabase.LoadAssetAtPath<T>(path);
if (clone != null)
Undo.RegisterCreatedObjectUndo(clone, "Clone " + source.name);
return clone;
}
// ── 查询 ─────────────────────────────────────────────────────────────
/// <summary>在 AssetDatabase 中查找所有 T 类型资产。</summary>
public static List<T> FindAll<T>() where T : ScriptableObject
{
var result = new List<T>();
string[] guids = AssetDatabase.FindAssets("t:" + typeof(T).Name);
foreach (var guid in guids)
{
string p = AssetDatabase.GUIDToAssetPath(guid);
var asset = AssetDatabase.LoadAssetAtPath<T>(p);
if (asset != null) result.Add(asset);
}
return result;
}
// ── GUID 工具 ─────────────────────────────────────────────────────────
public static string GetGuid(UnityEngine.Object asset)
{
if (asset == null) return string.Empty;
return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(asset));
}
public static T LoadByGuid<T>(string guid) where T : UnityEngine.Object
{
if (string.IsNullOrEmpty(guid)) return null;
return AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(guid));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2212f3dc47d61dd42b245a2470d2a90a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor
{
/// <summary>
/// 详情区顶部标题行:显示资产名称,双击或按 ✏ 进入行内重命名。
/// </summary>
public class DetailHeader : VisualElement
{
// ── 事件 ─────────────────────────────────────────────────────────────
/// <summary>用户确认重命名时触发,参数为新名称字符串。</summary>
public event Action<string> RenameRequested;
// ── 私有字段 ──────────────────────────────────────────────────────────
private Label _nameLabel;
private TextField _renameField;
private bool _renaming;
// ── 构造 ─────────────────────────────────────────────────────────────
public DetailHeader()
{
style.flexDirection = FlexDirection.Row;
style.alignItems = Align.Center;
style.paddingLeft = 12;
style.paddingRight = 8;
style.paddingTop = 10;
style.paddingBottom = 10;
style.borderBottomWidth = 1;
style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.2f));
// 名称 Label
_nameLabel = new Label("(未选中)");
_nameLabel.style.flexGrow = 1;
_nameLabel.style.fontSize = 15;
_nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_nameLabel.RegisterCallback<MouseDownEvent>(e =>
{
if (e.clickCount == 2) BeginRename();
});
Add(_nameLabel);
// 重命名输入框(默认隐藏)
_renameField = new TextField();
_renameField.style.flexGrow = 1;
_renameField.style.fontSize = 15;
_renameField.style.display = DisplayStyle.None;
_renameField.RegisterCallback<KeyDownEvent>(e =>
{
if (e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter)
{
e.StopPropagation();
CommitRename();
}
else if (e.keyCode == KeyCode.Escape)
{
e.StopPropagation();
CancelRename();
}
});
_renameField.RegisterCallback<FocusOutEvent>(_ => CommitRename());
Add(_renameField);
// 编辑按钮
var btnEdit = new Button(BeginRename) { text = "✏", tooltip = "重命名(双击名称也可触发)" };
btnEdit.style.width = 24;
btnEdit.style.height = 24;
btnEdit.style.marginLeft = 6;
btnEdit.style.paddingLeft = 0;
btnEdit.style.paddingRight = 0;
btnEdit.style.fontSize = 13;
Add(btnEdit);
}
// ── 公共 API ──────────────────────────────────────────────────────────
/// <summary>绑定新资产,更新标题显示。</summary>
public void SetAsset(UnityEngine.Object asset)
{
CancelRename();
_nameLabel.text = asset != null ? asset.name : "(未选中)";
}
// ── 内部逻辑 ──────────────────────────────────────────────────────────
private void BeginRename()
{
if (_renaming) return;
_renaming = true;
_renameField.value = _nameLabel.text;
_nameLabel.style.display = DisplayStyle.None;
_renameField.style.display = DisplayStyle.Flex;
schedule.Execute(() =>
{
_renameField.Focus();
_renameField.SelectAll();
});
}
private void CommitRename()
{
if (!_renaming) return;
_renaming = false;
_nameLabel.style.display = DisplayStyle.Flex;
_renameField.style.display = DisplayStyle.None;
var newName = _renameField.value.Trim();
if (!string.IsNullOrEmpty(newName) && newName != _nameLabel.text)
RenameRequested?.Invoke(newName);
}
private void CancelRename()
{
if (!_renaming) return;
_renaming = false;
_nameLabel.style.display = DisplayStyle.Flex;
_renameField.style.display = DisplayStyle.None;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4a51b9cef4da264fb261dac2e74700e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor
{
@@ -11,6 +12,139 @@ namespace BaseGames.Editor
/// </summary>
public static class EditorScaffoldUtils
{
// ── 资产命名前缀 ──────────────────────────────────────────────────────
private static readonly Dictionary<string, (string Prefix, string Convention)> s_PrefixMap = new()
{
{ "WeaponSO", ("WPN_", "WPN_{ID}WPN_SkyBlade") },
{ "FormSkillSO", ("SKL_", "SKL_{Name}SKL_SoulBlade") },
{ "BossSkillSO", ("SKL_", "SKL_{Name}SKL_BossRage") },
{ "SkillSequenceSO", ("SKL_", "SKL_Seq_{Name}SKL_Seq_RageCombo") },
{ "EnemyStatsSO", ("ENM_", "ENM_E{ID}_StatsENM_E001_Stats") },
{ "LootTableSO", ("ENM_", "ENM_E{ID}_LootENM_E001_Loot") },
{ "FormConfigSO", ("PLY_", "PLY_{FormID}PLY_Player01") },
{ "DamageSourceSO", ("CMB_", "CMB_DamageSource_{Name}CMB_DamageSource_Sword") },
{ "AbilityConfigSO", ("ABL_", "ABL_{Name}ABL_DoubleJump") },
{ "CharmConfigSO", ("CHM_", "CHM_{Name}CHM_GhostMantis") },
{ "ShopInventorySO", ("SHP_", "SHP_Inventory_{Name}SHP_Inventory_Forest") },
{ "MapRoomDataSO", ("MAP_", "MAP_RoomData_{Name}MAP_RoomData_Forest_01") },
{ "AudioPlaylistSO", ("AUD_", "AUD_BGM_{Name}AUD_BGM_Forest") },
{ "AudioConfigSO", ("AUD_", "AUD_SFX_{Name}AUD_SFX_Sword") },
{ "GlobalSettingsSO", ("SET_", "SET_{Name}SET_GlobalSettings") },
};
/// <summary>
/// 根据 SO 类型返回命名前缀和规范说明。
/// 事件频道(类名以 EventChannelSO 结尾)统一返回 EVT_ 前缀。
/// </summary>
public static (string Prefix, string Convention) GetAssetPrefixInfo(Type t)
{
if (t == null) return ("", "");
// 事件频道
if (t.Name.EndsWith("EventChannelSO"))
return ("EVT_", "EVT_{Description}EVT_PlayerDied");
// 直接匹配
if (s_PrefixMap.TryGetValue(t.Name, out var info)) return info;
// 向上遍历基类
var baseType = t.BaseType;
while (baseType != null && baseType != typeof(ScriptableObject))
{
if (s_PrefixMap.TryGetValue(baseType.Name, out var baseInfo)) return baseInfo;
baseType = baseType.BaseType;
}
return ("", "");
}
// ── 重命名 UI 组件 ────────────────────────────────────────────────────
/// <summary>
/// 创建可复用的"重命名资产"操作条UIToolkit
/// 重命名成功后以资产 GUID字符串调用 onRenamed调用方可用
/// <see cref="FindIndexByGuid{T}"/> 在刷新后的列表中恢复选中。
/// </summary>
/// <param name="asset">要重命名的资产对象。</param>
/// <param name="prefix">自动前缀(如 WPN_留空则禁用前缀勾选框。</param>
/// <param name="convention">命名规范提示文字,留空则不显示。</param>
/// <param name="onRenamed">重命名成功后的回调,参数为资产 GUID重命名不会改变 GUID。</param>
public static VisualElement MakeRenameBar(
UnityEngine.Object asset,
string prefix,
string convention,
Action<string> onRenamed = null)
{
var bar = new VisualElement();
bar.AddToClassList("rename-bar");
// ── 标题行 ────────────────────────────────────────────────────
var headerRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
var titleLbl = new Label("重命名") { style = { unityFontStyleAndWeight = FontStyle.Bold, flexGrow = 1 } };
headerRow.Add(titleLbl);
if (!string.IsNullOrEmpty(convention))
{
var hint = new Label($"规范:{convention}");
hint.AddToClassList("rename-hint");
headerRow.Add(hint);
}
bar.Add(headerRow);
// ── 输入行 ────────────────────────────────────────────────────
var inputRow = new VisualElement();
inputRow.AddToClassList("rename-bar-row");
var nameField = new TextField { value = asset.name, style = { flexGrow = 1 } };
inputRow.Add(nameField);
bool hasPrefix = !string.IsNullOrEmpty(prefix);
var toggle = new Toggle("自动前缀") { value = hasPrefix };
toggle.style.marginLeft = 6;
toggle.style.marginRight = 6;
if (!hasPrefix) toggle.SetEnabled(false);
inputRow.Add(toggle);
var btn = new Button(() =>
{
string newName = nameField.value.Trim();
if (string.IsNullOrEmpty(newName)) return;
if (toggle.value && hasPrefix && !newName.StartsWith(prefix, StringComparison.Ordinal))
newName = prefix + newName;
string assetPath = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(assetPath))
{
EditorUtility.DisplayDialog("重命名失败", "资产路径为空,请先保存资产。", "确定");
return;
}
// GUID 在重命名后不变,提前捕获
string guid = AssetDatabase.AssetPathToGUID(assetPath);
// 1. 更新内部序列化名称
asset.name = newName;
EditorUtility.SetDirty(asset);
// 2. 重命名文件(只改文件名,不含扩展名)
string err = AssetDatabase.RenameAsset(assetPath, newName);
if (!string.IsNullOrEmpty(err))
{
// 回滚内存名称
asset.name = System.IO.Path.GetFileNameWithoutExtension(assetPath);
EditorUtility.DisplayDialog("重命名失败", err, "确定");
return;
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 同步输入框显示实际名称
nameField.value = asset.name;
onRenamed?.Invoke(guid);
}) { text = "重命名" };
inputRow.Add(btn);
bar.Add(inputRow);
return bar;
}
// ── SO 资产创建 ───────────────────────────────────────────────────────
/// <summary>
@@ -37,6 +171,68 @@ namespace BaseGames.Editor
return asset;
}
/// <summary>
/// 通过 SaveFilePanel 让用户选择名称和路径,交互式创建 SO 资产。
/// 取消时返回 null。
/// </summary>
public static T CreateSOAssetInteractive<T>(string defaultFolder, string defaultName) where T : ScriptableObject
{
string path = EditorUtility.SaveFilePanelInProject(
$"创建 {typeof(T).Name}",
defaultName, "asset",
$"选择 {typeof(T).Name} 的保存路径",
defaultFolder);
if (string.IsNullOrEmpty(path)) return null;
EnsureFolder(System.IO.Path.GetDirectoryName(path)?.Replace('\\', '/') ?? defaultFolder);
var asset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(asset, path);
AssetDatabase.SaveAssets();
PingAndSelect(asset);
return asset;
}
// ── 资产删除 ──────────────────────────────────────────────────────────
/// <summary>
/// 弹出确认对话框后删除 SO 资产。返回 true 表示已成功删除。
/// </summary>
public static bool DeleteSOAsset(ScriptableObject asset)
{
if (asset == null) return false;
string path = AssetDatabase.GetAssetPath(asset);
if (string.IsNullOrEmpty(path)) return false;
bool ok = EditorUtility.DisplayDialog(
"确认删除",
$"确定要删除「{asset.name}」?\n{path}\n\n此操作不可撤销",
"删除", "取消");
if (!ok) return false;
AssetDatabase.DeleteAsset(path);
AssetDatabase.SaveAssets();
return true;
}
// ── 按 GUID 查找 ──────────────────────────────────────────────────────
/// <summary>
/// 在列表中按资产 GUID 查找索引(重命名后 GUID 不变,用于在 Refresh 后恢复选中)。
/// 返回 -1 表示未找到。
/// </summary>
public static int FindIndexByGuid<T>(List<T> list, string guid) where T : UnityEngine.Object
{
if (string.IsNullOrEmpty(guid)) return -1;
for (int i = 0; i < list.Count; i++)
{
if (list[i] == null) continue;
string g = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(list[i]));
if (g == guid) return i;
}
return -1;
}
// ── 目录工具 ──────────────────────────────────────────────────────────
/// <summary>确保 Assets 相对路径目录存在(不存在则递归创建)。</summary>

View File

@@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace BaseGames.Editor
{
/// <summary>
/// 通用 ScriptableObject 列表面板VisualElement 子类)。
/// 提供:搜索栏、[新建] 按钮、带类型徽章的 ListView、右键上下文菜单、GUID 选中追踪。
/// </summary>
public class SoListPane<T> : VisualElement where T : ScriptableObject
{
// ── 事件(使用字段委托,允许外部直接赋值替换,避免累积)─────────────
public Action<T> SelectionChanged;
// ── 字段 ─────────────────────────────────────────────────────────────
private readonly string _defaultFolder;
private readonly string _defaultPrefix;
private readonly Func<T, string> _getTypeBadge;
private List<T> _all = new();
private List<T> _filtered = new();
private string _search = "";
private string _savedGuid = "";
private ListView _listView;
private Label _countLabel;
// ── 构造 ─────────────────────────────────────────────────────────────
/// <param name="defaultFolder">新建资产的默认保存目录。</param>
/// <param name="defaultPrefix">新建资产文件名前缀(如 "WPN_")。</param>
/// <param name="getTypeBadge">返回每个资产的类型徽章文本;返回 null/空则不显示徽章。</param>
public SoListPane(
string defaultFolder,
string defaultPrefix = "",
Func<T, string> getTypeBadge = null)
{
_defaultFolder = defaultFolder;
_defaultPrefix = defaultPrefix;
_getTypeBadge = getTypeBadge;
style.flexGrow = 1;
style.flexDirection = FlexDirection.Column;
BuildUI();
}
// ── UI ────────────────────────────────────────────────────────────────
private void BuildUI()
{
// Toolbar
var toolbar = new VisualElement();
toolbar.style.flexDirection = FlexDirection.Row;
toolbar.style.alignItems = Align.Center;
toolbar.style.paddingLeft = 6;
toolbar.style.paddingRight = 6;
toolbar.style.paddingTop = 5;
toolbar.style.paddingBottom = 5;
toolbar.style.borderBottomWidth = 1;
toolbar.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
Add(toolbar);
var searchField = new TextField();
searchField.style.flexGrow = 1;
searchField.style.marginRight = 4;
searchField.RegisterValueChangedCallback(e => { _search = e.newValue; ApplyFilter(); });
toolbar.Add(searchField);
var btnNew = new Button(OnCreateClicked) { text = "+ 新建" };
btnNew.style.height = 20;
btnNew.style.paddingLeft = 8;
btnNew.style.paddingRight = 8;
toolbar.Add(btnNew);
// ListView
_listView = new ListView(_filtered, 24, MakeItem, BindItem);
_listView.style.flexGrow = 1;
_listView.selectionType = SelectionType.Single;
_listView.showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly;
_listView.onSelectionChange += objects =>
{
var sel = objects.OfType<T>().FirstOrDefault();
if (sel != null) _savedGuid = AssetOperations.GetGuid(sel);
SelectionChanged?.Invoke(sel);
};
Add(_listView);
// Count footer
_countLabel = new Label();
_countLabel.style.fontSize = 10;
_countLabel.style.opacity = 0.55f;
_countLabel.style.paddingLeft = 6;
_countLabel.style.paddingBottom = 3;
_countLabel.style.paddingTop = 2;
Add(_countLabel);
}
// ── ListView 回调 ─────────────────────────────────────────────────────
private VisualElement MakeItem()
{
var root = new VisualElement();
root.style.flexDirection = FlexDirection.Row;
root.style.alignItems = Align.Center;
root.style.paddingLeft = 6;
root.style.paddingRight = 4;
root.style.height = 24;
// 类型徽章
var badge = new Label { name = "badge" };
badge.style.fontSize = 10;
badge.style.paddingLeft = 4;
badge.style.paddingRight = 4;
badge.style.paddingTop = 1;
badge.style.paddingBottom = 1;
badge.style.borderTopLeftRadius = 3;
badge.style.borderTopRightRadius = 3;
badge.style.borderBottomLeftRadius = 3;
badge.style.borderBottomRightRadius = 3;
badge.style.backgroundColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
badge.style.marginRight = 5;
badge.style.display = DisplayStyle.None;
root.Add(badge);
// 名称标签
var nameLabel = new Label { name = "name" };
nameLabel.style.flexGrow = 1;
root.Add(nameLabel);
// 右键菜单(通过 userData 获取当前绑定的资产)
root.AddManipulator(new ContextualMenuManipulator(evt =>
{
if (root.userData is not T item) return;
evt.menu.AppendAction("在 Project 中定位", _ =>
{
EditorGUIUtility.PingObject(item);
Selection.activeObject = item;
});
evt.menu.AppendAction("在 Inspector 中打开", _ =>
Selection.activeObject = item);
evt.menu.AppendSeparator();
evt.menu.AppendAction("克隆...", _ =>
{
var clone = AssetOperations.Clone(item, _defaultFolder);
if (clone != null) Refresh(clone);
});
evt.menu.AppendSeparator();
evt.menu.AppendAction("删除", _ =>
{
if (AssetOperations.Delete(item)) Refresh(null);
});
}));
return root;
}
private void BindItem(VisualElement el, int idx)
{
if (idx < 0 || idx >= _filtered.Count) return;
var item = _filtered[idx];
el.userData = item;
el.Q<Label>("name").text = item.name;
var badge = el.Q<Label>("badge");
if (badge != null)
{
if (_getTypeBadge != null)
{
var txt = _getTypeBadge(item);
if (!string.IsNullOrEmpty(txt))
{
badge.text = txt;
badge.style.display = DisplayStyle.Flex;
return;
}
}
badge.style.display = DisplayStyle.None;
}
}
// ── 内部操作 ──────────────────────────────────────────────────────────
private void OnCreateClicked()
{
string name = _defaultPrefix.Length > 0 ? _defaultPrefix + "New" : "New" + typeof(T).Name;
var asset = AssetOperations.Create<T>(_defaultFolder, name);
if (asset != null) Refresh(asset);
}
private void ApplyFilter()
{
_filtered.Clear();
foreach (var item in _all)
{
if (string.IsNullOrEmpty(_search) ||
item.name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) >= 0)
_filtered.Add(item);
}
_listView.RefreshItems();
_countLabel.text = _all.Count == _filtered.Count
? $"{_all.Count} 项"
: $"{_filtered.Count} / {_all.Count} 项";
TryRestoreSelection();
}
private void TryRestoreSelection()
{
if (string.IsNullOrEmpty(_savedGuid)) return;
for (int i = 0; i < _filtered.Count; i++)
{
if (AssetOperations.GetGuid(_filtered[i]) == _savedGuid)
{
_listView.SetSelection(i);
_listView.ScrollToItem(i);
return;
}
}
}
// ── 公共 API ──────────────────────────────────────────────────────────
/// <summary>重新加载所有资产,可选择在刷新后选中指定资产。</summary>
public void Refresh(T selectAfter = null)
{
if (selectAfter != null)
_savedGuid = AssetOperations.GetGuid(selectAfter);
_all = AssetOperations.FindAll<T>();
_all.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase));
ApplyFilter();
}
/// <summary>强制重建列表视觉(如重命名后需刷新显示)。</summary>
public void Invalidate()
{
_listView.RefreshItems();
}
/// <summary>当前选中的资产;无选中时为 null。</summary>
public T Selected =>
_listView.selectedIndex >= 0 && _listView.selectedIndex < _filtered.Count
? _filtered[_listView.selectedIndex]
: null;
/// <summary>清除列表选中,触发 SelectionChanged(null)。</summary>
public void ClearSelection()
{
_listView.ClearSelection();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 011ed46e4350a784f88ae3687ce76197
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: