2026/2/19

This commit is contained in:
项阳 2026-02-19 15:10:22 +08:00
parent 3eecf75779
commit aca53a6204
6 changed files with 611 additions and 198 deletions

View file

@ -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<string, CardInfo> = new Map(); // 启用单词
newCards: Map<string, CardInfo> = new Map(); // 新单词
dueCards: Map<string, CardInfo> = 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() {

View file

@ -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,
};

View file

@ -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('重置单词属性');
}
}));
}

View file

@ -32,3 +32,62 @@ export class FolderSuggest extends AbstractInputSuggest<string> {
this.close();
}
}
// 标签历史建议类,用于显示标签输入历史
export class TagHistorySuggest extends AbstractInputSuggest<string> {
inputEl: HTMLInputElement;
tagHistory: string[];
onSelectCallback: (value: string) => void;
onDeleteCallback: (value: string) => Promise<void>;
constructor(
app: App,
inputEl: HTMLInputElement,
tagHistory: string[],
onSelectCallback: (value: string) => void,
onDeleteCallback: (value: string) => Promise<void>
) {
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);
}
}

View file

@ -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() });
}
}

View file

@ -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);
}