feat(mindspace): add achievements showcase with paginated management
Memind CI / Test, build, and release guards (push) Successful in 3m28s

Ship the M成果 page with list/delete UX, fix deletePage for incomplete page records, route /space/achievements, and add M成果 under the WeChat M空间 menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-12 15:21:42 +08:00
parent e9cecf8733
commit 421e204711
21 changed files with 1405 additions and 29 deletions
+215 -18
View File
@@ -15,10 +15,12 @@ import {
getMindSpacePage,
getMindSpacePageDeletePreview,
ignoreMindSpaceScheduleReminder,
isMindSpacePageNotFoundError,
listMindSpaceAgentJobs,
listMindSpaceAssets,
listMindSpaceCleanupItems,
listMindSpacePages,
listMindSpaceAchievementPages,
retryMindSpaceAgentJob,
runMindSpaceAgentJob,
runMindSpaceCleanup,
@@ -55,6 +57,7 @@ import {
} from '../utils/mindspaceCards';
import { PREVIEW_ASSETS, PREVIEW_PAGES, PREVIEW_SPACE } from '../dev/mindspacePreviewData';
import { MindSpaceFeedCard } from './MindSpaceFeedCard';
import { MindSpaceAchievementsPanel } from './MindSpaceAchievementsPanel';
import { MindSpacePageDetail } from './MindSpacePageDetail';
import { MindSpaceModal } from './MindSpaceModal';
import { MindSpaceDeletePlazaOption } from './MindSpaceDeletePlazaOption';
@@ -83,6 +86,7 @@ const CATEGORY_ACTIONS: Partial<Record<MindSpaceCategory['code'], string>> = {
const AGENT_JOBS_PAGE_SIZE = 10;
const RECENT_PAGES_PAGE_SIZE = 6;
const ALL_PAGES_PAGE_SIZE = 15;
const ACHIEVEMENTS_PAGE_SIZE = 20;
function sortPagesByCreatedAt(pages: MindSpacePage[]) {
return [...pages].sort((left, right) => {
@@ -499,6 +503,7 @@ function canGenerateWithAgent(asset: MindSpaceAsset) {
type MindSpaceRouteSync = {
pushHome: () => void;
pushAchievements: () => void;
pushCategory: (code: MindSpaceSaveCategory | MindSpaceCategory['code']) => void;
pushPage: (pageId: string) => void;
};
@@ -528,6 +533,7 @@ export function MindSpaceView({
previewMode = false,
initialPageId,
initialCategoryCode,
initialAchievementsOpen = false,
onBack,
onLogout,
onOpenFeedback,
@@ -537,6 +543,7 @@ export function MindSpaceView({
previewMode?: boolean;
initialPageId?: string | null;
initialCategoryCode?: MindSpaceSaveCategory | null;
initialAchievementsOpen?: boolean;
onBack: () => void;
onLogout: () => void;
onOpenFeedback?: () => void;
@@ -589,6 +596,13 @@ export function MindSpaceView({
const [allPagesTotal, setAllPagesTotal] = useState(0);
const [allPagesPageIndex, setAllPagesPageIndex] = useState(0);
const [allPagesLoading, setAllPagesLoading] = useState(false);
const [achievementsOpen, setAchievementsOpen] = useState(initialAchievementsOpen);
const [achievementsItems, setAchievementsItems] = useState<MindSpacePage[]>([]);
const [achievementsTotal, setAchievementsTotal] = useState(0);
const [achievementsPageIndex, setAchievementsPageIndex] = useState(0);
const [achievementsLoading, setAchievementsLoading] = useState(false);
const [selectedAchievementPageIds, setSelectedAchievementPageIds] = useState<string[]>([]);
const [bulkDeletingAchievements, setBulkDeletingAchievements] = useState(false);
const [selectedAllPageIds, setSelectedAllPageIds] = useState<string[]>([]);
const [bulkDeletingAllPages, setBulkDeletingAllPages] = useState(false);
const [selectedScheduleReminderIds, setSelectedScheduleReminderIds] = useState<string[]>([]);
@@ -605,7 +619,7 @@ export function MindSpaceView({
const [pageDeleteRemoveFromPlaza, setPageDeleteRemoveFromPlaza] = useState(false);
const [deletingPageId, setDeletingPageId] = useState<string | null>(null);
const [bulkPageDeleteOpen, setBulkPageDeleteOpen] = useState(false);
const [bulkPageDeleteSource, setBulkPageDeleteSource] = useState<'all' | 'draft'>('all');
const [bulkPageDeleteSource, setBulkPageDeleteSource] = useState<'all' | 'draft' | 'achievements'>('all');
const [bulkPageDeletePreviews, setBulkPageDeletePreviews] = useState<
MindSpacePageDeletePreview[] | null
>(null);
@@ -649,11 +663,11 @@ export function MindSpaceView({
}, [selectedPageId]);
useEffect(() => {
if (!allPagesOpen && !selectedPageId) return;
if (!allPagesOpen && !achievementsOpen && !selectedPageId) return;
window.requestAnimationFrame(() => {
window.scrollTo({ top: 0, left: 0 });
});
}, [allPagesOpen, selectedPageId]);
}, [allPagesOpen, achievementsOpen, selectedPageId]);
useEffect(() => {
if (!selectedPageId || !session?.id || previewMode || pageFullscreenPreviewOpen) return;
@@ -802,8 +816,30 @@ export function MindSpaceView({
setPageDeleteTarget(null);
setPageDeletePreview(null);
setPageDeleteRemoveFromPlaza(false);
setSelectedAchievementPageIds((ids) => ids.filter((id) => id !== pageDeleteTarget.id));
setAchievementsItems((items) => items.filter((page) => page.id !== pageDeleteTarget.id));
setAchievementsTotal((total) => Math.max(0, total - 1));
await refreshMindSpaceSnapshot({ quota: result.quota, freedBytes: result.freedBytes });
if (achievementsOpen) {
await loadAchievementsPage(achievementsPageIndex);
}
} catch (err) {
if (isMindSpacePageNotFoundError(err)) {
if (selectedPageId === pageDeleteTarget.id) {
closePage();
}
setPageDeleteTarget(null);
setPageDeletePreview(null);
setPageDeleteRemoveFromPlaza(false);
setSelectedAchievementPageIds((ids) => ids.filter((id) => id !== pageDeleteTarget.id));
setAchievementsItems((items) => items.filter((page) => page.id !== pageDeleteTarget.id));
setAchievementsTotal((total) => Math.max(0, total - 1));
await refreshMindSpaceSnapshot();
if (achievementsOpen) {
await loadAchievementsPage(achievementsPageIndex);
}
return;
}
setError(err instanceof Error ? err.message : '删除页面失败');
} finally {
setDeletingPageId(null);
@@ -959,12 +995,58 @@ export function MindSpaceView({
setSelectedPageId(null);
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAchievementsOpen(false);
setAllPagesOpen(true);
setSelectedAllPageIds([]);
routeSync?.pushHome();
void loadAllPagesPage(0);
};
const loadAchievementsPage = async (pageIndex: number) => {
if (previewMode) {
setAchievementsItems(PREVIEW_PAGES);
setAchievementsTotal(PREVIEW_PAGES.length);
setAchievementsPageIndex(0);
setSelectedAchievementPageIds([]);
return;
}
setAchievementsLoading(true);
setError(null);
try {
const { items, page } = await listMindSpaceAchievementPages({
limit: ACHIEVEMENTS_PAGE_SIZE,
offset: pageIndex * ACHIEVEMENTS_PAGE_SIZE,
includeStats: false,
});
setAchievementsItems(items);
setAchievementsPageIndex(pageIndex);
setAchievementsTotal(page.total ?? items.length);
setSelectedAchievementPageIds([]);
} catch (err) {
setError(err instanceof Error ? err.message : '成果列表加载失败');
} finally {
setAchievementsLoading(false);
}
};
const openAchievementsPanel = () => {
setSelectedCategory(null);
setSelectedPageId(null);
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAllPagesOpen(false);
setAchievementsOpen(true);
setSelectedAchievementPageIds([]);
routeSync?.pushAchievements();
void loadAchievementsPage(0);
};
const closeAchievementsPanel = () => {
setAchievementsOpen(false);
setSelectedAchievementPageIds([]);
routeSync?.pushHome();
};
const refreshAgentJobsSummary = async () => {
if (previewMode) return;
const { page } = await listMindSpaceAgentJobs({ limit: 1, offset: 0 });
@@ -999,6 +1081,7 @@ export function MindSpaceView({
setSelectedPageId(null);
setNewPageOpen(false);
setAllPagesOpen(false);
setAchievementsOpen(false);
setAgentJobsPanelOpen(true);
routeSync?.pushHome();
void loadAgentJobsPage(0);
@@ -1066,6 +1149,9 @@ export function MindSpaceView({
if (allPagesOpen) {
await loadAllPagesPage(allPagesPageIndex);
}
if (achievementsOpen) {
await loadAchievementsPage(achievementsPageIndex);
}
} catch {
// ignore background list refresh errors
}
@@ -1114,7 +1200,7 @@ export function MindSpaceView({
useEffect(() => {
if (previewMode) return;
const refreshSchedule = () => {
if (!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen) {
if (!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen) {
void refreshSpaceQuietly();
}
};
@@ -1130,6 +1216,7 @@ export function MindSpaceView({
newPageOpen,
agentJobsPanelOpen,
allPagesOpen,
achievementsOpen,
]);
useEffect(() => {
@@ -1137,10 +1224,21 @@ export function MindSpaceView({
}, [initialPageId]);
useEffect(() => {
if (!space || !initialCategoryCode || initialCategoryCode === 'draft') return;
if (!space || !initialCategoryCode || initialCategoryCode === 'draft' || initialAchievementsOpen) return;
const category = space.categories.find((item) => item.code === initialCategoryCode);
if (category) void openCategory(category);
}, [space, initialCategoryCode]);
}, [space, initialCategoryCode, initialAchievementsOpen]);
useEffect(() => {
if (!initialAchievementsOpen || previewMode) return;
setSelectedCategory(null);
setSelectedPageId(null);
setNewPageOpen(false);
setAgentJobsPanelOpen(false);
setAllPagesOpen(false);
setAchievementsOpen(true);
void loadAchievementsPage(0);
}, [initialAchievementsOpen, previewMode]);
useEffect(() => {
if (previewMode || !agentJob || !['queued', 'running'].includes(agentJob.status)) return;
@@ -1167,6 +1265,7 @@ export function MindSpaceView({
const openCategory = async (category: MindSpaceCategory) => {
setAgentJobsPanelOpen(false);
setAllPagesOpen(false);
setAchievementsOpen(false);
setSelectedCategory(category);
setSelectedPageId(null);
setNewPageOpen(false);
@@ -1586,13 +1685,13 @@ export function MindSpaceView({
}, [bulkPageDeletePreviews]);
const closeBulkPageDeleteDialog = () => {
if (bulkDeletingAllPages || bulkDeletingDraftPages) return;
if (bulkDeletingAllPages || bulkDeletingDraftPages || bulkDeletingAchievements) return;
setBulkPageDeleteOpen(false);
setBulkPageDeletePreviews(null);
setBulkPageDeleteRemoveFromPlaza(false);
};
const startBulkPageDelete = async (pageIds: string[], source: 'all' | 'draft') => {
const startBulkPageDelete = async (pageIds: string[], source: 'all' | 'draft' | 'achievements') => {
if (pageIds.length === 0) return;
if (previewMode) {
setError('预览模式下无法删除页面');
@@ -1632,7 +1731,11 @@ export function MindSpaceView({
if (!bulkPageDeletePreviews || bulkPageDeletePreviews.length === 0) return;
const deletingIds = bulkPageDeletePreviews.map((preview) => preview.page.id);
const setBulkDeleting =
bulkPageDeleteSource === 'all' ? setBulkDeletingAllPages : setBulkDeletingDraftPages;
bulkPageDeleteSource === 'all'
? setBulkDeletingAllPages
: bulkPageDeleteSource === 'draft'
? setBulkDeletingDraftPages
: setBulkDeletingAchievements;
setBulkDeleting(true);
setError(null);
const failedIds: string[] = [];
@@ -1645,6 +1748,10 @@ export function MindSpaceView({
});
if (selectedPageId === preview.page.id) closePage();
} catch (err) {
if (isMindSpacePageNotFoundError(err)) {
if (selectedPageId === preview.page.id) closePage();
continue;
}
failedIds.push(preview.page.id);
failedNames.push(preview.page.title);
console.error('[MindSpace] delete page failed:', preview.page.id, err);
@@ -1653,8 +1760,10 @@ export function MindSpaceView({
const deletedIds = deletingIds.filter((id) => !failedIds.includes(id));
if (bulkPageDeleteSource === 'all') {
setSelectedAllPageIds((ids) => ids.filter((id) => failedIds.includes(id)));
} else {
} else if (bulkPageDeleteSource === 'draft') {
setSelectedDraftPageIds((ids) => ids.filter((id) => failedIds.includes(id)));
} else {
setSelectedAchievementPageIds((ids) => ids.filter((id) => failedIds.includes(id)));
}
setBulkPageDeleteOpen(false);
setBulkPageDeletePreviews(null);
@@ -1668,6 +1777,12 @@ export function MindSpaceView({
: allPagesPageIndex;
await loadAllPagesPage(nextPageIndex);
}
if (bulkPageDeleteSource === 'achievements' && deletedIds.length > 0) {
setAchievementsItems((items) => items.filter((page) => !deletedIds.includes(page.id)));
setAchievementsTotal((total) => Math.max(0, total - deletedIds.length));
setSelectedAchievementPageIds((ids) => ids.filter((id) => !deletedIds.includes(id)));
await loadAchievementsPage(achievementsPageIndex);
}
if (failedNames.length > 0) {
setError(`有 ${failedNames.length} 个页面删除失败:${failedNames.slice(0, 3).join('、')}`);
}
@@ -1698,6 +1813,45 @@ export function MindSpaceView({
void startBulkPageDelete([...selectedAllPageIds], 'all');
};
const selectedAchievementPageSet = useMemo(
() => new Set(selectedAchievementPageIds),
[selectedAchievementPageIds],
);
const toggleAchievementPageSelected = (pageId: string, checked: boolean) => {
setSelectedAchievementPageIds((ids) =>
checked ? (ids.includes(pageId) ? ids : [...ids, pageId]) : ids.filter((id) => id !== pageId),
);
};
const toggleAchievementDateGroupSelected = (pageIds: string[], checked: boolean) => {
setSelectedAchievementPageIds((ids) => {
if (!checked) {
const remove = new Set(pageIds);
return ids.filter((id) => !remove.has(id));
}
return [...new Set([...ids, ...pageIds])];
});
};
const toggleAllAchievementPagesSelected = () => {
const allSelected =
achievementsItems.length > 0 &&
achievementsItems.every((page) => selectedAchievementPageSet.has(page.id));
if (allSelected) {
const visibleIds = new Set(achievementsItems.map((page) => page.id));
setSelectedAchievementPageIds((ids) => ids.filter((id) => !visibleIds.has(id)));
return;
}
setSelectedAchievementPageIds((ids) => [
...new Set([...ids, ...achievementsItems.map((page) => page.id)]),
]);
};
const removeSelectedAchievementPages = () => {
void startBulkPageDelete([...selectedAchievementPageIds], 'achievements');
};
const allPagesPageCount = Math.max(1, Math.ceil(allPagesTotal / ALL_PAGES_PAGE_SIZE));
const previewAsset = useMemo(
@@ -1926,7 +2080,13 @@ export function MindSpaceView({
);
const showHomeModules =
space && !selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen;
space &&
!selectedCategory &&
!selectedPageId &&
!newPageOpen &&
!agentJobsPanelOpen &&
!allPagesOpen &&
!achievementsOpen;
const rawUpcomingReminders = previewMode ? [] : space?.schedule?.upcomingReminders ?? [];
const scheduleReminders = previewMode
? PREVIEW_SCHEDULE_REMINDERS.map((item) => ({ ...item }))
@@ -2066,7 +2226,7 @@ export function MindSpaceView({
</header>
<main className="mindspace-content">
{!allPagesOpen && (
{!allPagesOpen && !achievementsOpen && (
<section className="mindspace-hero">
<div className="mindspace-hero-left">
<p className="mindspace-eyebrow">MINDSPACE</p>
@@ -2088,6 +2248,7 @@ export function MindSpaceView({
setSelectedCategory(null);
setSelectedPageId(null);
setAllPagesOpen(false);
setAchievementsOpen(false);
setNewPageOpen(true);
}}
>
@@ -2362,7 +2523,7 @@ export function MindSpaceView({
<section className="mindspace-page-reader">
<header className="mindspace-page-reader-bar">
<button type="button" className="mindspace-inline-back" onClick={closePage}>
{allPagesOpen ? '返回全部页面' : '返回空间首页'}
{allPagesOpen ? '返回全部页面' : achievementsOpen ? '返回成果展示' : '返回空间首页'}
</button>
<span>站内浏览</span>
</header>
@@ -2444,6 +2605,29 @@ export function MindSpaceView({
</button>
</div>
</section>
) : achievementsOpen ? (
<MindSpaceAchievementsPanel
items={achievementsItems}
total={achievementsTotal}
loading={achievementsLoading}
pageIndex={achievementsPageIndex}
pageSize={ACHIEVEMENTS_PAGE_SIZE}
selectedIds={selectedAchievementPageIds}
deleting={bulkDeletingAchievements}
deletingPageId={deletingPageId}
onBack={closeAchievementsPanel}
onOpenPage={(page) => void openPageViewInNewTab(page)}
onShowPage={(pageId) => showPage(pageId)}
onTogglePage={toggleAchievementPageSelected}
onToggleDateGroup={toggleAchievementDateGroupSelected}
onToggleAll={toggleAllAchievementPagesSelected}
onClearSelection={() => setSelectedAchievementPageIds([])}
onDeleteSelected={() => void removeSelectedAchievementPages()}
onDeletePage={(page) => void openPageDeleteDialog(page)}
onPrevPage={() => void loadAchievementsPage(achievementsPageIndex - 1)}
onNextPage={() => void loadAchievementsPage(achievementsPageIndex + 1)}
onFirstPage={() => void loadAchievementsPage(0)}
/>
) : allPagesOpen ? (
<section className="mindspace-independent-page mindspace-all-pages">
<div className="mindspace-section-heading">
@@ -3023,6 +3207,17 @@ export function MindSpaceView({
))}
</div>
<div className="mindspace-grid-secondary">
<article className="mindspace-card mindspace-card-compact mindspace-card-achievements">
<div className="mindspace-card-top">
<span className="mindspace-card-code">SHOWCASE</span>
<span>{recentPagesTotal} 项</span>
</div>
<h3>我的成果展示</h3>
<p>按日期查看页面标题、创建/修改时间与访问数据。</p>
<button type="button" onClick={openAchievementsPanel}>
查看成果
</button>
</article>
{space.categories
.filter((category) => category.code === 'draft')
.map((category) => (
@@ -3048,7 +3243,7 @@ export function MindSpaceView({
</section>
)}
{!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && (
{!selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen && !achievementsOpen && (
<section className="mindspace-section mindspace-recent-work">
<div className="mindspace-section-heading">
<div>
@@ -3433,7 +3628,7 @@ export function MindSpaceView({
eyebrow="DELETE PAGES"
className="mindspace-delete-dialog"
disableClose={
bulkPageDeleteLoading || bulkDeletingAllPages || bulkDeletingDraftPages
bulkPageDeleteLoading || bulkDeletingAllPages || bulkDeletingDraftPages || bulkDeletingAchievements
}
>
{bulkPageDeleteLoading && <div className="mindspace-state">正在分析连带内容…</div>}
@@ -3460,7 +3655,7 @@ export function MindSpaceView({
<button
type="button"
onClick={closeBulkPageDeleteDialog}
disabled={bulkDeletingAllPages || bulkDeletingDraftPages}
disabled={bulkDeletingAllPages || bulkDeletingDraftPages || bulkDeletingAchievements}
>
取消
</button>
@@ -3468,9 +3663,11 @@ export function MindSpaceView({
type="button"
className="mindspace-primary"
onClick={() => void confirmBulkPageDelete()}
disabled={bulkDeletingAllPages || bulkDeletingDraftPages}
disabled={bulkDeletingAllPages || bulkDeletingDraftPages || bulkDeletingAchievements}
>
{bulkDeletingAllPages || bulkDeletingDraftPages ? '删除中…' : '确认删除'}
{bulkDeletingAllPages || bulkDeletingDraftPages || bulkDeletingAchievements
? '删除中…'
: '确认删除'}
</button>
</div>
</>