fix(vocab): 兼容 Swift dictionary.json 老 schema (notes/hitCount)

老 Swift 版 DictionaryEntry 写到 ~/Library/Application Support/OpenLess/dictionary.json 的
字段名是 `notes`(复数)和 `hitCount`,Rust 这边期待 `note`/`hits` → serde decode 失败 →
list_vocab 命令永远返回 Err → 前端 spinner 永久卡死。

修法:
- types.rs DictionaryEntry 加 #[serde(alias = "notes")] / #[serde(alias = "hitCount")] +
  default,兼容老文件 + 新写入。
- Vocab.tsx refresh() 加 try/catch + finally,即使后端报错也清掉加载状态,显示错误提示。
This commit is contained in:
baiqing committed 2026-04-29 17:05:13 +08:00
1 parent 4475a6dc31
commit 7326aa2d7e
2 files changed
+29 -5

No files matched your search

+10
View File
@@ -57,12 +57,22 @@ pub struct DictationSession {
pub struct DictionaryEntry {
pub id: String,
pub phrase: String,
/// Swift `DictionaryEntry.swift` 用的是 `notes`(复数);Rust 用 `note`(单数)。
/// alias 接受老文件 + 自身字段名。
#[serde(default, alias = "notes")]
pub note: Option<String>,
#[serde(default = "default_true")]
pub enabled: bool,
/// Swift 用 `hitCount`,Rust 用 `hits`。alias + default 让老文件不缺字段。
#[serde(default, alias = "hitCount")]
pub hits: u64,
/// Swift 写 ISO8601;Rust 也用 String,直接通过。
#[serde(default)]
pub created_at: String,
}
fn default_true() -> bool { true }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserPreferences {
+19 -5
View File
@@ -11,10 +11,19 @@ export function Vocab() {
const [loading, setLoading] = useState(true);
const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = useState<string | null>(null);
const refresh = async () => {
const data = await listVocab();
setEntries(data);
setLoading(false);
try {
setError(null);
const data = await listVocab();
setEntries(data);
} catch (e) {
// 之前没 try/catch,后端 decode 失败时 spinner 永久卡死。
setError(e instanceof Error ? e.message : String(e));
} finally {
setLoading(false);
}
};
useEffect(() => {
@@ -82,12 +91,17 @@ export function Vocab() {
</div>
<div style={{ padding: 18, display: 'flex', flexWrap: 'wrap', gap: 8, minHeight: 80 }}>
{loading && <div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>加载中…</div>}
{!loading && entries.length === 0 && (
{!loading && error && (
<div style={{ fontSize: 12, color: 'var(--ol-err)', lineHeight: 1.6 }}>
加载失败:{error}
</div>
)}
{!loading && !error && entries.length === 0 && (
<div style={{ fontSize: 12, color: 'var(--ol-ink-4)' }}>
还没有词条。在上面输入一个生词或专业术语,让模型在听写时优先匹配。
</div>
)}
{entries.map(e => (
{!error && entries.map(e => (
<VocabChip key={e.id} entry={e} onRemove={() => onRemove(e.id)} onToggle={() => onToggle(e)} />
))}
</div>