From aca53a6204f139d1fac24ddb68601743f9fc3c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A1=B9=E9=98=B3?= <39241051+insile@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:10:22 +0800 Subject: [PATCH] 2026/2/19 --- src/main.ts | 177 +++++++++++++++------- src/settings/SettingData.ts | 38 +++-- src/settings/SettingTab.ts | 291 +++++++++++++++++++++++------------- src/utils/InputSuggest.ts | 59 ++++++++ src/views/MainView.ts | 35 ++++- styles.css | 209 +++++++++++++++++++++++--- 6 files changed, 611 insertions(+), 198 deletions(-) diff --git a/src/main.ts b/src/main.ts index a7c9ae8..3b0d279 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import {MarkdownView, Notice, Plugin, TFile, TFolder, Vault} from 'obsidian'; +import {EventRef, MarkdownView, Notice, Plugin, TFile, TFolder, Vault} from 'obsidian'; import { OpenWordsSettings, DEFAULT_SETTINGS } from "./settings/SettingData"; import { OpenWordsSettingTab } from './settings/SettingTab'; import { MainView, MAIN_VIEW, PageType } from './views/MainView'; @@ -17,6 +17,9 @@ export default class OpenWords extends Plugin { enabledCards: Map = new Map(); // 启用单词 newCards: Map = new Map(); // 新单词 dueCards: Map = new Map(); // 旧单词 + currentFolderPath: string = ""; // 当前监听的文件夹路径 + fileWatcherRefs: EventRef[] = []; // 存储文件监听器的 EventRef 以便注销 + // 插件加载时执行的操作 async onload() { @@ -28,10 +31,6 @@ export default class OpenWords extends Plugin { // 等待布局完成 this.app.workspace.onLayoutReady(async () => { - const normalized = this.settings.folderPath.endsWith("/") - ? this.settings.folderPath - : this.settings.folderPath + "/"; - // 激活视图命令 this.addCommand({ id: 'LearningTypeModal', @@ -48,29 +47,8 @@ export default class OpenWords extends Plugin { this.addDoubleBrackets(); } }); - // 监听单词文件创建事件 - this.registerEvent(this.app.vault.on("create", (file: TFile) => { - if (file.path.endsWith(".md") && file.path.startsWith(normalized)) { - this.loadWordMetadata(file); - } - })); - // 监听单词文件删除事件 - this.registerEvent(this.app.vault.on("delete", (file: TFile) => { - if (file.path.endsWith(".md") && file.path.startsWith(normalized)) { - this.allCards.delete(file.basename); - this.masterCards.delete(file.basename); - this.enabledCards.delete(file.basename); - this.newCards.delete(file.basename); - this.dueCards.delete(file.basename); - this.updateStatusBar() - } - })); - // 监听单词文件缓存修改事件 - this.registerEvent(this.app.metadataCache.on("changed", (file) => { - if (file.path.endsWith(".md") && file.path.startsWith(normalized)) { - this.loadWordMetadata(file); - } - })); + // 注册文件监听器 + this.registerFileWatchers(); // 扫描所有单词文件, 更新状态栏 await this.scanAllNotes(); this.updateStatusBar(); @@ -121,15 +99,7 @@ export default class OpenWords extends Plugin { const tags: string[] = frontMatter.tags || []; const isMastered = frontMatter["掌握"] === true; - const isTagged = - (this.settings.enableWords1 && tags.includes('级别/小学')) || - (this.settings.enableWords2 && tags.includes('级别/中考')) || - (this.settings.enableWords3 && tags.includes('级别/高考四级')) || - (this.settings.enableWords4 && tags.includes('级别/考研')) || - (this.settings.enableWords5 && tags.includes('级别/六级')) || - (this.settings.enableWords6 && tags.includes('级别/雅思')) || - (this.settings.enableWords7 && tags.includes('级别/托福')) || - (this.settings.enableWords8 && tags.includes('级别/GRE')); + const isTagged = tags.some(tag => this.settings.enabledTags.includes(tag)); const card: CardInfo = { front: file.basename, @@ -218,7 +188,7 @@ export default class OpenWords extends Plugin { (mode === 'new' && !this.newCards.has(card.front)) || (mode === 'old' && !this.dueCards.has(card.front)) ) { - new Notice("当前单词不属于本模式范围, 评分无效并跳过"); + new Notice(`${card.front} 不属于本模式范围 \n评分无效并跳过`); return; } @@ -240,7 +210,16 @@ export default class OpenWords extends Plugin { new Notice(`${card.front} \n易记因子: ${result.efactor.toFixed(2)} \n重复次数: ${result.repetition} \n间隔: ${result.interval} \n到期日: ${newDate}`); - await new Promise(resolve => setTimeout(resolve, 200)) + // 等待元数据缓存更新(最多等待 1 秒) + let attempts = 0; + while (attempts < 10) { + const updated = this.app.metadataCache.getFileCache(file); + if (updated?.frontmatter?.["易记因子"] === Math.round(result.efactor * 100)) { + break; // 缓存已更新 + } + await new Promise(resolve => setTimeout(resolve, 100)); + attempts++; + } } // 重置单词属性 @@ -249,28 +228,57 @@ export default class OpenWords extends Plugin { new Notice("没有单词需要重置!"); return; } + + // 注销监听器,避免重置过程中触发大量缓存更新事件 + this.unregisterFileWatchers(); + const notice = new Notice('重置中...', 0); // 创建一个持续显示的 Notice let count = 0; // 计数器 const cardList = Array.from(this.enabledCards.values()); // 将 Map 转换为数组 - for (const card of cardList) { - const file = this.app.vault.getFileByPath(card.path); - if (!file) { - new Notice(`文件 ${card.path} 不存在!`); - continue; + const total = cardList.length; + const concurrency = 10; // 并发处理数量 + const todayDate = window.moment().format('YYYY-MM-DD'); // 提前计算日期,避免重复计算 + let updateFrequency = Math.max(1, Math.floor(total / 50)); // 减少更新频率到 2% + + // 创建并发处理任务 + const processTasks = async () => { + for (let i = 0; i < cardList.length; i += concurrency) { + const chunk = cardList.slice(i, i + concurrency); + await Promise.all(chunk.map(async (card) => { + const file = this.app.vault.getFileByPath(card.path); + if (file) { + await this.app.fileManager.processFrontMatter(file, (frontMatter) => { + frontMatter["到期日"] = todayDate; + frontMatter["间隔"] = 0; + frontMatter["易记因子"] = 250; + frontMatter["重复次数"] = 0; + }); + } + count++; + // 定期更新 Notice,减少 UI 更新频率 + if (count % updateFrequency === 0 || count === total) { + notice.setMessage(`重置中... ${count}/${total}`); + } + })); } - await this.app.fileManager.processFrontMatter(file, (frontMatter) => { - frontMatter["到期日"] = window.moment().format('YYYY-MM-DD'); - frontMatter["间隔"] = 0; - frontMatter["易记因子"] = 250; - frontMatter["重复次数"] = 0; - }); - count++; // 增加计数器 - notice.setMessage(`重置中... ${count}/${this.enabledCards.size}`); // 更新 Notice 的消息 - } - notice.setMessage(`重置完成!共 ${count} 个单词`); // 完成后更新消息 - setTimeout(() => notice.hide(), 2000); // 2 秒后自动隐藏 Notice - } + }; + try { + await processTasks(); + notice.setMessage(`重置完成!共 ${count} 个单词`); + } catch (error) { + new Notice(`重置出错: ${error}`); + console.error('Reset card error:', error); + } finally { + // 重新注册监听器 + this.registerFileWatchers(); + // 重新扫描所有单词文件,确保数据同步 + await this.scanAllNotes(); + this.updateStatusBar(); + setTimeout(() => notice.hide(), 2000); // 2 秒后自动隐藏 Notice + } + } + // 建立单词双链 async addDoubleBrackets() { const unmasteredWords = new Set(Array.from(this.enabledCards.values()) @@ -536,8 +544,63 @@ export default class OpenWords extends Plugin { }); } - onunload() { + // 注册文件监听器 + private registerFileWatchers() { + // 先清理旧的监听器 + this.unregisterFileWatchers(); + // 更新当前监听的文件夹路径 + this.currentFolderPath = this.settings.folderPath.endsWith("/") + ? this.settings.folderPath + : this.settings.folderPath + "/"; + + // 监听单词文件创建事件 + const createRef = this.app.vault.on("create", (file: TFile) => { + if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) { + this.loadWordMetadata(file); + } + }); + this.fileWatcherRefs.push(createRef); + + // 监听单词文件删除事件 + const deleteRef = this.app.vault.on("delete", (file: TFile) => { + if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) { + this.allCards.delete(file.basename); + this.masterCards.delete(file.basename); + this.enabledCards.delete(file.basename); + this.newCards.delete(file.basename); + this.dueCards.delete(file.basename); + this.updateStatusBar() + } + }); + this.fileWatcherRefs.push(deleteRef); + + // 监听单词文件缓存修改事件 + const changedRef = this.app.metadataCache.on("changed", (file) => { + if (file.path.endsWith(".md") && file.path.startsWith(this.currentFolderPath)) { + this.loadWordMetadata(file); + } + }); + this.fileWatcherRefs.push(changedRef); + } + + // 注销文件监听器 + private unregisterFileWatchers() { + this.fileWatcherRefs.forEach(ref => { + this.app.vault.offref(ref); + this.app.metadataCache.offref(ref); + }); + this.fileWatcherRefs = []; + } + + // 重新初始化文件监听器(供设置改变时调用) + async reinitializeFileWatchers() { + this.registerFileWatchers(); + } + + onunload() { + // 清理文件监听器 + this.unregisterFileWatchers(); } async loadSettings() { diff --git a/src/settings/SettingData.ts b/src/settings/SettingData.ts index 8c06e85..5234103 100644 --- a/src/settings/SettingData.ts +++ b/src/settings/SettingData.ts @@ -5,14 +5,8 @@ import { normalizePath } from "obsidian"; export interface OpenWordsSettings { folderPath: string; indexPath: string; - enableWords1: boolean; - enableWords2: boolean; - enableWords3: boolean; - enableWords4: boolean; - enableWords5: boolean; - enableWords6: boolean; - enableWords7: boolean; - enableWords8: boolean; + enabledTags: string[]; // 用户选择的标签列表 + tagHistory: string[]; // 标签输入历史记录 randomRatio: number; maxEfactorForLink: number; } @@ -22,14 +16,26 @@ export interface OpenWordsSettings { export const DEFAULT_SETTINGS: OpenWordsSettings = { folderPath: normalizePath(""), indexPath: normalizePath("索引"), - enableWords1: false, - enableWords2: false, - enableWords3: false, - enableWords4: false, - enableWords5: false, - enableWords6: false, - enableWords7: false, - enableWords8: false, + enabledTags: [ + "级别/小学", + "级别/中考", + "级别/高考四级", + "级别/考研", + "级别/六级", + "级别/雅思", + "级别/托福", + "级别/GRE", + ], + tagHistory: [ + "级别/小学", + "级别/中考", + "级别/高考四级", + "级别/考研", + "级别/六级", + "级别/雅思", + "级别/托福", + "级别/GRE", + ], randomRatio: 0.7, maxEfactorForLink: 2.6, }; diff --git a/src/settings/SettingTab.ts b/src/settings/SettingTab.ts index b9c1ff3..ea5b512 100644 --- a/src/settings/SettingTab.ts +++ b/src/settings/SettingTab.ts @@ -1,5 +1,6 @@ import { App, Notice, PluginSettingTab, Setting, normalizePath } from 'obsidian'; -import { FolderSuggest } from '../utils/InputSuggest'; +import { FolderSuggest, TagHistorySuggest } from '../utils/InputSuggest'; +import { DEFAULT_SETTINGS } from './SettingData'; import OpenWords from '../main'; @@ -15,140 +16,189 @@ export class OpenWordsSettingTab extends PluginSettingTab { display(): void { const {containerEl} = this; containerEl.empty(); - const textContainer = containerEl.createDiv({ cls: 'openwords-text-grid' }); - const toggleContainer = containerEl.createDiv({ cls: 'openwords-toggle-grid' }); - this.plugin.settingsSnapshot = Object.assign({}, this.plugin.settings); // 创建设置快照 - new Setting(textContainer) + + // 初始化设置快照 + this.plugin.settingsSnapshot = Object.assign({}, this.plugin.settings); + this.plugin.settingsSnapshot.enabledTags = [...this.plugin.settings.enabledTags]; + this.plugin.settingsSnapshot.tagHistory = [...(this.plugin.settings.tagHistory || this.plugin.settings.enabledTags)]; + + // 初始化设置界面 + this.createPathSettings(containerEl); + this.createTagManagement(containerEl); + this.createActionButtons(containerEl); + } + + private createPathSettings(container: HTMLElement) { + new Setting(container) .setName('单词文件夹路径') - .setDesc('指定存放单词文件的文件夹路径,默认为仓库:/') + .setDesc('指定存放单词文件的文件夹路径,默认为 "仓库:/"') .addText(text => { const input = text.setValue(this.plugin.settings.folderPath); - new FolderSuggest(this.app, input.inputEl); // 添加文件夹路径自动补全支持 + new FolderSuggest(this.app, input.inputEl); input.onChange(async (value) => { this.plugin.settingsSnapshot.folderPath = normalizePath(value); }); }); - new Setting(textContainer) + new Setting(container) .setName('索引文件夹路径') - .setDesc('指定存放索引文件的文件夹路径,默认为仓库:索引') + .setDesc('指定存放索引文件的文件夹路径,默认为 "仓库:索引"') .addText(text => { const input = text.setValue(this.plugin.settings.indexPath); - new FolderSuggest(this.app, input.inputEl); // 添加文件夹路径自动补全支持 + new FolderSuggest(this.app, input.inputEl); input.onChange(async (value) => { this.plugin.settingsSnapshot.indexPath = normalizePath(value); }); }); - new Setting(toggleContainer) - .setName('小学') - .setDesc('启用小学单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords1) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords1 = value; - })); - new Setting(toggleContainer) - .setName('中考') - .setDesc('启用中考单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords2) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords2 = value; - })); - new Setting(toggleContainer) - .setName('高四') - .setDesc('启用高四单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords3) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords3 = value; - })); - new Setting(toggleContainer) - .setName('考研') - .setDesc('启用考研单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords4) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords4 = value; - })); - new Setting(toggleContainer) - .setName('六级') - .setDesc('启用六级单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords5) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords5 = value; - })); - new Setting(toggleContainer) - .setName('雅思') - .setDesc('启用雅思单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords6) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords6 = value; - })); - new Setting(toggleContainer) - .setName('托福') - .setDesc('启用托福单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords7) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords7 = value; - })); - new Setting(toggleContainer) - .setName('GRE') - .setDesc('启用GRE单词') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWords8) - .onChange(async (value) => { - this.plugin.settingsSnapshot.enableWords8 = value; - })); + } + + private createTagManagement(containerEl: HTMLElement) { + const tagsSetting = new Setting(containerEl); + tagsSetting.setName('启用的标签') + .setDesc('选择要启用的标签,点击下方按钮可添加更多标签'); + + const tagsContainer = containerEl.createDiv({ cls: 'openwords-tags-container' }); + tagsSetting.settingEl.appendChild(tagsContainer); + + const renderTags = () => { + tagsContainer.empty(); + if (this.plugin.settingsSnapshot.enabledTags.length === 0) { + tagsContainer.createEl('p', { text: '暂无启用的标签', cls: 'openwords-empty-tags' }); + } else { + this.plugin.settingsSnapshot.enabledTags.forEach((tag, index) => { + const tagEl = tagsContainer.createEl('div', { cls: 'openwords-tag-item' }); + tagEl.createEl('span', { text: tag }); + tagEl.createEl('button', { text: '删除' }).onclick = () => { + this.plugin.settingsSnapshot.enabledTags.splice(index, 1); + renderTags(); + }; + }); + } + }; + + // 添加标签输入与历史建议 + new Setting(containerEl) + .setName('添加标签') + .setDesc('输入新的标签名称,例如 "级别/小学"') + .addText(text => { + text.setPlaceholder('输入标签名称'); + text.onChange(async (value) => { + (text.inputEl as any).currentValue = value; + }); + + new TagHistorySuggest( + this.app, + text.inputEl, + this.plugin.settingsSnapshot.tagHistory || [], + (selectedValue) => text.setValue(selectedValue), + async (valueToDelete) => { + const index = this.plugin.settingsSnapshot.tagHistory.indexOf(valueToDelete); + if (index > -1) { + this.plugin.settingsSnapshot.tagHistory.splice(index, 1); + } + } + ); + + text.inputEl.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const value = (text.inputEl as any).value?.trim(); + if (value && !this.plugin.settingsSnapshot.enabledTags.includes(value)) { + this.plugin.settingsSnapshot.enabledTags.push(value); + if (!this.plugin.settingsSnapshot.tagHistory.includes(value)) { + this.plugin.settingsSnapshot.tagHistory.push(value); + } + renderTags(); + text.setValue(''); + } else if (value && this.plugin.settingsSnapshot.enabledTags.includes(value)) { + new Notice('该标签已存在!'); + } + } + }); + }) + .addButton(button => button + .setButtonText('添加') + .onClick(() => { + const inputs = containerEl.querySelectorAll('input[type="text"]'); + let tagInput = ''; + for (let i = inputs.length - 1; i >= 0; i--) { + const input = inputs[i] as HTMLInputElement; + if (input.placeholder === '输入标签名称') { + tagInput = input.value?.trim() || ''; + break; + } + } + if (tagInput && !this.plugin.settingsSnapshot.enabledTags.includes(tagInput)) { + this.plugin.settingsSnapshot.enabledTags.push(tagInput); + if (!this.plugin.settingsSnapshot.tagHistory.includes(tagInput)) { + this.plugin.settingsSnapshot.tagHistory.push(tagInput); + } + renderTags(); + for (let i = inputs.length - 1; i >= 0; i--) { + const input = inputs[i] as HTMLInputElement; + if (input.placeholder === '输入标签名称') { + input.value = ''; + break; + } + } + } else if (tagInput && this.plugin.settingsSnapshot.enabledTags.includes(tagInput)) { + new Notice('该标签已存在!'); + } else { + new Notice('请输入标签名称!'); + } + })); + + renderTags(); + } + + private createActionButtons(containerEl: HTMLElement) { new Setting(containerEl) .setName('扫描单词') .setDesc('按路径和级别设置扫描并缓存单词') .addButton(button => button .setButtonText('扫描') - .setCta() // 设置为主要按钮样式 + .setCta() .onClick(async () => { - if (button.disabled) return; // 如果按钮已禁用,直接返回 - - button.setDisabled(true); // 禁用按钮 - button.setButtonText('处理中...'); // 更新按钮文本 - + if (button.disabled) return; + button.setDisabled(true); + button.setButtonText('处理中...'); try { - this.plugin.settings = {...this.plugin.settingsSnapshot}; // 更新插件设置 - await this.plugin.saveSettings(); // 保存设置 - await this.plugin.scanAllNotes(); // 重新缓存单词 - new Notice(`扫描完成!共 ${this.plugin.allCards.size} 个单词`); // 完成后更新消息 - this.plugin.updateStatusBar(); // 更新状态栏 + this.plugin.settings = {...this.plugin.settingsSnapshot}; + this.plugin.settings.enabledTags = [...this.plugin.settingsSnapshot.enabledTags]; + this.plugin.settings.tagHistory = [...(this.plugin.settingsSnapshot.tagHistory || this.plugin.settingsSnapshot.enabledTags)]; + await this.plugin.saveSettings(); + // 重新初始化文件监听器 + await this.plugin.reinitializeFileWatchers(); + await this.plugin.scanAllNotes(); + new Notice(`扫描完成!共 ${this.plugin.allCards.size} 个单词`); + this.plugin.updateStatusBar(); } catch (error) { console.error('缓存过程中发生错误:', error); new Notice('缓存失败,请检查控制台日志!'); } finally { - button.setDisabled(false); // 重新启用按钮 - button.setButtonText('扫描'); // 恢复按钮文本 + button.setDisabled(false); + button.setButtonText('扫描'); } })); + new Setting(containerEl) .setName('生成索引') .setDesc('按级别生成所有单词的首字母索引文件') .addButton(button => button .setButtonText('索引') .onClick(async () => { - if (button.disabled) return; // 如果按钮已禁用,直接返回 - - button.setDisabled(true); // 禁用按钮 - button.setButtonText('处理中...'); // 更新按钮文本 - + if (button.disabled) return; + button.setDisabled(true); + button.setButtonText('处理中...'); try { - await this.plugin.generateIndex(); // 生成索引文件 + await this.plugin.generateIndex(); } catch (error) { console.error('生成索引过程中发生错误:', error); new Notice('生成索引失败,请检查控制台日志!'); } finally { - button.setDisabled(false); // 重新启用按钮 - button.setButtonText('生成'); // 恢复按钮文本 + button.setDisabled(false); + button.setButtonText('生成'); } })); + new Setting(containerEl) .setName("随机调度概率") .setDesc("设置每次抽卡时随机调度的概率,其余为优先调度概率") @@ -163,6 +213,7 @@ export class OpenWordsSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); + new Setting(containerEl) .setName("最大双链因子") .setDesc("易记因子小于等于该值的单词可添加双链") @@ -177,25 +228,57 @@ export class OpenWordsSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); + + new Setting(containerEl) + .setName('恢复默认设置') + .setDesc('将所有插件设置恢复为默认值') + .addButton(button => button + .setButtonText('恢复默认设置') + .onClick(async () => { + if (button.disabled) return; + button.setDisabled(true); + button.setButtonText('处理中...'); + try { + this.plugin.settings = Object.assign({}, DEFAULT_SETTINGS); + await this.plugin.saveSettings(); + new Notice('已恢复默认设置'); + // 重新渲染设置页以反映默认值 + this.display(); + this.plugin.settings = {...this.plugin.settingsSnapshot}; + this.plugin.settings.enabledTags = [...this.plugin.settingsSnapshot.enabledTags]; + this.plugin.settings.tagHistory = [...(this.plugin.settingsSnapshot.tagHistory || this.plugin.settingsSnapshot.enabledTags)]; + await this.plugin.saveSettings(); + // 重新初始化文件监听器 + await this.plugin.reinitializeFileWatchers(); + await this.plugin.scanAllNotes(); + new Notice(`扫描完成!共 ${this.plugin.allCards.size} 个单词`); + this.plugin.updateStatusBar(); + } catch (error) { + console.error('恢复默认设置失败:', error); + new Notice('恢复失败,请检查控制台日志!'); + } finally { + button.setDisabled(false); + button.setButtonText('恢复默认设置'); + } + })); + new Setting(containerEl) .setName('重置单词属性') .setDesc('重置作用域单词的易记因子、重复次数、间隔和到期日') .addButton(button => button - .setButtonText('重置') + .setButtonText('重置单词属性') .onClick(async () => { - if (button.disabled) return; // 如果按钮已禁用,直接返回 - - button.setDisabled(true); // 禁用按钮 - button.setButtonText('处理中...'); // 更新按钮文本 - + if (button.disabled) return; + button.setDisabled(true); + button.setButtonText('处理中...'); try { - await this.plugin.resetCard(); // 重置单词属性 + await this.plugin.resetCard(); } catch (error) { console.error('重置过程中发生错误:', error); new Notice('重置失败,请检查控制台日志!'); } finally { - button.setDisabled(false); // 重新启用按钮 - button.setButtonText('重置'); // 恢复按钮文本 + button.setDisabled(false); + button.setButtonText('重置单词属性'); } })); } diff --git a/src/utils/InputSuggest.ts b/src/utils/InputSuggest.ts index 5962393..d0c67aa 100644 --- a/src/utils/InputSuggest.ts +++ b/src/utils/InputSuggest.ts @@ -32,3 +32,62 @@ export class FolderSuggest extends AbstractInputSuggest { this.close(); } } + + +// 标签历史建议类,用于显示标签输入历史 +export class TagHistorySuggest extends AbstractInputSuggest { + inputEl: HTMLInputElement; + tagHistory: string[]; + onSelectCallback: (value: string) => void; + onDeleteCallback: (value: string) => Promise; + + constructor( + app: App, + inputEl: HTMLInputElement, + tagHistory: string[], + onSelectCallback: (value: string) => void, + onDeleteCallback: (value: string) => Promise + ) { + super(app, inputEl); + this.inputEl = inputEl; + this.tagHistory = tagHistory; + this.onSelectCallback = onSelectCallback; + this.onDeleteCallback = onDeleteCallback; + } + + getSuggestions(query: string): string[] { + if (!query.trim()) { + // 如果查询为空,返回所有历史记录(反向排序,最新的在前) + return [...this.tagHistory].reverse(); + } + // 否则按查询过滤,然后反向排序 + return this.tagHistory.filter(tag => + tag.toLowerCase().includes(query.toLowerCase()) + ).reverse(); + } + + renderSuggestion(value: string, el: HTMLElement): void { + const container = el.createDiv({ cls: 'openwords-tag-history-item' }); + + container.createDiv({ text: value, cls: 'openwords-tag-history-text' }); + + const deleteBtn = container.createEl('button', { + cls: 'openwords-tag-history-delete', + text: '删除', + }); + + deleteBtn.onclick = async (e) => { + e.stopPropagation(); + await this.onDeleteCallback(value); + // 删除该条历史记录的 DOM 元素,保持提示框打开 + el.remove(); + }; + } + + selectSuggestion(value: string): void { + this.inputEl.value = value; + this.inputEl.trigger('input'); + this.close(); + this.onSelectCallback(value); + } +} diff --git a/src/views/MainView.ts b/src/views/MainView.ts index 4050e11..b73a75c 100644 --- a/src/views/MainView.ts +++ b/src/views/MainView.ts @@ -84,9 +84,36 @@ export class MainView extends ItemView { async updateStatusBar() { if (!this.statusBarEl) return; - this.statusBarEl.setText( - `${this.plugin.newCards.size} + ${Array.from(this.plugin.dueCards.values()) - .filter(card => window.moment(card.dueDate).isBefore(window.moment())).length} / ${this.plugin.dueCards.size} + ${this.plugin.allCards.size - this.plugin.enabledCards.size} = ${this.plugin.allCards.size}` - ); + + // 计算各分类的单词数 + const newCount = this.plugin.newCards.size; + const dueTodayCount = Array.from(this.plugin.dueCards.values()) + .filter(card => window.moment(card.dueDate).isBefore(window.moment())).length; + const reviewCount = this.plugin.dueCards.size - dueTodayCount; + const masteredCount = this.plugin.masterCards.size; + const disabledCount = this.plugin.allCards.size - this.plugin.enabledCards.size; + const totalCount = this.plugin.allCards.size; + + // 清空状态栏 + this.statusBarEl.empty(); + + // 创建第一行:新单词、待复习、今日到期、已掌握、未启用 + const row1 = this.statusBarEl.createDiv({ cls: 'openwords-statusbar-row' }); + + this.createStatItem(row1, '新单词', newCount); + this.createStatItem(row1, '待复习', reviewCount); + this.createStatItem(row1, '今日到期', dueTodayCount); + this.createStatItem(row1, '已掌握', masteredCount); + this.createStatItem(row1, '未启用', disabledCount); + + // 创建第二行:总计 + const row2 = this.statusBarEl.createDiv({ cls: 'openwords-statusbar-row openwords-statusbar-total' }); + this.createStatItem(row2, '总计', totalCount, true); + } + + private createStatItem(container: HTMLElement, label: string, count: number, isTotal: boolean = false) { + const item = container.createDiv({ cls: isTotal ? 'openwords-stat-item total' : 'openwords-stat-item' }); + item.createSpan({ cls: 'openwords-stat-label', text: label }); + item.createSpan({ cls: 'openwords-stat-count', text: count.toString() }); } } diff --git a/styles.css b/styles.css index b3e5d03..e0a3557 100644 --- a/styles.css +++ b/styles.css @@ -8,18 +8,6 @@ If your plugin does not need CSS, delete this file. */ -/* 插件设置选项卡 */ -.openwords-toggle-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); /* 每行 4 列 */ - gap: 10px; /* 每个 toggle 之间的间距 */ - /* margin-top: 10px; */ -} -.openwords-toggle-grid .setting-item:first-child { - padding-top: 10px; - border-top: 1px solid var(--background-modifier-border) -} - /* 返回按钮 */ .openwords-back-btn { display: block; @@ -29,13 +17,130 @@ If your plugin does not need CSS, delete this file. font-size: 1.05em; } +/* 状态栏 */ .openwords-view-statusbar { - display: block; - text-align: center; - margin: 32px auto 0 auto; - font-size: 1.1em; - color: var(--text-muted); + display: flex; + flex-direction: column; + gap: 20px; + margin: 20px auto 0 auto; + padding: 20px; + width: 70%; + max-width: 700px; + box-sizing: border-box; + background: linear-gradient(135deg, var(--background-secondary) 0%, var(--background-primary) 100%); + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.openwords-statusbar-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; width: 100%; + flex-wrap: wrap; +} + +.openwords-statusbar-total { + justify-content: center; + padding-top: 20px; + border-top: 2px solid var(--divider-color); +} + +.openwords-stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + flex: 1; + padding: 2px 2px; + background-color: var(--background-primary); + border-radius: 8px; + border: 1px solid var(--divider-color); + transition: all 0.3s ease; +} + +.openwords-stat-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-color: var(--text-accent); +} + +.openwords-stat-item.total { + padding: 2px 2px; + background: none; + border: 1px solid var(--divider-color); + color: var(--text-normal); +} + +.openwords-stat-item.total:hover { + transform: scale(1.05); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); +} + +.openwords-stat-label { + font-size: 0.8em; + color: var(--text-muted); + white-space: nowrap; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.openwords-stat-item.total .openwords-stat-label { + font-size: 0.8em; + letter-spacing: 0.5px; +} + +.openwords-stat-count { + font-size: 1.4em; + font-weight: 700; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.openwords-stat-item:nth-child(1) .openwords-stat-count { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.openwords-stat-item:nth-child(2) .openwords-stat-count { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.openwords-stat-item:nth-child(3) .openwords-stat-count { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.openwords-stat-item:nth-child(4) .openwords-stat-count { + background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.openwords-stat-item:nth-child(5) .openwords-stat-count { + background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.openwords-stat-item.total .openwords-stat-count { + font-size: 1.6em; + color: var(--text-normal); + background: none; + -webkit-text-fill-color: initial; } /* 学习模式页面 */ @@ -179,3 +284,73 @@ If your plugin does not need CSS, delete this file. font-size: 1em; color: var(--text-accent); } + + +/* 插件设置标签管理 */ +.openwords-tags-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + background-color: var(--background-secondary); + border-radius: 5px; +} + +.openwords-tag-item { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border-radius: 4px; + font-size: 0.9em; +} + +.openwords-tag-item button { + padding: 2px 6px; + border: none; + background-color: rgba(255, 255, 255, 0.3); + color: inherit; + border-radius: 3px; + cursor: pointer; + font-size: 0.85em; + transition: background-color 0.2s; +} + +.openwords-tag-item button:hover { + background-color: rgba(255, 255, 255, 0.5); +} + +.openwords-empty-tags { + color: var(--text-muted); + font-size: 0.9em; + margin: 0; +} + +/* 标签历史建议样式 */ +.openwords-tag-history-item { + display: flex; + justify-content: space-between; + align-items: center; + /* padding: 4px 0; */ +} + +.openwords-tag-history-text { + flex: 1; + word-break: break-all; +} + +.openwords-tag-history-delete { + padding: 2px 8px; + font-size: 12px; + margin-left: 8px; + border: none; + border-radius: 3px; + background-color: rgba(255, 255, 255, 0.3); + cursor: pointer; + transition: background-color 0.2s; +} + +.openwords-tag-history-delete:hover { + background-color: rgba(255, 255, 255, 0.5); +} \ No newline at end of file