feat(mindspace): 全部页面管理、级联删除与页面同步去重
支持全部页面分页/多选/删除/分享,删除时可选移除广场帖并清理工作区附件;修复并发同步重复创建页面,并补齐预览下载链接重写与 iframe 下载权限。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -73,7 +73,7 @@ export function ChatSharePreviewModal({
|
||||
<button type="button" className="chat-share-preview-close" onClick={onClose} aria-label="关闭">✕</button>
|
||||
</div>
|
||||
<div className="chat-share-preview-body">
|
||||
<iframe src={previewUrl} title="页面预览" sandbox="allow-same-origin allow-scripts allow-popups" className="chat-share-preview-frame" />
|
||||
<iframe src={previewUrl} title="页面预览" sandbox="allow-same-origin allow-scripts allow-popups allow-downloads" className="chat-share-preview-frame" />
|
||||
|
||||
<div className="chat-share-side-actions">
|
||||
{phase.kind === 'done' ? (
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { resolvePlazaPostUrl } from '../utils/publicUrl';
|
||||
import type { MindSpacePagePlazaPostPreview } from '../types';
|
||||
|
||||
function plazaPostStatusLabel(status: string): string {
|
||||
if (status === 'published') return '已公开';
|
||||
if (status === 'pending_review') return '待审核';
|
||||
if (status === 'rejected') return '已拒绝';
|
||||
return status;
|
||||
}
|
||||
|
||||
export function MindSpaceDeletePlazaOption({
|
||||
plazaPosts,
|
||||
removeFromPlaza,
|
||||
onRemoveFromPlazaChange,
|
||||
}: {
|
||||
plazaPosts: MindSpacePagePlazaPostPreview[];
|
||||
removeFromPlaza: boolean;
|
||||
onRemoveFromPlazaChange: (value: boolean) => void;
|
||||
}) {
|
||||
if (plazaPosts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mindspace-delete-plaza">
|
||||
<p className="mindspace-upload-dialog-desc">
|
||||
{plazaPosts.length === 1
|
||||
? '该页面已发布到广场。'
|
||||
: `该页面在广场有 ${plazaPosts.length} 个帖子。`}
|
||||
</p>
|
||||
<ul className="mindspace-delete-plaza-posts">
|
||||
{plazaPosts.map((post) => (
|
||||
<li key={post.id}>
|
||||
<strong>{post.title || '未命名帖子'}</strong>
|
||||
<span>{plazaPostStatusLabel(post.status)}</span>
|
||||
<a href={resolvePlazaPostUrl(post.id)} target="_blank" rel="noreferrer">
|
||||
查看广场帖
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<label className="mindspace-checkbox-row mindspace-delete-plaza-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={removeFromPlaza}
|
||||
onChange={(event) => onRemoveFromPlazaChange(event.target.checked)}
|
||||
/>
|
||||
同时从广场移除
|
||||
</label>
|
||||
<small className="mindspace-delete-plaza-hint">
|
||||
若不勾选,广场帖子将继续保留;页面与公开链接仍会删除。
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MindSpacePage } from '../types';
|
||||
import { WorkCover } from './WorkCover';
|
||||
import { WorkCover, type WorkCoverMode } from './WorkCover';
|
||||
|
||||
const ACCESS_MODE_ICONS: Record<string, { icon: string; title: string }> = {
|
||||
public: { icon: '🌐', title: '完全公开' },
|
||||
@@ -56,6 +56,11 @@ export function MindSpaceFeedCard({
|
||||
actions,
|
||||
checked,
|
||||
onCheckChange,
|
||||
checkboxPlacement = 'top-left',
|
||||
coverMode = 'thumbnail-first',
|
||||
onOpenPage,
|
||||
onEditPage,
|
||||
onShare,
|
||||
}: {
|
||||
page: MindSpacePage;
|
||||
previewMode?: boolean;
|
||||
@@ -63,10 +68,16 @@ export function MindSpaceFeedCard({
|
||||
actions?: { label: string; onClick: (e: React.MouseEvent) => void }[];
|
||||
checked?: boolean;
|
||||
onCheckChange?: (checked: boolean) => void;
|
||||
checkboxPlacement?: 'top-left' | 'top-right';
|
||||
coverMode?: WorkCoverMode;
|
||||
onOpenPage?: () => void;
|
||||
onEditPage?: () => void;
|
||||
onShare?: () => void;
|
||||
}) {
|
||||
const typeLabel = feedTypeLabel(page);
|
||||
const isHtml = page.contentFormat === 'html';
|
||||
const previewThumbnail = previewMode ? PREVIEW_THUMBNAILS[page.id] : null;
|
||||
const showLinkActions = Boolean(onOpenPage || onEditPage || onShare);
|
||||
|
||||
return (
|
||||
<div className="mindspace-feed-card-wrap">
|
||||
@@ -74,7 +85,12 @@ export function MindSpaceFeedCard({
|
||||
<div className="mindspace-feed-cover">
|
||||
{onCheckChange !== undefined && (
|
||||
<label
|
||||
className="mindspace-feed-select"
|
||||
className={[
|
||||
'mindspace-feed-select',
|
||||
checkboxPlacement === 'top-right' ? 'is-top-right is-compact' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
@@ -87,7 +103,7 @@ export function MindSpaceFeedCard({
|
||||
)}
|
||||
{isHtml ? (
|
||||
<WorkCover
|
||||
coverMode="thumbnail-first"
|
||||
coverMode={coverMode}
|
||||
thumbnailUrl={
|
||||
previewThumbnail ??
|
||||
`/api/mindspace/v1/pages/${page.id}/thumbnail?v=${page.updatedAt}`
|
||||
@@ -121,6 +137,46 @@ export function MindSpaceFeedCard({
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{showLinkActions && (
|
||||
<div className="mindspace-feed-card-links">
|
||||
{onOpenPage && (
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-feed-card-link"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onOpenPage();
|
||||
}}
|
||||
>
|
||||
打开页面
|
||||
</button>
|
||||
)}
|
||||
{onEditPage && (
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-feed-card-link"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onEditPage();
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
{onShare && (
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-feed-card-link"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onShare();
|
||||
}}
|
||||
>
|
||||
分享
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{actions && actions.length > 0 && (
|
||||
<div className="mindspace-feed-card-actions">
|
||||
{actions.map((action) => (
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from './MindSpacePageFullscreenPreview';
|
||||
import { MindSpacePagePreviewPanel } from './MindSpacePagePreviewPanel';
|
||||
import { MindSpaceModal } from './MindSpaceModal';
|
||||
import { MindSpaceDeletePlazaOption } from './MindSpaceDeletePlazaOption';
|
||||
import { MindSpacePublishSuccess } from './MindSpacePublishSuccess';
|
||||
import { ShareSheet } from './ShareSheet';
|
||||
import { buildPageSharePayload, type SharePayload } from '../utils/shareChannels';
|
||||
@@ -269,6 +270,7 @@ export function MindSpacePageDetail({
|
||||
const [deletePreviewOpen, setDeletePreviewOpen] = useState(false);
|
||||
const [deletePreview, setDeletePreview] = useState<MindSpacePageDeletePreview | null>(null);
|
||||
const [deletePreviewLoading, setDeletePreviewLoading] = useState(false);
|
||||
const [deleteRemoveFromPlaza, setDeleteRemoveFromPlaza] = useState(false);
|
||||
const [sharePayload, setSharePayload] = useState<SharePayload | null>(null);
|
||||
const [previewFrame, setPreviewFrame] = useState<HTMLIFrameElement | null>(null);
|
||||
const [isExportingImage, setIsExportingImage] = useState(false);
|
||||
@@ -532,6 +534,7 @@ export function MindSpacePageDetail({
|
||||
setDeletePreviewOpen(true);
|
||||
setDeletePreviewLoading(true);
|
||||
setDeletePreview(null);
|
||||
setDeleteRemoveFromPlaza(false);
|
||||
setError(null);
|
||||
try {
|
||||
setDeletePreview(await getMindSpacePageDeletePreview(page.id));
|
||||
@@ -547,6 +550,7 @@ export function MindSpacePageDetail({
|
||||
if (deletingPage) return;
|
||||
setDeletePreviewOpen(false);
|
||||
setDeletePreview(null);
|
||||
setDeleteRemoveFromPlaza(false);
|
||||
};
|
||||
|
||||
const confirmDeletePage = async () => {
|
||||
@@ -554,9 +558,12 @@ export function MindSpacePageDetail({
|
||||
setDeletingPage(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await deleteMindSpacePage(page.id);
|
||||
const result = await deleteMindSpacePage(page.id, {
|
||||
removeFromPlaza: deleteRemoveFromPlaza,
|
||||
});
|
||||
setDeletePreviewOpen(false);
|
||||
setDeletePreview(null);
|
||||
setDeleteRemoveFromPlaza(false);
|
||||
await onSaved({ quota: result.quota, freedBytes: result.freedBytes });
|
||||
onBack();
|
||||
} catch (err) {
|
||||
@@ -950,6 +957,11 @@ export function MindSpacePageDetail({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<MindSpaceDeletePlazaOption
|
||||
plazaPosts={deletePreview.plazaPosts ?? []}
|
||||
removeFromPlaza={deleteRemoveFromPlaza}
|
||||
onRemoveFromPlazaChange={setDeleteRemoveFromPlaza}
|
||||
/>
|
||||
<div className="mindspace-page-editor-actions">
|
||||
<button type="button" onClick={closeDeleteDialog} disabled={deletingPage}>
|
||||
取消
|
||||
|
||||
@@ -129,7 +129,7 @@ export function MindSpacePageDraftPreviewFrame({
|
||||
ref={iframeRef}
|
||||
title={`${title || '页面'} 预览`}
|
||||
srcDoc={srcdoc}
|
||||
sandbox="allow-same-origin"
|
||||
sandbox="allow-same-origin allow-downloads"
|
||||
className={className}
|
||||
style={{ height: `${height}px`, visibility: srcdoc ? 'visible' : 'hidden' }}
|
||||
/>
|
||||
|
||||
@@ -130,7 +130,7 @@ export function MindSpacePageEditablePreviewFrame({
|
||||
ref={iframeRef}
|
||||
title={`${title || '页面'} 预览`}
|
||||
srcDoc={srcdoc}
|
||||
sandbox="allow-scripts"
|
||||
sandbox="allow-scripts allow-downloads"
|
||||
scrolling={scrollInsideFrame ? 'yes' : 'no'}
|
||||
className={frameClassName}
|
||||
style={scrollInsideFrame ? { height: '100%' } : { height: `${height}px` }}
|
||||
|
||||
@@ -53,10 +53,11 @@ import { PREVIEW_ASSETS, PREVIEW_PAGES, PREVIEW_SPACE } from '../dev/mindspacePr
|
||||
import { MindSpaceFeedCard } from './MindSpaceFeedCard';
|
||||
import { MindSpacePageDetail } from './MindSpacePageDetail';
|
||||
import { MindSpaceModal } from './MindSpaceModal';
|
||||
import { MindSpaceDeletePlazaOption } from './MindSpaceDeletePlazaOption';
|
||||
import { ShareSheet } from './ShareSheet';
|
||||
import { WorkCover } from './WorkCover';
|
||||
import { buildAssetSharePayload, type SharePayload } from '../utils/shareChannels';
|
||||
import { resolvePublicPageUrl, resolvePlazaHomeUrl } from '../utils/publicUrl';
|
||||
import { buildAssetSharePayload, buildPageSharePayload, type SharePayload } from '../utils/shareChannels';
|
||||
import { resolveMindSpaceHomeUrl, resolveMindSpacePageOwnerPreviewUrl, resolveMindSpacePagePublicViewUrl, resolvePublicPageUrl, resolvePlazaHomeUrl } from '../utils/publicUrl';
|
||||
import { PagePreviewFrame } from './PagePreviewFrame';
|
||||
import { BalanceRing } from './BalanceRing';
|
||||
import { NotificationCenter } from './NotificationCenter';
|
||||
@@ -77,6 +78,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;
|
||||
|
||||
function sortPagesByCreatedAt(pages: MindSpacePage[]) {
|
||||
return [...pages].sort((left, right) => {
|
||||
@@ -526,6 +528,13 @@ export function MindSpaceView({
|
||||
const [agentJobsPage, setAgentJobsPage] = useState(0);
|
||||
const [agentJobsLoading, setAgentJobsLoading] = useState(false);
|
||||
const [allPagesOpen, setAllPagesOpen] = useState(false);
|
||||
const [allPagesItems, setAllPagesItems] = useState<MindSpacePage[]>([]);
|
||||
const [allPagesTotal, setAllPagesTotal] = useState(0);
|
||||
const [allPagesPageIndex, setAllPagesPageIndex] = useState(0);
|
||||
const [allPagesLoading, setAllPagesLoading] = useState(false);
|
||||
const [selectedAllPageIds, setSelectedAllPageIds] = useState<string[]>([]);
|
||||
const [bulkDeletingAllPages, setBulkDeletingAllPages] = useState(false);
|
||||
const [recentPagesTotal, setRecentPagesTotal] = useState(0);
|
||||
const [exportingPageId, setExportingPageId] = useState<string | null>(null);
|
||||
const [sharePayload, setSharePayload] = useState<SharePayload | null>(null);
|
||||
const [previewPageTarget, setPreviewPageTarget] = useState<MindSpacePage | null>(null);
|
||||
@@ -534,7 +543,15 @@ export function MindSpaceView({
|
||||
null,
|
||||
);
|
||||
const [pageDeleteLoading, setPageDeleteLoading] = useState(false);
|
||||
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 [bulkPageDeletePreviews, setBulkPageDeletePreviews] = useState<
|
||||
MindSpacePageDeletePreview[] | null
|
||||
>(null);
|
||||
const [bulkPageDeleteLoading, setBulkPageDeleteLoading] = useState(false);
|
||||
const [bulkPageDeleteRemoveFromPlaza, setBulkPageDeleteRemoveFromPlaza] = useState(false);
|
||||
const [assetDeleteDialog, setAssetDeleteDialog] = useState<{
|
||||
message: string;
|
||||
references: Array<{ id: string; title: string; publishedOnline?: boolean }>;
|
||||
@@ -632,6 +649,41 @@ export function MindSpaceView({
|
||||
return `${value.slice(0, max - 1)}…`;
|
||||
};
|
||||
|
||||
const resolvePagePublicViewUrl = async (page: MindSpacePage): Promise<string | null> => {
|
||||
const fromList = resolveMindSpacePagePublicViewUrl(page);
|
||||
if (fromList) return fromList;
|
||||
try {
|
||||
const pageDetail = await getMindSpacePage(page.id);
|
||||
const publicUrl = pageDetail.publication?.publicUrl ?? pageDetail.publicationUrl;
|
||||
return publicUrl ? resolvePublicPageUrl(publicUrl) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const openPageViewInNewTab = async (page: MindSpacePage) => {
|
||||
if (previewMode) {
|
||||
setError('预览模式下暂不支持打开页面');
|
||||
return;
|
||||
}
|
||||
const publicUrl = await resolvePagePublicViewUrl(page);
|
||||
const viewUrl = publicUrl ?? resolveMindSpacePageOwnerPreviewUrl(page);
|
||||
window.open(viewUrl, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
const openPageShare = async (page: MindSpacePage) => {
|
||||
if (previewMode) {
|
||||
setError('预览模式下暂不支持分享');
|
||||
return;
|
||||
}
|
||||
const publicUrl = await resolvePagePublicViewUrl(page);
|
||||
if (!publicUrl) {
|
||||
setError('该页面尚未对外发布,暂无法分享链接;你可以先打开预览,或在编辑页完成发布后再分享');
|
||||
return;
|
||||
}
|
||||
setSharePayload(buildPageSharePayload(page, publicUrl));
|
||||
};
|
||||
|
||||
const showPage = (pageId: string, autoPlaza = false) => {
|
||||
setSelectedPageId(pageId);
|
||||
setSelectedPageAutoPlaza(autoPlaza);
|
||||
@@ -649,6 +701,7 @@ export function MindSpaceView({
|
||||
}
|
||||
setPageDeleteTarget(page);
|
||||
setPageDeletePreview(null);
|
||||
setPageDeleteRemoveFromPlaza(false);
|
||||
setPageDeleteLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -665,6 +718,7 @@ export function MindSpaceView({
|
||||
if (deletingPageId) return;
|
||||
setPageDeleteTarget(null);
|
||||
setPageDeletePreview(null);
|
||||
setPageDeleteRemoveFromPlaza(false);
|
||||
};
|
||||
|
||||
const confirmDeletePage = async () => {
|
||||
@@ -672,12 +726,15 @@ export function MindSpaceView({
|
||||
setDeletingPageId(pageDeleteTarget.id);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await deleteMindSpacePage(pageDeleteTarget.id);
|
||||
const result = await deleteMindSpacePage(pageDeleteTarget.id, {
|
||||
removeFromPlaza: pageDeleteRemoveFromPlaza,
|
||||
});
|
||||
if (selectedPageId === pageDeleteTarget.id) {
|
||||
closePage();
|
||||
}
|
||||
setPageDeleteTarget(null);
|
||||
setPageDeletePreview(null);
|
||||
setPageDeleteRemoveFromPlaza(false);
|
||||
await refreshMindSpaceSnapshot({ quota: result.quota, freedBytes: result.freedBytes });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '删除页面失败');
|
||||
@@ -734,24 +791,7 @@ export function MindSpaceView({
|
||||
};
|
||||
|
||||
const openPagePublicShare = async (page: MindSpacePage) => {
|
||||
if (previewMode) {
|
||||
setError('预览模式下暂不支持跳转分享页');
|
||||
return;
|
||||
}
|
||||
if (page.publicationUrl) {
|
||||
window.open(resolvePublicPageUrl(page.publicationUrl), '_blank', 'noopener,noreferrer');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const pageDetail = await getMindSpacePage(page.id);
|
||||
if (!pageDetail.publication?.publicUrl) {
|
||||
setError('该页面未发布,先发布后再分享');
|
||||
return;
|
||||
}
|
||||
window.open(resolvePublicPageUrl(pageDetail.publication.publicUrl), '_blank', 'noopener,noreferrer');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取分享链接失败');
|
||||
}
|
||||
await openPageViewInNewTab(page);
|
||||
};
|
||||
|
||||
const exportPageLongImage = async (page: MindSpacePage) => {
|
||||
@@ -823,6 +863,41 @@ export function MindSpaceView({
|
||||
}
|
||||
};
|
||||
|
||||
const loadAllPagesPage = async (pageIndex: number) => {
|
||||
if (previewMode) {
|
||||
setAllPagesItems(PREVIEW_PAGES);
|
||||
setAllPagesTotal(PREVIEW_PAGES.length);
|
||||
setAllPagesPageIndex(0);
|
||||
return;
|
||||
}
|
||||
setAllPagesLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { items, page } = await listMindSpacePages({
|
||||
limit: ALL_PAGES_PAGE_SIZE,
|
||||
offset: pageIndex * ALL_PAGES_PAGE_SIZE,
|
||||
});
|
||||
setAllPagesItems(items);
|
||||
setAllPagesPageIndex(pageIndex);
|
||||
setAllPagesTotal(page.total ?? items.length);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '全部页面加载失败');
|
||||
} finally {
|
||||
setAllPagesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openAllPagesPanel = () => {
|
||||
setSelectedCategory(null);
|
||||
setSelectedPageId(null);
|
||||
setNewPageOpen(false);
|
||||
setAgentJobsPanelOpen(false);
|
||||
setAllPagesOpen(true);
|
||||
setSelectedAllPageIds([]);
|
||||
routeSync?.pushHome();
|
||||
void loadAllPagesPage(0);
|
||||
};
|
||||
|
||||
const refreshAgentJobsSummary = async () => {
|
||||
if (previewMode) return;
|
||||
const { page } = await listMindSpaceAgentJobs({ limit: 1, offset: 0 });
|
||||
@@ -882,12 +957,13 @@ export function MindSpaceView({
|
||||
setPages(PREVIEW_PAGES);
|
||||
return;
|
||||
}
|
||||
const [nextSpace, nextPages] = await Promise.all([
|
||||
const [nextSpace, recentPagesResult] = await Promise.all([
|
||||
getMindSpace(),
|
||||
listMindSpacePages(),
|
||||
listMindSpacePages({ limit: RECENT_PAGES_PAGE_SIZE, offset: 0 }),
|
||||
]);
|
||||
setSpace(nextSpace);
|
||||
setPages(nextPages);
|
||||
setPages(recentPagesResult.items);
|
||||
setRecentPagesTotal(recentPagesResult.page.total ?? recentPagesResult.items.length);
|
||||
await refreshAgentJobsSummary();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '空间加载失败');
|
||||
@@ -910,8 +986,15 @@ export function MindSpaceView({
|
||||
const refreshPagesSilently = async () => {
|
||||
if (previewMode) return;
|
||||
try {
|
||||
const nextPages = await listMindSpacePages();
|
||||
setPages(nextPages);
|
||||
const recentPagesResult = await listMindSpacePages({
|
||||
limit: RECENT_PAGES_PAGE_SIZE,
|
||||
offset: 0,
|
||||
});
|
||||
setPages(recentPagesResult.items);
|
||||
setRecentPagesTotal(recentPagesResult.page.total ?? recentPagesResult.items.length);
|
||||
if (allPagesOpen) {
|
||||
await loadAllPagesPage(allPagesPageIndex);
|
||||
}
|
||||
} catch {
|
||||
// ignore background list refresh errors
|
||||
}
|
||||
@@ -1009,6 +1092,13 @@ export function MindSpaceView({
|
||||
return;
|
||||
}
|
||||
setAssets(await listMindSpaceAssets(category.code));
|
||||
if (['draft', 'oa', 'public'].includes(category.code)) {
|
||||
const { items } = await listMindSpacePages({ categoryCode: category.code, limit: 100 });
|
||||
setPages(items);
|
||||
if (category.code === 'draft') {
|
||||
setSelectedDraftPageIds([]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '资产加载失败');
|
||||
} finally {
|
||||
@@ -1389,37 +1479,135 @@ export function MindSpaceView({
|
||||
}
|
||||
};
|
||||
|
||||
const removeDraftPages = async () => {
|
||||
if (selectedDraftPageIds.length === 0) return;
|
||||
const bulkDeletePlazaPosts = useMemo(() => {
|
||||
if (!bulkPageDeletePreviews) return [];
|
||||
const seen = new Set<string>();
|
||||
const posts = [];
|
||||
for (const preview of bulkPageDeletePreviews) {
|
||||
for (const post of preview.plazaPosts ?? []) {
|
||||
if (seen.has(post.id)) continue;
|
||||
seen.add(post.id);
|
||||
posts.push(post);
|
||||
}
|
||||
}
|
||||
return posts;
|
||||
}, [bulkPageDeletePreviews]);
|
||||
|
||||
const closeBulkPageDeleteDialog = () => {
|
||||
if (bulkDeletingAllPages || bulkDeletingDraftPages) return;
|
||||
setBulkPageDeleteOpen(false);
|
||||
setBulkPageDeletePreviews(null);
|
||||
setBulkPageDeleteRemoveFromPlaza(false);
|
||||
};
|
||||
|
||||
const startBulkPageDelete = async (pageIds: string[], source: 'all' | 'draft') => {
|
||||
if (pageIds.length === 0) return;
|
||||
if (previewMode) {
|
||||
setError('预览模式下无法删除页面');
|
||||
return;
|
||||
}
|
||||
setBulkDeletingDraftPages(true);
|
||||
setBulkPageDeleteSource(source);
|
||||
setBulkPageDeletePreviews(null);
|
||||
setBulkPageDeleteRemoveFromPlaza(false);
|
||||
setBulkPageDeleteLoading(true);
|
||||
setBulkPageDeleteOpen(true);
|
||||
setError(null);
|
||||
try {
|
||||
const previews = await Promise.all(
|
||||
pageIds.map((pageId) =>
|
||||
getMindSpacePageDeletePreview(pageId).catch((err) => {
|
||||
console.error('[MindSpace] delete preview failed:', pageId, err);
|
||||
return null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
const valid = previews.filter(Boolean) as MindSpacePageDeletePreview[];
|
||||
if (valid.length === 0) {
|
||||
setBulkPageDeleteOpen(false);
|
||||
setError('无法加载删除预览');
|
||||
return;
|
||||
}
|
||||
setBulkPageDeletePreviews(valid);
|
||||
} catch (err) {
|
||||
setBulkPageDeleteOpen(false);
|
||||
setError(err instanceof Error ? err.message : '无法加载删除预览');
|
||||
} finally {
|
||||
setBulkPageDeleteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBulkPageDelete = async () => {
|
||||
if (!bulkPageDeletePreviews || bulkPageDeletePreviews.length === 0) return;
|
||||
const deletingIds = bulkPageDeletePreviews.map((preview) => preview.page.id);
|
||||
const setBulkDeleting =
|
||||
bulkPageDeleteSource === 'all' ? setBulkDeletingAllPages : setBulkDeletingDraftPages;
|
||||
setBulkDeleting(true);
|
||||
setError(null);
|
||||
const deletingIds = [...selectedDraftPageIds];
|
||||
const failedIds: string[] = [];
|
||||
const failedNames: string[] = [];
|
||||
try {
|
||||
for (const pageId of deletingIds) {
|
||||
for (const preview of bulkPageDeletePreviews) {
|
||||
try {
|
||||
await deleteMindSpacePage(pageId);
|
||||
if (selectedPageId === pageId) closePage();
|
||||
} catch {
|
||||
failedIds.push(pageId);
|
||||
failedNames.push(pages.find((p) => p.id === pageId)?.title ?? pageId);
|
||||
await deleteMindSpacePage(preview.page.id, {
|
||||
removeFromPlaza: bulkPageDeleteRemoveFromPlaza,
|
||||
});
|
||||
if (selectedPageId === preview.page.id) closePage();
|
||||
} catch (err) {
|
||||
failedIds.push(preview.page.id);
|
||||
failedNames.push(preview.page.title);
|
||||
console.error('[MindSpace] delete page failed:', preview.page.id, err);
|
||||
}
|
||||
}
|
||||
setSelectedDraftPageIds((ids) => ids.filter((id) => failedIds.includes(id)));
|
||||
const deletedIds = deletingIds.filter((id) => !failedIds.includes(id));
|
||||
if (bulkPageDeleteSource === 'all') {
|
||||
setSelectedAllPageIds((ids) => ids.filter((id) => failedIds.includes(id)));
|
||||
} else {
|
||||
setSelectedDraftPageIds((ids) => ids.filter((id) => failedIds.includes(id)));
|
||||
}
|
||||
setBulkPageDeleteOpen(false);
|
||||
setBulkPageDeletePreviews(null);
|
||||
setBulkPageDeleteRemoveFromPlaza(false);
|
||||
await refreshMindSpaceSnapshot();
|
||||
await load();
|
||||
if (bulkPageDeleteSource === 'all' && deletedIds.length > 0) {
|
||||
const nextPageIndex =
|
||||
allPagesPageIndex > 0 && allPagesItems.length <= deletedIds.length
|
||||
? Math.max(0, allPagesPageIndex - 1)
|
||||
: allPagesPageIndex;
|
||||
await loadAllPagesPage(nextPageIndex);
|
||||
}
|
||||
if (failedNames.length > 0) {
|
||||
setError(`有 ${failedNames.length} 个页面删除失败:${failedNames.slice(0, 3).join('、')}`);
|
||||
}
|
||||
} finally {
|
||||
setBulkDeletingDraftPages(false);
|
||||
setBulkDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeDraftPages = () => {
|
||||
void startBulkPageDelete([...selectedDraftPageIds], 'draft');
|
||||
};
|
||||
|
||||
const selectedAllPageSet = useMemo(() => new Set(selectedAllPageIds), [selectedAllPageIds]);
|
||||
const allCurrentAllPagesSelected =
|
||||
allPagesItems.length > 0 && allPagesItems.every((page) => selectedAllPageSet.has(page.id));
|
||||
|
||||
const toggleAllPagesOnCurrentPageSelected = () => {
|
||||
if (allCurrentAllPagesSelected) {
|
||||
const currentIds = new Set(allPagesItems.map((page) => page.id));
|
||||
setSelectedAllPageIds((ids) => ids.filter((id) => !currentIds.has(id)));
|
||||
} else {
|
||||
const merged = new Set([...selectedAllPageIds, ...allPagesItems.map((page) => page.id)]);
|
||||
setSelectedAllPageIds([...merged]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelectedAllPages = () => {
|
||||
void startBulkPageDelete([...selectedAllPageIds], 'all');
|
||||
};
|
||||
|
||||
const allPagesPageCount = Math.max(1, Math.ceil(allPagesTotal / ALL_PAGES_PAGE_SIZE));
|
||||
|
||||
const previewAsset = useMemo(
|
||||
() => assets.find((asset) => asset.id === previewAssetId) ?? null,
|
||||
[assets, previewAssetId],
|
||||
@@ -2054,19 +2242,89 @@ export function MindSpaceView({
|
||||
<p className="mindspace-eyebrow">ALL PAGES</p>
|
||||
<h2>全部页面</h2>
|
||||
</div>
|
||||
<span>{pages.length} 个页面</span>
|
||||
<span>{allPagesTotal} 个页面</span>
|
||||
</div>
|
||||
{pages.length > 0 ? (
|
||||
<div className="mindspace-feed-grid mindspace-recent-work-grid is-all-pages">
|
||||
{sortedPages.map((page) => (
|
||||
<MindSpaceFeedCard
|
||||
key={page.id}
|
||||
page={page}
|
||||
previewMode={previewMode}
|
||||
onClick={() => showPage(page.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{allPagesLoading && allPagesItems.length === 0 ? (
|
||||
<div className="mindspace-state">正在加载页面…</div>
|
||||
) : allPagesTotal > 0 ? (
|
||||
<>
|
||||
<div className="mindspace-asset-bulkbar mindspace-all-pages-bulkbar">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAllPagesOnCurrentPageSelected}
|
||||
disabled={bulkDeletingAllPages || allPagesItems.length === 0}
|
||||
>
|
||||
{allCurrentAllPagesSelected ? '取消全选' : '全选'}
|
||||
</button>
|
||||
<span>已选 {selectedAllPageIds.length} 项</span>
|
||||
{selectedAllPageIds.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedAllPageIds([])}
|
||||
disabled={bulkDeletingAllPages}
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-asset-bulk-delete"
|
||||
onClick={() => void removeSelectedAllPages()}
|
||||
disabled={selectedAllPageIds.length === 0 || bulkDeletingAllPages}
|
||||
>
|
||||
{bulkDeletingAllPages ? '删除中…' : '删除选中'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mindspace-feed-grid mindspace-recent-work-grid is-all-pages">
|
||||
{allPagesItems.map((page) => (
|
||||
<MindSpaceFeedCard
|
||||
key={page.id}
|
||||
page={page}
|
||||
previewMode={previewMode}
|
||||
coverMode="thumbnail-only"
|
||||
checkboxPlacement="top-right"
|
||||
checked={selectedAllPageSet.has(page.id)}
|
||||
onCheckChange={(checked) => {
|
||||
setSelectedAllPageIds((ids) =>
|
||||
checked
|
||||
? ids.includes(page.id)
|
||||
? ids
|
||||
: [...ids, page.id]
|
||||
: ids.filter((id) => id !== page.id),
|
||||
);
|
||||
}}
|
||||
onClick={() => void openPageViewInNewTab(page)}
|
||||
onEditPage={() => showPage(page.id)}
|
||||
onOpenPage={() => void openPageViewInNewTab(page)}
|
||||
onShare={() => void openPageShare(page)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{allPagesTotal > ALL_PAGES_PAGE_SIZE && (
|
||||
<div className="mindspace-pagination">
|
||||
<button
|
||||
type="button"
|
||||
disabled={allPagesPageIndex <= 0 || allPagesLoading}
|
||||
onClick={() => void loadAllPagesPage(allPagesPageIndex - 1)}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span>
|
||||
第 {allPagesPageIndex + 1} / {allPagesPageCount} 页
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={
|
||||
allPagesLoading ||
|
||||
(allPagesPageIndex + 1) * ALL_PAGES_PAGE_SIZE >= allPagesTotal
|
||||
}
|
||||
onClick={() => void loadAllPagesPage(allPagesPageIndex + 1)}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mindspace-recent-work-empty">
|
||||
<p>上传资料或从聊天保存页面后,所有页面会集中显示在这里。</p>
|
||||
@@ -2584,11 +2842,11 @@ export function MindSpaceView({
|
||||
<p className="mindspace-eyebrow">RECENT WORK</p>
|
||||
<h2>{pages.length > 0 ? '最近页面' : '这里会出现你的 AI 成果'}</h2>
|
||||
</div>
|
||||
{pages.length > 0 && (
|
||||
{recentPagesTotal > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-section-link"
|
||||
onClick={() => setAllPagesOpen(true)}
|
||||
onClick={openAllPagesPanel}
|
||||
>
|
||||
查看全部
|
||||
</button>
|
||||
@@ -2933,6 +3191,11 @@ export function MindSpaceView({
|
||||
{resolvePublicPageUrl(pageDeletePreview.onlinePublication.publicUrl)}
|
||||
</a>
|
||||
)}
|
||||
<MindSpaceDeletePlazaOption
|
||||
plazaPosts={pageDeletePreview.plazaPosts ?? []}
|
||||
removeFromPlaza={pageDeleteRemoveFromPlaza}
|
||||
onRemoveFromPlazaChange={setPageDeleteRemoveFromPlaza}
|
||||
/>
|
||||
<div className="mindspace-page-editor-actions">
|
||||
<button type="button" onClick={closePageDeleteDialog} disabled={Boolean(deletingPageId)}>
|
||||
取消
|
||||
@@ -2950,6 +3213,57 @@ export function MindSpaceView({
|
||||
)}
|
||||
</MindSpaceModal>
|
||||
|
||||
<MindSpaceModal
|
||||
open={bulkPageDeleteOpen}
|
||||
onClose={closeBulkPageDeleteDialog}
|
||||
title="确认批量删除页面"
|
||||
eyebrow="DELETE PAGES"
|
||||
className="mindspace-delete-dialog"
|
||||
disableClose={
|
||||
bulkPageDeleteLoading || bulkDeletingAllPages || bulkDeletingDraftPages
|
||||
}
|
||||
>
|
||||
{bulkPageDeleteLoading && <div className="mindspace-state">正在分析连带内容…</div>}
|
||||
{bulkPageDeletePreviews && (
|
||||
<>
|
||||
<p className="mindspace-upload-dialog-desc">
|
||||
确定删除选中的 {bulkPageDeletePreviews.length} 个页面吗?删除后无法恢复,关联附件与在线发布也会一并移除。
|
||||
</p>
|
||||
<ul className="mindspace-delete-summary">
|
||||
{bulkPageDeletePreviews.map((preview) => (
|
||||
<li key={preview.page.id}>
|
||||
{preview.page.title}
|
||||
{preview.onlinePublication ? '(含在线公开链接)' : ''}
|
||||
{(preview.plazaPosts?.length ?? 0) > 0 ? '(含广场帖子)' : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<MindSpaceDeletePlazaOption
|
||||
plazaPosts={bulkDeletePlazaPosts}
|
||||
removeFromPlaza={bulkPageDeleteRemoveFromPlaza}
|
||||
onRemoveFromPlazaChange={setBulkPageDeleteRemoveFromPlaza}
|
||||
/>
|
||||
<div className="mindspace-page-editor-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeBulkPageDeleteDialog}
|
||||
disabled={bulkDeletingAllPages || bulkDeletingDraftPages}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-primary"
|
||||
onClick={() => void confirmBulkPageDelete()}
|
||||
disabled={bulkDeletingAllPages || bulkDeletingDraftPages}
|
||||
>
|
||||
{bulkDeletingAllPages || bulkDeletingDraftPages ? '删除中…' : '确认删除'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</MindSpaceModal>
|
||||
|
||||
<MindSpaceModal
|
||||
open={Boolean(assetDeleteDialog)}
|
||||
onClose={() => setAssetDeleteDialog(null)}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function PagePreviewFrame({
|
||||
key={`${src}:${reloadKey}`}
|
||||
title={title}
|
||||
src={src}
|
||||
sandbox="allow-same-origin"
|
||||
sandbox="allow-same-origin allow-downloads"
|
||||
className={className}
|
||||
style={{ height: `${height}px` }}
|
||||
/>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function PageSavePreviewPanel({
|
||||
title="页面预览"
|
||||
src={pagePreviewUrl}
|
||||
className="page-save-mini-page-frame"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
sandbox="allow-scripts allow-same-origin allow-downloads"
|
||||
scrolling="yes"
|
||||
onError={() => setPreviewFailed(true)}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import type { MindSpaceAsset } from '../types';
|
||||
import { buildAssetPreviewFrameConfig } from '../utils/mindspaceCards';
|
||||
|
||||
export type WorkCoverMode = 'thumbnail-first' | 'preview-first' | 'preview-only';
|
||||
export type WorkCoverMode = 'thumbnail-first' | 'thumbnail-only' | 'preview-first' | 'preview-only';
|
||||
|
||||
export function WorkCover({
|
||||
thumbnailUrl,
|
||||
@@ -34,6 +34,7 @@ export function WorkCover({
|
||||
: null;
|
||||
|
||||
const showPreview =
|
||||
coverMode !== 'thumbnail-only' &&
|
||||
Boolean(previewFrame) &&
|
||||
!previewFailed &&
|
||||
(coverMode === 'preview-first' || coverMode === 'preview-only' || thumbFailed);
|
||||
@@ -64,6 +65,10 @@ export function WorkCover({
|
||||
);
|
||||
}
|
||||
|
||||
if (coverMode === 'thumbnail-only') {
|
||||
return <span>{fallbackLabel}</span>;
|
||||
}
|
||||
|
||||
if (previewFrame && !previewFailed) {
|
||||
return (
|
||||
<div className={previewFrameClassName} aria-hidden="true">
|
||||
|
||||
Reference in New Issue
Block a user