File size: 6,506 Bytes
605cef2 12c5b18 605cef2 fc062b2 9702df4 605cef2 6284eeb 12c5b18 e994b4e 605cef2 12c5b18 fc062b2 9702df4 605cef2 12c5b18 605cef2 6284eeb 605cef2 4030987 605cef2 12c5b18 e994b4e 12c5b18 e994b4e 4030987 e994b4e 12c5b18 6284eeb 605cef2 12c5b18 605cef2 12c5b18 ce2c2f8 12c5b18 605cef2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | /**
* 应用状态管理模块
* 负责集中管理应用状态和状态驱动的按钮状态更新
*/
import * as d3 from 'd3';
/**
* 应用状态对象
*/
export interface AppState {
isAnalyzing: boolean;
isGlobalLoading: boolean;
isSaving: boolean; // 是否正在保存
isSemanticSearching: boolean; // 语义搜索进行中
lastSearchedQuery: string | null; // 上次成功搜索的 query,用于在输入未变化时保持 Search 按钮灰色
hasValidData: boolean; // 是否有有效数据
// 以下字段仅用于按钮状态控制
dataSource: 'local' | 'server' | null; // 数据来源:null表示新分析的内容
isSavedToLocal: boolean; // 是否已保存到本地
isSavedToServer: boolean; // 是否已保存到服务器
currentFileName: string | null; // 当前文件名(本地或服务器)
}
/**
* 按钮状态管理依赖
*/
export interface ButtonStateDependencies {
submitBtn: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
saveBtn: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
saveLocalBtn: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
textField: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
textMetrics: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
semanticSearchBtn?: d3.Selection<HTMLElement, unknown, HTMLElement, unknown>;
/** 获取当前语义搜索输入框的 query,用于判断是否与上次搜索一致 */
getSemanticSearchQuery?: () => string;
/** 翻译函数,用于 Search/Stop 按钮文案 */
tr?: (key: string) => string;
}
/**
* 应用状态管理器
*/
export class AppStateManager {
private state: AppState;
private deps: ButtonStateDependencies;
constructor(deps: ButtonStateDependencies) {
this.state = {
isAnalyzing: false,
isGlobalLoading: false,
isSaving: false,
isSemanticSearching: false,
lastSearchedQuery: null,
hasValidData: false,
dataSource: null,
isSavedToLocal: false,
isSavedToServer: false,
currentFileName: null
};
this.deps = deps;
}
/**
* 获取当前状态
*/
getState(): Readonly<AppState> {
return { ...this.state };
}
/**
* 获取 isAnalyzing 状态
*/
getIsAnalyzing(): boolean {
return this.state.isAnalyzing;
}
/**
* 设置 isAnalyzing 状态
*/
setIsAnalyzing(analyzing: boolean): void {
this.updateState({ isAnalyzing: analyzing });
}
/**
* 设置全局 loading 状态
*/
setGlobalLoading(loading: boolean): void {
this.updateState({ isGlobalLoading: loading });
}
setSemanticSearching(searching: boolean): void {
this.updateState({ isSemanticSearching: searching });
}
setLastSearchedQuery(query: string | null): void {
this.updateState({ lastSearchedQuery: query });
}
/**
* 根据应用状态计算并更新所有按钮状态和UI元素状态
*/
private updateButtonStatesFromAppState(): void {
const hasText = (this.deps.textField.property('value') || '').length > 0;
const isBusy = this.state.isAnalyzing || this.state.isGlobalLoading || this.state.isSaving;
// 数据完整性:有有效数据即可保存
const dataReadyForSave = this.state.hasValidData;
// Analyze按钮:有文本 && 不忙 && 无有效数据(复用 hasValidData,与 TextMetrics 一致:有数据时灰色)
this.deps.submitBtn.classed('inactive', !hasText || isBusy || this.state.hasValidData);
// Upload / Save:仅在没有数据可保存时禁用
this.deps.saveBtn.classed('inactive', !dataReadyForSave);
this.deps.saveLocalBtn.classed('inactive', !dataReadyForSave);
// 语义搜索按钮:Search/Stop 共用。进行中显示 Stop 且可点;否则显示 Search,按条件禁用
if (this.deps.semanticSearchBtn && !this.deps.semanticSearchBtn.empty()) {
const tr = this.deps.tr ?? ((k: string) => k);
this.deps.semanticSearchBtn.text(this.state.isSemanticSearching ? tr('Stop') : tr('Search'));
if (this.state.isSemanticSearching) {
this.deps.semanticSearchBtn.classed('inactive', false);
} else {
const currentQuery = this.deps.getSemanticSearchQuery?.() ?? '';
const queryUnchanged = this.state.lastSearchedQuery !== null && currentQuery === this.state.lastSearchedQuery;
const canRunSemantic = (hasText || this.state.hasValidData) && currentQuery.length > 0;
this.deps.semanticSearchBtn.classed('inactive', !canRunSemantic || queryUnchanged);
}
}
// TextMetrics显示:有有效数据(hasValidData = true 时,stats 一定不为 null)
if (!this.deps.textMetrics.empty()) {
this.deps.textMetrics.classed('is-hidden', !this.state.hasValidData);
}
}
/**
* 更新应用状态并触发按钮状态更新
*/
updateState(updates: Partial<AppState>): void {
// 数据变更时清空 lastSearchedQuery(参考 TextMetrics:数据变化则状态重置)
if (updates.hasValidData === false) {
updates = { ...updates, lastSearchedQuery: null };
}
Object.assign(this.state, updates);
this.updateButtonStatesFromAppState();
// 同时更新全局loading的UI(如果需要)
if (updates.isGlobalLoading !== undefined) {
d3.selectAll(".loadersmall").style('display', updates.isGlobalLoading ? null : 'none');
// 隐藏进度显示
if (!updates.isGlobalLoading) {
d3.select('#analyze_progress').text('').style('display', 'none');
}
}
// 语义搜索 loading:独立 loader
if (updates.isSemanticSearching !== undefined) {
d3.select('#semantic_search_loader').style('visibility', updates.isSemanticSearching ? 'visible' : 'hidden');
if (!updates.isSemanticSearching) {
d3.select('#semantic_progress').text('').style('display', 'none');
}
}
}
/**
* 手动触发按钮状态更新(用于外部调用)
*/
updateButtonStates(): void {
this.updateButtonStatesFromAppState();
}
}
|