feat: Round 48 narrative systems improvements
- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop - QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip - IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info) - IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it - IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event - QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions - DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix) - DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks - DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match - WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API - DialogueModule: List badge shows warning indicator for unconditional-shadowing variants - DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,13 +4,15 @@ using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.Editor.Dialogue;
|
||||
using BaseGames.Editor.Shared;
|
||||
|
||||
namespace BaseGames.Editor.Modules
|
||||
{
|
||||
/// <summary>
|
||||
/// DataHub 对话序列模块 —— 管理 DialogueSequenceSO 资产。
|
||||
/// </summary>
|
||||
public class DialogueModule : IDataModule
|
||||
public class DialogueModule : IDataModule, IDataModuleOrdered
|
||||
{
|
||||
private const string Folder = "Assets/_Game/Data/Dialogue";
|
||||
private const string Prefix = "DLG_";
|
||||
@@ -18,6 +20,7 @@ namespace BaseGames.Editor.Modules
|
||||
public string ModuleId => "dialogue";
|
||||
public string DisplayName => "对话";
|
||||
public string IconName => "d_UnityEditor.ConsoleWindow";
|
||||
public int DisplayOrder => 100;
|
||||
|
||||
private SoListPane<DialogueSequenceSO> _listPane;
|
||||
private DetailHeader _header;
|
||||
@@ -29,9 +32,20 @@ namespace BaseGames.Editor.Modules
|
||||
Folder, Prefix,
|
||||
s =>
|
||||
{
|
||||
int v = s.variants != null ? s.variants.Length : 0;
|
||||
return v > 0 ? $"{v}变体" : null;
|
||||
if (s.variants == null || s.variants.Length == 0) return null;
|
||||
int v = s.variants.Length;
|
||||
// 检测无条件变体遮蔽(非末尾的无条件变体会让后续变体永不命中)
|
||||
bool hasShadow = false;
|
||||
for (int i = 0; i < s.variants.Length - 1; i++)
|
||||
{
|
||||
var vv = s.variants[i];
|
||||
if (vv.sequence != null && (vv.requiredFlags == null || vv.requiredFlags.Length == 0))
|
||||
{ hasShadow = true; break; }
|
||||
}
|
||||
return hasShadow ? $"{v}变体 ⚠" : $"{v}变体";
|
||||
});
|
||||
// 扩展搜索:sequenceId
|
||||
_listPane.GetExtraSearchText = d => d.sequenceId;
|
||||
}
|
||||
|
||||
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
|
||||
@@ -41,6 +55,52 @@ namespace BaseGames.Editor.Modules
|
||||
_selected = sel;
|
||||
onSelected?.Invoke(sel);
|
||||
};
|
||||
|
||||
// ── 快速过滤标签行 ────────────────────────────────────────────
|
||||
var filterRow = new VisualElement();
|
||||
filterRow.style.flexDirection = FlexDirection.Row;
|
||||
filterRow.style.flexWrap = Wrap.Wrap;
|
||||
filterRow.style.paddingLeft = 6;
|
||||
filterRow.style.paddingRight = 6;
|
||||
filterRow.style.paddingBottom = 3;
|
||||
container.Add(filterRow);
|
||||
|
||||
bool filterVariants = false, filterBranches = false, filterNoVoice = false;
|
||||
|
||||
void RebuildFilter()
|
||||
{
|
||||
if (!filterVariants && !filterBranches && !filterNoVoice)
|
||||
{
|
||||
_listPane.ExtraFilter = null;
|
||||
return;
|
||||
}
|
||||
_listPane.ExtraFilter = s =>
|
||||
{
|
||||
if (filterVariants && (s.variants == null || s.variants.Length == 0)) return false;
|
||||
if (filterBranches)
|
||||
{
|
||||
bool hasBranch = false;
|
||||
if (s.lines != null)
|
||||
foreach (var l in s.lines)
|
||||
if (l.choices != null && l.choices.Length > 0) { hasBranch = true; break; }
|
||||
if (!hasBranch) return false;
|
||||
}
|
||||
if (filterNoVoice)
|
||||
{
|
||||
bool hasVoice = false;
|
||||
if (s.lines != null)
|
||||
foreach (var l in s.lines)
|
||||
if (l.voiceClip != null) { hasVoice = true; break; }
|
||||
if (hasVoice) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
filterRow.Add(QuestModule.MakeFilterChip("有变体", v => { filterVariants = v; RebuildFilter(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("有分支", v => { filterBranches = v; RebuildFilter(); }));
|
||||
filterRow.Add(QuestModule.MakeFilterChip("无语音", v => { filterNoVoice = v; RebuildFilter(); }));
|
||||
|
||||
container.Add(_listPane);
|
||||
_listPane.Refresh();
|
||||
}
|
||||
@@ -90,8 +150,11 @@ namespace BaseGames.Editor.Modules
|
||||
return card;
|
||||
}
|
||||
|
||||
private static VisualElement BuildLinesPreview(DialogueSequenceSO s)
|
||||
private VisualElement BuildLinesPreview(DialogueSequenceSO s)
|
||||
{
|
||||
var so = new SerializedObject(s);
|
||||
var linesProp = so.FindProperty("lines");
|
||||
|
||||
var section = new VisualElement();
|
||||
section.style.paddingLeft = 12;
|
||||
section.style.paddingRight = 12;
|
||||
@@ -114,14 +177,14 @@ namespace BaseGames.Editor.Modules
|
||||
return section;
|
||||
}
|
||||
|
||||
int preview = Mathf.Min(5, s.lines.Length);
|
||||
for (int i = 0; i < preview; i++)
|
||||
int previewCount = Mathf.Min(5, s.lines.Length);
|
||||
for (int i = 0; i < previewCount; i++)
|
||||
{
|
||||
var line = s.lines[i];
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.marginBottom = 3;
|
||||
row.style.marginBottom = 2;
|
||||
|
||||
// 头像图标(actor 优先,回退到直接字段)
|
||||
var portrait = line.ResolvedPortrait;
|
||||
@@ -152,36 +215,126 @@ namespace BaseGames.Editor.Modules
|
||||
}
|
||||
}
|
||||
|
||||
// 说话人(actor 优先,回退到直接字段)
|
||||
// 说话人(actor 优先,回退到直接字段;尝试解析本地化实际文本)
|
||||
string speakerKey = line.ResolvedNameKey;
|
||||
if (!string.IsNullOrEmpty(speakerKey))
|
||||
{
|
||||
var spk = new Label(speakerKey + ":");
|
||||
var speakerResolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(speakerKey, "Dialogue");
|
||||
bool speakerMissing = speakerResolved == null;
|
||||
string speakerText = speakerMissing ? speakerKey : speakerResolved;
|
||||
var spk = new Label(speakerText + ":");
|
||||
spk.style.fontSize = 11;
|
||||
spk.style.opacity = 0.55f;
|
||||
spk.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
spk.style.marginRight = 4;
|
||||
spk.style.flexShrink = 0;
|
||||
var accent = line.ResolvedAccentColor;
|
||||
if (speakerMissing)
|
||||
{
|
||||
// 说话人 Key 缺少本地化 → 橙色警告
|
||||
spk.style.color = new StyleColor(new Color(1f, 0.6f, 0.1f));
|
||||
spk.style.opacity = 1.0f;
|
||||
}
|
||||
else if (accent != Color.white)
|
||||
{
|
||||
spk.style.color = new StyleColor(accent);
|
||||
spk.style.opacity = 1.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
spk.style.opacity = 0.55f;
|
||||
}
|
||||
row.Add(spk);
|
||||
}
|
||||
|
||||
// 文本 key(尝试显示本地化实际内容,回退到 key 本身)
|
||||
string rawText = string.IsNullOrEmpty(line.textKey) ? "(空)" : line.textKey;
|
||||
string preview = string.IsNullOrEmpty(line.textKey)
|
||||
? "(空)"
|
||||
: (BaseGames.Localization.LocalizationManager.GetEditorPreview(line.textKey, "Dialogue") ?? rawText);
|
||||
if (preview.Length > 48) preview = preview[..48] + "…";
|
||||
var lbl = new Label(preview);
|
||||
// 文本(本地化预览;Key 有值但无本地化内容时橙色 ⚠ 警告)
|
||||
string textPreview;
|
||||
bool textL10nMissing = false;
|
||||
if (string.IsNullOrEmpty(line.textKey))
|
||||
{
|
||||
textPreview = "(空)";
|
||||
}
|
||||
else
|
||||
{
|
||||
var resolved = BaseGames.Localization.LocalizationManager.GetEditorPreview(line.textKey, "Dialogue");
|
||||
if (resolved != null)
|
||||
{
|
||||
textPreview = resolved;
|
||||
}
|
||||
else
|
||||
{
|
||||
textPreview = line.textKey + " ⚠";
|
||||
textL10nMissing = true;
|
||||
}
|
||||
}
|
||||
if (textPreview.Length > 48) textPreview = textPreview[..48] + "…";
|
||||
var lbl = new Label(textPreview);
|
||||
lbl.style.fontSize = 11;
|
||||
lbl.style.overflow = Overflow.Hidden;
|
||||
lbl.style.flexGrow = 1;
|
||||
if (textL10nMissing)
|
||||
lbl.style.color = new StyleColor(new Color(1f, 0.6f, 0.1f));
|
||||
row.Add(lbl);
|
||||
|
||||
// 选项分支徽章(有 choices 时显示"→N选")
|
||||
if (line.choices != null && line.choices.Length > 0)
|
||||
{
|
||||
var choiceBadge = new Label($"→{line.choices.Length}选");
|
||||
choiceBadge.style.fontSize = 9;
|
||||
choiceBadge.style.color = new StyleColor(new Color(0.3f, 0.8f, 1f));
|
||||
choiceBadge.style.marginLeft = 4;
|
||||
choiceBadge.style.flexShrink = 0;
|
||||
row.Add(choiceBadge);
|
||||
}
|
||||
|
||||
// ▾ 内联编辑按钮
|
||||
var editBtn = new Button { text = "▾" };
|
||||
editBtn.style.fontSize = 9;
|
||||
editBtn.style.marginLeft = 4;
|
||||
editBtn.style.paddingLeft = 3;
|
||||
editBtn.style.paddingRight = 3;
|
||||
editBtn.style.height = 16;
|
||||
row.Add(editBtn);
|
||||
|
||||
// 内联编辑区域(默认隐藏,点击 ▾ 展开)
|
||||
int capturedIdx = i;
|
||||
var editRow = new VisualElement();
|
||||
editRow.style.display = DisplayStyle.None;
|
||||
editRow.style.paddingLeft = 26;
|
||||
editRow.style.paddingRight = 8;
|
||||
editRow.style.marginBottom = 4;
|
||||
|
||||
if (linesProp != null)
|
||||
{
|
||||
var lineProp = linesProp.GetArrayElementAtIndex(capturedIdx);
|
||||
var textKeyProp = lineProp?.FindPropertyRelative("textKey");
|
||||
if (textKeyProp != null)
|
||||
{
|
||||
var tf = new TextField("文本 Key") { value = textKeyProp.stringValue };
|
||||
tf.style.fontSize = 10;
|
||||
tf.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
so.Update();
|
||||
textKeyProp.stringValue = evt.newValue;
|
||||
so.ApplyModifiedProperties();
|
||||
});
|
||||
editRow.Add(tf);
|
||||
}
|
||||
}
|
||||
|
||||
editBtn.clicked += () =>
|
||||
{
|
||||
bool open = editRow.style.display == DisplayStyle.None;
|
||||
editRow.style.display = open ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
editBtn.text = open ? "▴" : "▾";
|
||||
};
|
||||
|
||||
section.Add(row);
|
||||
section.Add(editRow);
|
||||
}
|
||||
|
||||
if (s.lines.Length > preview)
|
||||
if (s.lines.Length > previewCount)
|
||||
{
|
||||
var more = new Label($"… 还有 {s.lines.Length - preview} 行");
|
||||
var more = new Label($"… 还有 {s.lines.Length - previewCount} 行");
|
||||
more.style.opacity = 0.4f;
|
||||
more.style.fontSize = 10;
|
||||
section.Add(more);
|
||||
@@ -202,29 +355,207 @@ namespace BaseGames.Editor.Modules
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
card.Add(title);
|
||||
|
||||
foreach (var v in s.variants)
|
||||
for (int i = 0; i < s.variants.Length; i++)
|
||||
{
|
||||
var v = s.variants[i];
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.marginBottom = 2;
|
||||
|
||||
string flags = v.requiredFlags != null && v.requiredFlags.Length > 0
|
||||
? string.Join(", ", v.requiredFlags)
|
||||
: "(无条件)";
|
||||
SkillModule.AddChip(row, "条件", flags);
|
||||
SkillModule.AddChip(row, "替换序列", v.sequence != null ? v.sequence.name : "(未设置)");
|
||||
bool isUnconditional = v.requiredFlags == null || v.requiredFlags.Length == 0;
|
||||
bool isShadowing = isUnconditional && v.sequence != null && i < s.variants.Length - 1;
|
||||
|
||||
// 条件徽章:显示逻辑模式(And/Or)和标志列表
|
||||
string conditionText;
|
||||
if (isUnconditional)
|
||||
{
|
||||
conditionText = isShadowing ? "⚠ (无条件)" : "(无条件)";
|
||||
}
|
||||
else
|
||||
{
|
||||
string logicPrefix = v.requiredFlags.Length > 1
|
||||
? $"[{(v.logic == BaseGames.Core.WorldStateFlagLogic.Or ? "OR" : "AND")}] "
|
||||
: "";
|
||||
conditionText = logicPrefix + string.Join(", ", v.requiredFlags);
|
||||
}
|
||||
|
||||
SkillModule.AddChip(row, "条件", conditionText);
|
||||
|
||||
// 替换序列徽章
|
||||
string seqName = v.sequence != null ? v.sequence.name : "(未设置)";
|
||||
int seqLines = v.sequence != null && v.sequence.lines != null ? v.sequence.lines.Length : 0;
|
||||
string seqLabel = v.sequence != null ? $"{seqName}({seqLines}行)" : seqName;
|
||||
SkillModule.AddChip(row, "替换序列", seqLabel);
|
||||
|
||||
card.Add(row);
|
||||
|
||||
// 遮蔽警告行
|
||||
if (isShadowing)
|
||||
{
|
||||
int remaining = s.variants.Length - 1 - i;
|
||||
var warn = new Label($" ↑ 此变体无条件,其后 {remaining} 个变体永不生效。请移至末尾或添加条件。");
|
||||
warn.style.fontSize = 9;
|
||||
warn.style.color = new StyleColor(new Color(1f, 0.6f, 0.1f));
|
||||
warn.style.marginBottom = 2;
|
||||
card.Add(warn);
|
||||
}
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
private VisualElement BuildActionBar(DialogueSequenceSO s)
|
||||
{
|
||||
return SkillModule.BuildStandardActionBar(
|
||||
var bar = SkillModule.BuildStandardActionBar(
|
||||
s, Folder, Prefix,
|
||||
onCreated: c => _listPane.Refresh(c),
|
||||
onCloned: c => _listPane.Refresh(c),
|
||||
onDeleted: () => _listPane.Refresh(null));
|
||||
onDeleted: () => _listPane.Refresh(null),
|
||||
wizardCreate: cb => AssetCreationWizard.Show<DialogueSequenceSO>(
|
||||
Folder, Prefix,
|
||||
(d, id) =>
|
||||
{
|
||||
d.sequenceId = id;
|
||||
EditorUtility.SetDirty(d);
|
||||
AssetDatabase.SaveAssets();
|
||||
cb(d);
|
||||
}));
|
||||
|
||||
new Button(ValidateAllSequences) { text = "批量验证" }.AlsoAddTo(bar);
|
||||
|
||||
if (s.variants != null && s.variants.Length > 0)
|
||||
{
|
||||
var capturedS = s;
|
||||
new Button(() => DialogueVariantPreviewWindow.OpenWith(capturedS))
|
||||
{ text = "预览变体" }.AlsoAddTo(bar);
|
||||
}
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
// ── 批量验证 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 遍历所有 DialogueSequenceSO,检查:
|
||||
/// 1. sequenceId 为空
|
||||
/// 2. sequenceId 重复
|
||||
/// 3. 每行 textKey 是否在本地化表中存在
|
||||
/// 4. 每行 speakerNameKey(无 actor 时)是否在本地化表中存在
|
||||
/// 5. 每个选项 textKey 是否在本地化表中存在
|
||||
/// 结果显示在 QuestValidationResultWindow 中,每项问题附"选中"按钮可一键定位资产。
|
||||
/// </summary>
|
||||
private static void ValidateAllSequences()
|
||||
{
|
||||
var allSeqs = AssetOperations.FindAll<DialogueSequenceSO>();
|
||||
var issues = new System.Collections.Generic.List<QuestValidationResultWindow.Issue>();
|
||||
int errorCount = 0, warnCount = 0;
|
||||
|
||||
// 预构建本地化缓存(整个验证过程只查询一次,避免大批量序列时重复读取本地化表)
|
||||
var locCache = new System.Collections.Generic.Dictionary<string, string>(System.StringComparer.Ordinal);
|
||||
string GetLoc(string key)
|
||||
{
|
||||
if (locCache.TryGetValue(key, out var v)) return v;
|
||||
v = BaseGames.Localization.LocalizationManager.GetEditorPreview(key, "Dialogue");
|
||||
locCache[key] = v;
|
||||
return v;
|
||||
}
|
||||
|
||||
void AddError(string msg, UnityEngine.Object asset = null)
|
||||
{
|
||||
issues.Add(new QuestValidationResultWindow.Issue { message = msg, isError = true, asset = asset });
|
||||
errorCount++;
|
||||
}
|
||||
void AddWarn(string msg, UnityEngine.Object asset = null)
|
||||
{
|
||||
issues.Add(new QuestValidationResultWindow.Issue { message = msg, isError = false, asset = asset });
|
||||
warnCount++;
|
||||
}
|
||||
|
||||
// 1 & 2:空 / 重复 sequenceId
|
||||
var idMap = new System.Collections.Generic.Dictionary<string, DialogueSequenceSO>(System.StringComparer.Ordinal);
|
||||
var keyFormatRegex = new System.Text.RegularExpressions.Regex(@"^[\w\-\.]+$");
|
||||
foreach (var seq in allSeqs)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(seq.sequenceId))
|
||||
{
|
||||
AddError($"{seq.name}: sequenceId 为空。", seq);
|
||||
continue;
|
||||
}
|
||||
if (idMap.TryGetValue(seq.sequenceId, out var existing))
|
||||
AddError($"重复 sequenceId \"{seq.sequenceId}\":{seq.name} 与 {existing.name}", seq);
|
||||
else
|
||||
idMap[seq.sequenceId] = seq;
|
||||
|
||||
// 2b. sequenceId 格式异常
|
||||
if (!keyFormatRegex.IsMatch(seq.sequenceId))
|
||||
AddWarn($"{seq.name}: sequenceId \"{seq.sequenceId}\" 含有空格或非法字符,建议只使用字母、数字、_、-、.。", seq);
|
||||
}
|
||||
|
||||
// 3-5:本地化 Key 存在性 + 格式检查
|
||||
foreach (var seq in allSeqs)
|
||||
{
|
||||
if (seq.lines == null) continue;
|
||||
for (int i = 0; i < seq.lines.Length; i++)
|
||||
{
|
||||
var line = seq.lines[i];
|
||||
string lineDesc = $"{seq.name} 行[{i}]";
|
||||
|
||||
// 文本 Key
|
||||
if (string.IsNullOrEmpty(line.textKey))
|
||||
{
|
||||
AddWarn($"{lineDesc}: textKey 为空,运行时显示空文本。", seq);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GetLoc(line.textKey) == null)
|
||||
AddWarn($"{lineDesc}: textKey \"{line.textKey}\" 在本地化表中不存在。", seq);
|
||||
if (!keyFormatRegex.IsMatch(line.textKey))
|
||||
AddWarn($"{lineDesc}: textKey \"{line.textKey}\" 含有空格或非法字符。", seq);
|
||||
}
|
||||
|
||||
// 说话人 Key(无 actor 时检查直接字段)
|
||||
if (line.actor == null && !string.IsNullOrEmpty(line.speakerNameKey))
|
||||
{
|
||||
if (GetLoc(line.speakerNameKey) == null)
|
||||
AddWarn($"{lineDesc}: speakerNameKey \"{line.speakerNameKey}\" 在本地化表中不存在。", seq);
|
||||
if (!keyFormatRegex.IsMatch(line.speakerNameKey))
|
||||
AddWarn($"{lineDesc}: speakerNameKey \"{line.speakerNameKey}\" 含有空格或非法字符。", seq);
|
||||
}
|
||||
|
||||
// 选项 Key
|
||||
if (line.choices != null)
|
||||
{
|
||||
for (int j = 0; j < line.choices.Length; j++)
|
||||
{
|
||||
var choice = line.choices[j];
|
||||
if (string.IsNullOrEmpty(choice.textKey))
|
||||
{
|
||||
AddWarn($"{lineDesc} 选项[{j}]: textKey 为空。", seq);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GetLoc(choice.textKey) == null)
|
||||
AddWarn($"{lineDesc} 选项[{j}]: textKey \"{choice.textKey}\" 在本地化表中不存在。", seq);
|
||||
if (!keyFormatRegex.IsMatch(choice.textKey))
|
||||
AddWarn($"{lineDesc} 选项[{j}]: textKey \"{choice.textKey}\" 含有空格或非法字符。", seq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. variants[i].sequence 为 null(变体存在但序列引用为空)
|
||||
foreach (var seq in allSeqs)
|
||||
{
|
||||
if (seq.variants == null) continue;
|
||||
for (int vi = 0; vi < seq.variants.Length; vi++)
|
||||
{
|
||||
if (seq.variants[vi].sequence == null)
|
||||
AddWarn($"{seq.name}: variants[{vi}].sequence 为 null,满足条件时会回退默认序列(可能是未完成配置)。", seq);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[DialogueModule] 验证完成:{allSeqs.Count} 个序列,{errorCount} 个错误,{warnCount} 个警告。");
|
||||
QuestValidationResultWindow.Show(issues, errorCount, warnCount, allSeqs.Count, "对话批量验证结果", "序列");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user