Skip to content

Commit 17ee41d

Browse files
committed
feat(profile): pinned_papers 支持 Zotero itemKey 运行时填充
- UserPaperItem 加 itemKey 字段(可选) - page.tsx 提取所有 itemKey 批量调 /api/user-center/zotero/items, Zotero 拉到的元信息作为主数据源,手填字段作为离线 fallback - EditProfileForm papers 区增加 itemKey 输入框(提示可选),保存条件放宽为 itemKey 或 title 至少有一个 - 完全向后兼容:历史 pinned_papers(只有 title/authors 的)照常渲染 配套后端 commit involutionhell-backend 1fb697f(Zotero API 代理 + Caffeine 1h) V2 全部完成:#1 编辑页 / #2 docs/history 迁 Java / #3 活跃度热力图 / #4 关注系统 / #5 GitHub repos 同步 / #6 保留 analyticsEvent / #7 Zotero itemKey
1 parent 982547c commit 17ee41d

2 files changed

Lines changed: 75 additions & 3 deletions

File tree

app/u/[username]/edit/EditProfileForm.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ interface ProjectItem {
1818
}
1919

2020
interface PaperItem {
21+
/** Zotero group item key;填了之后 profile 页会自动拉 Zotero 元信息填充 */
22+
itemKey: string;
2123
title: string;
2224
authors: string;
2325
year: string;
@@ -77,6 +79,7 @@ function normalize(raw: unknown): Preferences {
7779
: [],
7880
pinned_papers: Array.isArray(p.pinned_papers)
7981
? p.pinned_papers.map((x) => ({
82+
itemKey: x?.itemKey ?? "",
8083
title: x?.title ?? "",
8184
authors: x?.authors ?? "",
8285
year: String(x?.year ?? ""),
@@ -153,7 +156,8 @@ export function EditProfileForm({ targetIdentifier }: Props) {
153156
tagline: prefs.tagline,
154157
links: prefs.links.filter((l) => l.label && l.url),
155158
projects: prefs.projects.filter((p) => p.title),
156-
pinned_papers: prefs.pinned_papers.filter((p) => p.title),
159+
// 有 itemKey 或有 title 的条目保留,至少得有一个标识符
160+
pinned_papers: prefs.pinned_papers.filter((p) => p.itemKey || p.title),
157161
};
158162
try {
159163
const res = await fetch("/api/user-center/preferences", {
@@ -328,6 +332,7 @@ export function EditProfileForm({ targetIdentifier }: Props) {
328332
items={prefs.pinned_papers}
329333
onChange={(items) => setPrefs({ ...prefs, pinned_papers: items })}
330334
empty={{
335+
itemKey: "",
331336
title: "",
332337
authors: "",
333338
year: "",
@@ -337,6 +342,13 @@ export function EditProfileForm({ targetIdentifier }: Props) {
337342
maxItems={8}
338343
render={(item, idx, update) => (
339344
<div className="flex flex-col gap-2">
345+
<input
346+
type="text"
347+
value={item.itemKey}
348+
onChange={(e) => update({ ...item, itemKey: e.target.value })}
349+
placeholder="Zotero itemKey(可选,填了会自动拉元信息;不填就手填下面字段)"
350+
className="border border-dashed border-[var(--foreground)] bg-[var(--background)] px-3 py-2 font-mono text-xs uppercase tracking-wider"
351+
/>
340352
<input
341353
type="text"
342354
value={item.title}

app/u/[username]/page.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,54 @@ interface UserProjectItem {
2828
}
2929

3030
interface UserPaperItem {
31-
title: string;
31+
/** Zotero group item key;有值时优先从后端 /api/user-center/zotero/items 拉元信息 */
32+
itemKey?: string;
33+
title?: string;
3234
authors?: string;
3335
year?: string | number;
3436
url?: string;
3537
abstract?: string;
3638
}
3739

40+
interface ZoteroItemDto {
41+
itemKey: string;
42+
title: string;
43+
authors: string;
44+
year: string;
45+
url: string;
46+
abstractNote: string;
47+
publicationTitle: string;
48+
}
49+
50+
/**
51+
* 批量调后端 Zotero 代理,返回 itemKey → 元信息映射。
52+
* 任意失败或没 itemKey 时返回空 map,调用方走手填 fallback。
53+
*/
54+
async function fetchZoteroByKeys(
55+
keys: string[],
56+
): Promise<Record<string, ZoteroItemDto>> {
57+
if (keys.length === 0) return {};
58+
const backendUrl = process.env.BACKEND_URL;
59+
if (!backendUrl) return {};
60+
try {
61+
const res = await fetch(
62+
`${backendUrl}/api/user-center/zotero/items?keys=${encodeURIComponent(keys.join(","))}`,
63+
{ next: { revalidate: 3600 } },
64+
);
65+
if (!res.ok) return {};
66+
const json = (await res.json()) as {
67+
success: boolean;
68+
data?: ZoteroItemDto[];
69+
};
70+
if (!json.success || !Array.isArray(json.data)) return {};
71+
const map: Record<string, ZoteroItemDto> = {};
72+
for (const it of json.data) map[it.itemKey] = it;
73+
return map;
74+
} catch {
75+
return {};
76+
}
77+
}
78+
3879
interface UserLinkItem {
3980
label: string;
4081
url: string;
@@ -139,9 +180,28 @@ export default async function UserProfilePage({ params }: Param) {
139180
user.githubId,
140181
);
141182
const projects = preferences.projects ?? [];
142-
const papers = preferences.pinned_papers ?? [];
183+
const rawPapers = preferences.pinned_papers ?? [];
143184
const links = preferences.links ?? [];
144185

186+
// 用 itemKey 批量拉 Zotero 元信息 → 用它填字段,覆盖手填值(手填作为离线 fallback)
187+
const zoteroKeys = rawPapers
188+
.map((p) => p.itemKey)
189+
.filter((k): k is string => typeof k === "string" && k.length > 0);
190+
const zoteroMap = await fetchZoteroByKeys(zoteroKeys);
191+
const papers: UserPaperItem[] = rawPapers.map((p) => {
192+
if (!p.itemKey) return p;
193+
const z = zoteroMap[p.itemKey];
194+
if (!z) return p; // Zotero 拉不到就用手填值兜底
195+
return {
196+
itemKey: p.itemKey,
197+
title: p.title || z.title,
198+
authors: p.authors || z.authors,
199+
year: p.year || z.year,
200+
url: p.url || z.url,
201+
abstract: p.abstract || z.abstractNote,
202+
};
203+
});
204+
145205
return (
146206
<>
147207
<Header />

0 commit comments

Comments
 (0)