0.4.0 convex及界面修改

This commit is contained in:
liaibo
2026-02-01 08:47:40 +08:00
parent d1f055f51a
commit af92c4b149
636 changed files with 7522 additions and 1815 deletions
+219
View File
@@ -178,6 +178,154 @@ async function purgeDocumentShareRelations(ctx: any, workspaceId: string, docume
}
}
async function purgeDocumentRelatedData(ctx: any, workspaceId: string, documentIds: string[]) {
const uniqueDocIds = Array.from(new Set(documentIds.filter(Boolean)));
if (uniqueDocIds.length === 0) return;
// 1) 思维导图
for (const docId of uniqueDocIds) {
const rows = await ctx.db
.query("mindmaps")
.withIndex("by_doc_mindmap", (q: any) => q.eq("document_id", docId))
.collect();
for (const r of rows) {
await ctx.db.delete(r._id);
}
}
// 2) 附件(含底层 storage 文件)
const assets: any[] = [];
for (const docId of uniqueDocIds) {
const rows = await ctx.db
.query("media_assets")
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
.collect();
assets.push(...rows);
}
if (assets.length) {
const assetIdsToDelete = new Set<string>(assets.map((a) => String(a.id)));
const byStorage = new Map<string, string[]>();
for (const a of assets) {
const sid = (a.storage_id as any) ?? null;
if (!sid) continue;
const list = byStorage.get(String(sid)) ?? [];
list.push(String(a.id));
byStorage.set(String(sid), list);
}
for (const [sid] of byStorage.entries()) {
const refs = await ctx.db
.query("media_assets")
.withIndex("by_storage_id", (q: any) => q.eq("storage_id", sid as any))
.collect();
// 说明:只要仍有“未清理”的引用(包括仍在垃圾桶但可恢复的记录),就不要删底层文件。
const otherAlive = refs.some((r: any) => !assetIdsToDelete.has(String(r.id)) && !r.purged_at);
if (!otherAlive) {
try {
await ctx.storage.delete(sid as any);
} catch {
// ignore
}
}
}
for (const a of assets) {
await ctx.db.delete(a._id);
}
}
// 3) 在线表格(含行数据)
for (const docId of uniqueDocIds) {
const tables = await ctx.db
.query("document_tables")
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
.collect();
for (const table of tables) {
const rows = await ctx.db
.query("document_table_rows")
.withIndex("by_table", (q: any) => q.eq("table_id", table.id))
.collect();
for (const r of rows) {
await ctx.db.delete(r._id);
}
await ctx.db.delete(table._id);
}
}
// 4) 评论
for (const docId of uniqueDocIds) {
const msgs = await ctx.db
.query("comment_messages")
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
.collect();
for (const m of msgs) {
await ctx.db.delete(m._id);
}
const threads = await ctx.db
.query("comment_threads")
.withIndex("by_document", (q: any) => q.eq("document_id", docId))
.collect();
for (const t of threads) {
await ctx.db.delete(t._id);
}
}
// 5) 页面引用/反链(source/target 任一命中即删除)
const deletedRefIds = new Set<string>();
for (const docId of uniqueDocIds) {
const bySource = await ctx.db
.query("page_references")
.withIndex("by_workspace_source", (q: any) => q.eq("workspace_id", workspaceId).eq("source_page_id", docId))
.collect();
for (const r of bySource) {
const id = String(r._id);
if (deletedRefIds.has(id)) continue;
deletedRefIds.add(id);
await ctx.db.delete(r._id);
}
const byTarget = await ctx.db
.query("page_references")
.withIndex("by_workspace_target", (q: any) => q.eq("workspace_id", workspaceId).eq("target_page_id", docId))
.collect();
for (const r of byTarget) {
const id = String(r._id);
if (deletedRefIds.has(id)) continue;
deletedRefIds.add(id);
await ctx.db.delete(r._id);
}
}
// 6) 收藏(避免残留用户数据)
for (const docId of uniqueDocIds) {
const stars = await ctx.db
.query("document_stars")
.withIndex("by_workspace_document_user", (q: any) => q.eq("workspace_id", workspaceId).eq("document_id", docId))
.collect();
for (const s of stars) {
await ctx.db.delete(s._id);
}
}
// 7) 最近访问(避免“最近/历史”里出现幽灵页面)
// 说明:user_recent_pages 当前缺少按 document_id 的索引,这里先全表扫描再过滤。
// 若未来数据量变大,再考虑加 index("by_document", ["document_id"]) 或按 workspace/user 拆分索引。
const recents = await ctx.db.query("user_recent_pages").collect();
if (recents.length) {
const docIdSet = new Set(uniqueDocIds);
for (const r of recents) {
if (!docIdSet.has(String((r as any).document_id ?? ""))) continue;
await ctx.db.delete(r._id);
}
}
// 兜底:如果未来新增了其它 “document_id 外键表”,这里可以继续补充;
// 当前先把最容易产生垃圾、且已出现历史堆积的表(media_assets/mindmaps/表格/评论/引用/收藏)清理掉。
// 同时,避免误删“非 document_id 维度”的用户行为表(如 jobs 等),后续如确认需要可再补齐。
}
export const getMeta = query({
args: { id: v.string() },
handler: async (ctx, args) => {
@@ -228,9 +376,17 @@ export const getMeta = query({
show_structure: doc.show_structure ?? null,
protect_editing: doc.protect_editing ?? null,
show_word_count: doc.show_word_count ?? null,
collapse_backlinks: (doc as any).collapse_backlinks ?? null,
page_font: (doc as any).page_font ?? null,
layout_density: (doc as any).layout_density ?? null,
hide_child_pages: (doc as any).hide_child_pages ?? null,
show_block_ref_count: (doc as any).show_block_ref_count ?? null,
embed_default_block_id: (doc as any).embed_default_block_id ?? null,
word_count: doc.word_count ?? null,
character_count: doc.character_count ?? null,
block_count: doc.block_count ?? null,
todo_total_count: (doc as any).todo_total_count ?? null,
todo_done_count: (doc as any).todo_done_count ?? null,
};
},
});
@@ -674,9 +830,17 @@ export const create = mutation({
show_structure: false,
protect_editing: false,
show_word_count: true,
collapse_backlinks: false,
page_font: "default",
layout_density: "normal",
hide_child_pages: false,
show_block_ref_count: false,
embed_default_block_id: null,
word_count: 0,
character_count: 0,
block_count: 0,
todo_total_count: 0,
todo_done_count: 0,
created_at: ts,
updated_at: ts,
deleted_at: null,
@@ -751,6 +915,32 @@ export const updateTitle = mutation({
},
});
export const setTemplate = mutation({
args: { id: v.string(), isTemplate: v.boolean() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const doc = await ctx.db
.query("documents")
.withIndex("by_document_id", (q) => q.eq("id", args.id))
.first();
if (!doc) throw new Error("页面不存在");
if (doc.deleted_at != null) throw new Error("页面不存在");
await requireWorkspaceMember(ctx, doc.workspace_id, userId);
if (doc.user_id !== userId) {
if (doc.access_scope === "public") throw new Error("无权限");
const perm =
(await resolveSharePermission(ctx, doc, userId)) ?? (await resolveGroupSharePermission(ctx, doc, userId));
if (perm !== "edit") throw new Error("无权限");
}
const ts = nowIso();
await ctx.db.patch(doc._id, { is_template: args.isTemplate, updated_at: ts });
return { ok: true, updated_at: ts };
},
});
export const move = mutation({
args: {
id: v.string(),
@@ -941,6 +1131,7 @@ export const purge = mutation({
const subtree = collectSubtree(owned, doc.id);
// 删除顺序对当前数据模型无强制要求,这里简单逐个删除即可。
await purgeDocumentRelatedData(ctx, doc.workspace_id, subtree.map((d) => d.id));
for (const item of subtree) {
await purgeDocumentShareRelations(ctx, doc.workspace_id, item.id);
await ctx.db.delete(item._id);
@@ -971,6 +1162,7 @@ export const emptyTrashByWorkspace = mutation({
.collect();
const toDelete = docs.filter((d) => d.user_id === userId && d.deleted_at != null);
await purgeDocumentRelatedData(ctx, args.workspaceId, toDelete.map((d) => d.id));
for (const d of toDelete) {
await purgeDocumentShareRelations(ctx, args.workspaceId, d.id);
await ctx.db.delete(d._id);
@@ -991,6 +1183,12 @@ export const updateOptions = mutation({
showStructure: v.optional(v.boolean()),
protectEditing: v.optional(v.boolean()),
showWordCount: v.optional(v.boolean()),
collapseBacklinks: v.optional(v.boolean()),
pageFont: v.optional(v.union(v.literal("default"), v.literal("song"), v.literal("kai"))),
layoutDensity: v.optional(v.union(v.literal("compact"), v.literal("normal"), v.literal("spacious"))),
hideChildPages: v.optional(v.boolean()),
showBlockRefCount: v.optional(v.boolean()),
embedDefaultBlockId: v.optional(v.union(v.string(), v.null())),
}),
},
handler: async (ctx, args) => {
@@ -1019,6 +1217,15 @@ export const updateOptions = mutation({
if (typeof args.options.showStructure === "boolean") patch.show_structure = args.options.showStructure;
if (typeof args.options.protectEditing === "boolean") patch.protect_editing = args.options.protectEditing;
if (typeof args.options.showWordCount === "boolean") patch.show_word_count = args.options.showWordCount;
if (typeof args.options.collapseBacklinks === "boolean") patch.collapse_backlinks = args.options.collapseBacklinks;
if (typeof (args.options as any).pageFont === "string") patch.page_font = (args.options as any).pageFont;
if (typeof (args.options as any).layoutDensity === "string") patch.layout_density = (args.options as any).layoutDensity;
if (typeof (args.options as any).hideChildPages === "boolean") patch.hide_child_pages = (args.options as any).hideChildPages;
if (typeof (args.options as any).showBlockRefCount === "boolean")
patch.show_block_ref_count = (args.options as any).showBlockRefCount;
if (typeof (args.options as any).embedDefaultBlockId === "string" || (args.options as any).embedDefaultBlockId === null) {
patch.embed_default_block_id = (args.options as any).embedDefaultBlockId;
}
if (Object.keys(patch).length === 0) {
throw new Error("缺少可更新的选项");
@@ -1036,6 +1243,8 @@ export const updateStats = mutation({
wordCount: v.number(),
characterCount: v.number(),
blockCount: v.number(),
todoTotal: v.optional(v.number()),
todoDone: v.optional(v.number()),
},
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
@@ -1058,6 +1267,8 @@ export const updateStats = mutation({
word_count: args.wordCount,
character_count: args.characterCount,
block_count: args.blockCount,
todo_total_count: typeof args.todoTotal === "number" ? args.todoTotal : (doc as any).todo_total_count ?? 0,
todo_done_count: typeof args.todoDone === "number" ? args.todoDone : (doc as any).todo_done_count ?? 0,
updated_at: ts,
});
return { ok: true, updated_at: ts };
@@ -1113,9 +1324,17 @@ export const duplicate = mutation({
show_structure: source.show_structure ?? false,
protect_editing: source.protect_editing ?? false,
show_word_count: source.show_word_count ?? true,
collapse_backlinks: (source as any).collapse_backlinks ?? false,
page_font: (source as any).page_font ?? "default",
layout_density: (source as any).layout_density ?? "normal",
hide_child_pages: (source as any).hide_child_pages ?? false,
show_block_ref_count: (source as any).show_block_ref_count ?? false,
embed_default_block_id: (source as any).embed_default_block_id ?? null,
word_count: source.word_count ?? 0,
character_count: source.character_count ?? 0,
block_count: source.block_count ?? 0,
todo_total_count: (source as any).todo_total_count ?? 0,
todo_done_count: (source as any).todo_done_count ?? 0,
created_at: ts,
updated_at: ts,