2025/6/21

This commit is contained in:
insile 2025-06-21 15:56:06 +08:00
parent 823d7ba084
commit 89f3491fac
10 changed files with 244 additions and 118 deletions

View file

@ -1,6 +1,6 @@
# OpenWords
**OpenWords** 是一个用于背单词和单词管理的 Obsidian 插件, 与 [OpenText](https://github.com/insile/OpenText) 单词库配套设计, 已上线[社区插件市场](obsidian://show-plugin?id=openwords), 安装后插件入口是左侧工具栏上的一个按钮再加上两个绑定命令, 主要功能有生成索引, 背单词, 默写单词, 添加双链等, 其中单词文件使用 OpenText 中的单词库, 而背单词的核心逻辑是 SuperMemo 2 间隔重复算法. 本插件完全依赖单词文件及其属性, 没有外部存储文件没有记录回答历史 (考虑中), 所以不能做一些记忆曲线拟合, 单词调度看起来很简单朴素, 仅依靠上次回答记录. 插件启动时会读取所有单词缓存, 更改时会即时写入相应的单词文件并更新缓存. 插件没有设计过多的搜索功能, 提倡使用 Obsidian 内置的搜索功能. 如果您认为本插件有帮助, 欢迎微信赞赏支持. 目前等待 Obsidian Bases 正式上线后进行功能更新
**OpenWords** 是一个用于背单词和单词管理的 Obsidian 插件, 与 [OpenText](https://github.com/insile/OpenText) 单词库配套设计, 已上线[社区插件市场](obsidian://show-plugin?id=openwords), 安装后插件入口是左侧工具栏上的一个按钮再加上两个绑定命令, 主要功能有生成索引, 背单词, 默写单词, 添加双链等, 其中单词文件使用 OpenText 中的单词库, 而背单词的核心逻辑是 SuperMemo 2 间隔重复算法. 本插件完全依赖单词文件及其属性, 没有外部存储记录回答历史, 所以不能做一些记忆曲线拟合, 单词调度看起来很简单朴素, 仅依靠上次回答记录. 插件启动时会读取所有单词缓存, 更改时会即时写入相应的单词文件并更新缓存. 插件没有设计过多的搜索功能, 提倡使用 Obsidian 内置的搜索功能. 如果您认为本插件有帮助, 欢迎微信赞赏支持. 目前等待 Obsidian Bases 正式上线后进行功能更新
![p7](README/p7.png)
@ -41,7 +41,7 @@
### 掌握列表
掌握列表能够即时快速修改单词的 `掌握` 属性, 首先将自己无比熟悉的单词设置为掌握, 例如 hello, that, my 等, 可以防止它们出现在后续功能中
掌握列表能够即时快速修改单词的 `掌握` 属性, 可以首先将自己无比熟悉的单词设置为掌握, 例如 hello, that, my 等, 可以防止它们出现在后续功能中, 到后期再将新学会的单词设置为掌握
![p2](README/p2.gif)
@ -53,19 +53,33 @@
### 背单词
背单词目标是尽可能的提高每个单词的重复次数和易记因子, 详细计算过程可查阅 SM-2 文档. 新词指重复次数为 0 的单词, 它可能是从未被学习过的, 也可能是前面学后面忘了的. 新词调度中 70% 纯随机, 30% 从易记因子和重复次数最低的 20% 中随机; 旧词指重复次数大于 0 的单词, 它是之前回答正确的. 旧词调度从已过期的单词中 70% 纯随机, 30% 从易记因子和重复次数最低的 20% 中随机. 回答的 0-5 分已与数字键绑定方便键盘控制
背单词目标是尽可能的提高每个单词的重复次数和易记因子, 详细计算过程可查阅 SM-2 文档, 随机调度概率设置每次抽卡时随机调度的概率, 其余为优先调度概率. 默认 70 % 随机调度, 30% 优先调度. `0-5` 数字键评分, `Tab` 键显示答案
- 新词指重复次数为 0 的单词, 它可能是从未被学习过的, 也可能是前面学后面忘了的. 新词间隔只能是 0 或 1, 其中 0 是完全没有学习过的, 1 是至少学过一次的. 新词优先调度遵从间隔降序和易记因子升序, 从前 1% 中随机
- 旧词指重复次数大于 0 的单词, 它是之前回答正确的. 旧词优先调度遵从过期的易记因子和重复次数升序, 从前 1% 中随机
回答评分, 答错降低易记因子, 重复次数和间隔重置, 答对易记因子增长缓慢, 间隔逐渐延长
- 评分 0, 易记因子 -0.8
- 评分 1, 易记因子 -0.54
- 评分 2, 易记因子 -0.32
- 评分 3, 易记因子 -0.14
- 评分 4, 易记因子 -0
- 评分 5, 易记因子 +0.1
![p4](README/p4.gif)
### 默写单词
默写单词是从作用域中随机抽取单词进行拼写, 目前只是一个练习功能, 不会更改任何单词属性, 按下 `Tab` 键显示答案
默写单词是从易记因子最低的 50% 旧词中随机抽取单词进行拼写, 会影响易记因子, 按下 `Tab` 键显示答案
- 没有看答案, 直接拼写正确, 易记因子 + 0.15
- 没有看答案, 经过几次错误后拼写正确, 易记因子 + 0.05
- 看答案, 易记因子 - 0.02
- 易记因子最低不低于 1.3
![p8](README/p8.gif)
### 添加双链
为当前活动页面笔记中的单词添加双链, 单词范围是作用域中易记因子小于等于 2.5 的, 所以默认情况所有单词都会添加. 词形还原准确率在 90 %. 需要文章尽可能规范, 减少不必要的标点符号. 目前只能通过 `ctrl z` 回撤, 请谨慎使用
为当前活动页面笔记中的单词添加双链, 单词范围是作用域中易记因子小于等于设置的参数, 所以默认情况所有单词都会添加. 词形还原准确率在 90 %. 需要文章尽可能规范, 减少不必要的标点符号. 可以将参数设为 1.2 进行还原, 因为没有单词易记因子小于 1.3
![p5](README/p5.gif)
@ -118,4 +132,4 @@ function supermemo(item, grade) {
};
}
```
```

View file

@ -1,7 +1,7 @@
{
"id": "openwords",
"name": "OpenWords",
"version": "1.0.5",
"version": "1.0.6",
"minAppVersion": "1.8.10",
"description": "用于英语学习中背单词与单词管理的插件",
"author": "insile",

View file

@ -1,6 +1,6 @@
{
"name": "openwords",
"version": "1.0.5",
"version": "1.0.6",
"description": "用于英语学习中背单词与单词管理的插件",
"main": "main.js",
"scripts": {

View file

@ -18,6 +18,8 @@ interface OpenWordsSettings {
enableWords6: boolean;
enableWords7: boolean;
enableWords8: boolean;
randomRatio: number;
maxEfactorForLink: number;
}
// 默认设置
@ -32,6 +34,8 @@ const DEFAULT_SETTINGS: OpenWordsSettings = {
enableWords6: false,
enableWords7: false,
enableWords8: false,
randomRatio: 0.7,
maxEfactorForLink: 2.6,
}
@ -257,13 +261,9 @@ export default class OpenWords extends Plugin {
// 建立单词双链
async addDoubleBrackets() {
const unmasteredWords = new Set(Array.from(this.enabledCards.values())
.filter(card => card.efactor <= 2.5)
.filter(card => card.efactor <= this.settings.maxEfactorForLink) // 只选择易记因子小于等于设置值的单词
.map(card => card.front)
);
if (unmasteredWords.size === 0) {
new Notice("没有单词需要添加双链!");
return;
}
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeView) {

View file

@ -21,75 +21,22 @@ export class LearningModal extends Modal {
}
onOpen() {
// 加载组件并选卡
this.component.load()
this.pickNextCard();
if (!this.currentCard) return;
// 创建卡片容器和设置容器
const { contentEl } = this;
contentEl.empty();
if (!this.currentCard) return;
// 创建卡片容器并固定样式
const cardContainer = contentEl.createDiv({ cls: 'openwords-card' });
const settingsContainer = contentEl.createDiv({ cls: 'openwords-card-settings' });
this.renderSettings(settingsContainer, cardContainer);
this.renderCard(cardContainer);
new Setting(settingsContainer)
.setName(`评分 0: 回答错误, 完全不会`)
.addButton(btn => btn
.setButtonText('0')
.onClick(() => this.rateCard(0 as SuperMemoGrade, cardContainer)))
new Setting(settingsContainer)
.setName(`评分 1: 回答错误, 看到正确答案后感觉很熟悉`)
.addButton(btn => btn
.setButtonText('1')
.onClick(() => this.rateCard(1 as SuperMemoGrade, cardContainer)))
new Setting(settingsContainer)
.setName(`评分 2: 回答错误, 看到正确答案后感觉很容易记住`)
.addButton(btn => btn
.setButtonText('2')
.onClick(() => this.rateCard(2 as SuperMemoGrade, cardContainer)))
new Setting(settingsContainer)
.setName(`评分 3: 回答正确, 需要花费很大力气才能回忆起来`)
.addButton(btn => btn
.setButtonText('3')
.onClick(() => this.rateCard(3 as SuperMemoGrade, cardContainer)))
new Setting(settingsContainer)
.setName(`评分 4: 回答正确, 需要经过一番犹豫才做出反应`)
.addButton(btn => btn
.setButtonText('4')
.onClick(() => this.rateCard(4 as SuperMemoGrade, cardContainer)))
new Setting(settingsContainer)
.setName(`评分 5: 回答正确, 完美响应`)
.addButton(btn => btn
.setButtonText('5')
.onClick(() => this.rateCard(5 as SuperMemoGrade, cardContainer)))
this.render(cardContainer);
// 监听数字键 0-5 的按键事件
const handleKeydown = async (event: KeyboardEvent) => {
if (event.key >= '0' && event.key <= '5') {
if (this.isRating) return;
this.isRating = true;
this.currentRatingKey = event.key
const grade = parseInt(event.key) as SuperMemoGrade;
await this.plugin.updateCard(this.currentCard, grade);
}
};
const handleKeyup = async (event: KeyboardEvent) => {
if (
this.isRating &&
this.currentRatingKey !== null &&
event.key === this.currentRatingKey
) {
this.isRating=false
this.currentRatingKey = null
this.pickNextCard()
this.render(cardContainer)
}
};
this.plugin.registerDomEvent(contentEl, 'keydown', handleKeydown);
this.plugin.registerDomEvent(contentEl, 'keyup', handleKeyup);
// 注册数字键和回车按键事件
this.registerRatingKeyEvents(contentEl, cardContainer);
this.registerRenderKeyEvents(cardContainer);
}
pickNextCard() {
@ -104,6 +51,15 @@ export class LearningModal extends Modal {
new Notice("已完成所有新词!");
return;
}
// 新词排序:间隔降序,间隔相同则易记因子升序
pool.sort((a, b) => {
const intervalA = Number(a.interval);
const intervalB = Number(b.interval);
if (intervalA !== intervalB) return intervalB - intervalA; // 降序
const efactorA = Number(a.efactor);
const efactorB = Number(b.efactor);
return efactorA - efactorB; // 升序
});
} else {
pool = Array.from(this.plugin.dueCards.values())
.filter(card => window.moment(card.dueDate).isBefore(now)) // 筛选已过期的单词
@ -112,40 +68,110 @@ export class LearningModal extends Modal {
new Notice("没有需要复习的卡片!");
return;
}
// 旧词排序:易记因子升序,易记因子相同则重复次数升序
pool.sort((a, b) => {
const efactorA = Number(a.efactor);
const efactorB = Number(b.efactor);
if (efactorA !== efactorB) return efactorA - efactorB; // 升序
const repetitionA = Number(a.repetition);
const repetitionB = Number(b.repetition);
return repetitionA - repetitionB; // 升序
});
}
// 单词调度 70% 纯随机, 30% 从易记因子和重复次数最低的 20% 中随机
const randomMode = Math.random() < 0.7;
if (randomMode) {
this.currentCard = pool[Math.floor(Math.random() * pool.length)];
} else {
const sortedPool = pool.sort((a, b) => {
const efactorDiff = (a.efactor || 1) - (b.efactor || 1); // 易记因子升序
if (efactorDiff !== 0) return efactorDiff;
return a.repetition - b.repetition; // 重复次数升序
});
const halfPool = sortedPool.slice(0, Math.ceil(sortedPool.length / 5)); // 取前 20%
this.currentCard = halfPool[Math.floor(Math.random() * halfPool.length)];
// 单词调度 70% 纯随机, 30% 从排序后前 1% 中随机
const randomMode = Math.random() < this.plugin.settings.randomRatio;
if (randomMode) {
this.currentCard = pool[Math.floor(Math.random() * pool.length)];
} else {
const topN = Math.max(1, Math.ceil(pool.length / 100));
const topPool = pool.slice(0, topN);
this.currentCard = topPool[Math.floor(Math.random() * topPool.length)];
}
}
renderSettings(settingsContainer: HTMLElement, cardContainer: HTMLDivElement) {
const grades: { grade: SuperMemoGrade, label: string }[] = [
{ grade: 0, label: '评分 0: 回答错误, 完全不会' },
{ grade: 1, label: '评分 1: 回答错误, 看到正确答案后感觉很熟悉' },
{ grade: 2, label: '评分 2: 回答错误, 看到正确答案后感觉很容易记住' },
{ grade: 3, label: '评分 3: 回答正确, 需要花费很大力气才能回忆起来' },
{ grade: 4, label: '评分 4: 回答正确, 需要经过一番犹豫才做出反应' },
{ grade: 5, label: '评分 5: 回答正确, 完美响应' },
];
for (const { grade, label } of grades) {
new Setting(settingsContainer)
.setName(label)
.addButton(btn => btn
.setButtonText(String(grade))
.onClick(() => this.rateCard(grade, cardContainer)));
}
}
render(cardContainer: HTMLDivElement) {
cardContainer.empty()
// 标记当前是否显示 Markdown 内容
let isShowingMarkdown = false;
// 初始显示单词
renderCard(cardContainer: HTMLDivElement) {
cardContainer.empty();
const wordContent = cardContainer.createDiv({ cls: 'openwords-card-content' });
wordContent.textContent = this.currentCard.front;
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
if (!file) {return;}
const fileCache = this.app.metadataCache.getFileCache(file);
const frontMatter = fileCache?.frontmatter;
if (!frontMatter) {return;}
const tags: string[] = (frontMatter.tags || []).map((tag: string) => {
const parts = tag.split('/');
return parts.length > 1 ? parts[1] : tag;
});
const dueDate: string = frontMatter["到期日"]
const interval: string = frontMatter["间隔"]
const efactor: string = frontMatter["易记因子"]
const repetition: string = frontMatter["重复次数"]
// 添加鼠标悬浮事件切换内容
cardContainer.addEventListener('mouseenter', async () => {
if (isShowingMarkdown) return; // 如果已经显示 Markdown 内容,则不重复加载
const metaDiv = cardContainer.createDiv({ cls: 'openwords-card-meta' });
// 第一行:标签
const tagsDiv = metaDiv.createDiv({ cls: 'openwords-card-meta-tags' });
tagsDiv.textContent = `标签: ${tags.join(', ')}`;
// 第二行:其余元数据
const infoDiv = metaDiv.createDiv({ cls: 'openwords-card-meta-info' });
infoDiv.textContent = `易记因子: ${efactor} | 重复次数: ${repetition} | 到期日: ${dueDate} | 间隔: ${interval} `;
}
registerRatingKeyEvents(contentEl: HTMLElement, cardContainer: HTMLDivElement) {
const handleKeydown = async (event: KeyboardEvent) => {
if (event.key >= '0' && event.key <= '5') {
if (this.isRating) return;
this.isRating = true;
this.currentRatingKey = event.key;
const grade = parseInt(event.key) as SuperMemoGrade;
await this.plugin.updateCard(this.currentCard, grade);
}
};
const handleKeyup = async (event: KeyboardEvent) => {
if (
this.isRating &&
this.currentRatingKey !== null &&
event.key === this.currentRatingKey
) {
this.isRating = false;
this.currentRatingKey = null;
this.pickNextCard();
this.renderCard(cardContainer);
}
};
this.plugin.registerDomEvent(contentEl, 'keydown', handleKeydown);
this.plugin.registerDomEvent(contentEl, 'keyup', handleKeyup);
}
registerRenderKeyEvents(cardContainer: HTMLDivElement) {
// 标记当前是否显示 Markdown 内容
let isShowingMarkdown = false;
const showMarkdown = async () => {
if (isShowingMarkdown) return;
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
if (file instanceof TFile) {
const markdownContent = await this.plugin.app.vault.cachedRead(file);
// 清空卡片内容并渲染 Markdown 内容
cardContainer.empty();
const markdownRenderContainer = cardContainer.createDiv({ cls: 'openwords-card-markdown' });
await MarkdownRenderer.render(
@ -155,24 +181,40 @@ export class LearningModal extends Modal {
file.path,
this.component
);
isShowingMarkdown = true;
} else {
new Notice('无法加载单词的 Markdown 文件!');
}
});
};
cardContainer.addEventListener('mouseleave', () => {
if (!isShowingMarkdown) return; // 如果已经显示单词内容,则不重复加载
// 切换回显示单词
cardContainer.empty();
const wordContent = cardContainer.createDiv({ cls: 'openwords-card-content' });
wordContent.textContent = this.currentCard.front;
const showWord = () => {
if (!isShowingMarkdown) return;
this.renderCard(cardContainer);
isShowingMarkdown = false;
});
};
cardContainer.addEventListener('mouseenter', showMarkdown);
cardContainer.addEventListener('mouseleave', showWord);
// 按下回车显示内容,释放回车显示单词
const handleKeyDown = async (event: KeyboardEvent) => {
if (event.key === 'Tab') {
event.preventDefault();
await showMarkdown();
}
};
const handleKeyUp = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
event.preventDefault();
showWord();
}
};
this.plugin.registerDomEvent(cardContainer, 'keydown', handleKeyDown);
this.plugin.registerDomEvent(cardContainer, 'keyup', handleKeyUp);
// 让卡片容器可聚焦以接收键盘事件
cardContainer.tabIndex = 0;
cardContainer.focus();
}
async rateCard(grade: SuperMemoGrade, cardContainer: HTMLDivElement) {
@ -180,7 +222,7 @@ export class LearningModal extends Modal {
this.isRating = true;
await this.plugin.updateCard(this.currentCard, grade);
this.pickNextCard();
this.render(cardContainer);
this.renderCard(cardContainer);
this.isRating = false;
}

View file

@ -59,7 +59,7 @@ export class LearningTypeModal extends Modal {
}));
new Setting(buttonContainer)
.setName('默写单词 ( 启用单词 )')
.setName('默写单词 ( 重复次数不为零 )')
.addButton(btn => btn
.setButtonText('开始')
.onClick(() => {
@ -68,7 +68,7 @@ export class LearningTypeModal extends Modal {
}));
new Setting(buttonContainer)
.setName(`添加双链 ( 启用单词中易记因子 <= 2.5 )`)
.setName(`添加双链 ( 启用单词中易记因子 <= ${this.plugin.settings.maxEfactorForLink} )`)
.addButton(btn => btn
.setButtonText("开始")
.onClick(async () => {

View file

@ -1,4 +1,4 @@
import { App, Modal, MarkdownRenderer, TFile, Component } from 'obsidian';
import { App, Modal, MarkdownRenderer, TFile, Component, Notice } from 'obsidian';
import { CardInfo } from '../card';
import OpenWords from '../main';
@ -8,6 +8,8 @@ export class SpellingModal extends Modal {
plugin: OpenWords;
component: Component;
currentCard: CardInfo | null = null;
hasPeeked: boolean = false; // 是否看过答案
errorCount: number = 0; // 本轮错误次数
constructor(app: App, plugin: OpenWords) {
super(app);
@ -16,6 +18,7 @@ export class SpellingModal extends Modal {
}
async onOpen() {
// 加载组件
const { contentEl } = this;
contentEl.empty();
this.component.load()
@ -34,6 +37,7 @@ export class SpellingModal extends Modal {
event.preventDefault(); // 阻止页面滚动
if (this.currentCard) {
title.setText(`${this.currentCard.front}`); // 显示答案
this.hasPeeked = true; // 标记已看答案
}
}
};
@ -53,14 +57,19 @@ export class SpellingModal extends Modal {
inputContainer: HTMLElement,
feedbackContainer: HTMLElement
) {
const cards = Array.from(this.plugin.enabledCards.values());
const cards = Array.from(this.plugin.dueCards.values());
if (cards.length === 0) {
feedbackContainer.setText('没有更多单词了!');
return;
}
this.hasPeeked = false;
this.errorCount = 0;
// 随机选择一个单词
this.currentCard = cards[Math.floor(Math.random() * cards.length)];
// 从易记因子最低的50%中随机选择一个单词
const sorted = cards.slice().sort((a, b) => a.efactor - b.efactor);
const half = Math.ceil(sorted.length / 2);
const pool = sorted.slice(0, half);
this.currentCard = pool[Math.floor(Math.random() * pool.length)];
// 渲染单词的词义
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
@ -118,7 +127,7 @@ export class SpellingModal extends Modal {
}
}
checkSpelling(
async checkSpelling(
inputFields: HTMLInputElement[],
word: string,
feedbackContainer: HTMLElement,
@ -129,11 +138,35 @@ export class SpellingModal extends Modal {
if (userInput.length === word.length) {
if (userInput.toLowerCase() === word.toLowerCase()) {
feedbackContainer.setText('正确!');
if (this.currentCard) {
let efactor = this.currentCard.efactor;
if (this.hasPeeked) {
efactor -= 0.02;
} else if (this.errorCount === 0) {
efactor += 0.15;
} else {
efactor += 0.05;
}
if (efactor < 1.3) efactor = 1.3;
this.currentCard.efactor = efactor;
// 同步到 frontmatter
const file = this.plugin.app.vault.getFileByPath(this.currentCard.path);
if (file instanceof TFile) {
await this.plugin.app.fileManager.processFrontMatter(file, (frontMatter) => {
frontMatter["易记因子"] = Math.round(efactor * 100);
});
}
new Notice(`${this.currentCard.front} \n易记因子: ${efactor.toFixed(2)} \n重复次数: ${this.currentCard.repetition} \n间隔: ${this.currentCard.interval} \n到期日: ${this.currentCard.dueDate}`);
}
setTimeout(async () => {
await this.pickNextCard(wordMeaningContainer, inputContainer, feedbackContainer);
}, 500);
} else {
feedbackContainer.setText('错误,请重试!');
this.errorCount += 1;
inputFields.forEach((field) => (field.value = '')); // 清空所有输入框
inputFields[0].focus(); // 聚焦第一个输入框
}

View file

@ -147,6 +147,34 @@ export class OpenWordsSettingTab extends PluginSettingTab {
button.setButtonText('生成'); // 恢复按钮文本
}
}));
new Setting(containerEl)
.setName("随机调度概率")
.setDesc("设置每次抽卡时随机调度的概率,其余为优先调度概率")
.addDropdown(dropdown => {
for (let i = 0; i <= 10; i++) {
const value = (i / 10).toFixed(1);
dropdown.addOption(value, value);
}
dropdown.setValue(this.plugin.settings.randomRatio.toFixed(1));
dropdown.onChange(async (value) => {
this.plugin.settings.randomRatio = Number(value);
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName("最大双链因子")
.setDesc("易记因子小于等于该值的单词可添加双链")
.addDropdown(dropdown => {
for (let i = 0; i < 10; i++) {
const value = (1.2 + i * 0.2).toFixed(1);
dropdown.addOption(value, value);
}
dropdown.setValue(this.plugin.settings.maxEfactorForLink.toFixed(1));
dropdown.onChange(async (value) => {
this.plugin.settings.maxEfactorForLink = Number(value);
await this.plugin.saveSettings();
});
});
new Setting(containerEl)
.setName('重置单词属性')
.setDesc('重置作用域单词的易记因子、重复次数、间隔和到期日')

View file

@ -36,6 +36,7 @@ If your plugin does not need CSS, delete this file.
width: 500px; /* 固定宽度 */
height: 300px; /* 固定高度 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid var(--color-border);
@ -50,9 +51,17 @@ If your plugin does not need CSS, delete this file.
}
.openwords-card-content { /* 单词内容样式 */
font-size: 2.5em; /* 设置单词字体大小 */
font-size: 3em; /* 设置单词字体大小 */
font-weight: bold; /* 设置单词加粗 */
color: var(--text-normal); /* 设置单词颜色 */
margin-top: 24px;
}
.openwords-card-meta {
font-size: 0.85em;
line-height: 1.5;
margin-top: 24px;
}
.openwords-card-markdown { /* Markdown 内容样式 */

View file

@ -1,3 +1,3 @@
{
"1.0.5": "1.8.10"
"1.0.6": "1.8.10"
}