Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
retryMindSpaceAgentJob,
|
||||
runMindSpaceAgentJob,
|
||||
runMindSpaceCleanup,
|
||||
purchaseSpaceQuota,
|
||||
uploadMindSpaceAsset,
|
||||
ApiError,
|
||||
} from '../api/client';
|
||||
@@ -58,6 +59,7 @@ import { buildAssetSharePayload, type SharePayload } from '../utils/shareChannel
|
||||
import { resolvePublicPageUrl, resolvePlazaHomeUrl } from '../utils/publicUrl';
|
||||
import { PagePreviewFrame } from './PagePreviewFrame';
|
||||
import { BalanceRing } from './BalanceRing';
|
||||
import { NotificationCenter } from './NotificationCenter';
|
||||
|
||||
const CATEGORY_DESCRIPTIONS: Record<MindSpaceCategory['code'], string> = {
|
||||
oa: '上传文档、表格和资料,让 AI 生成报告与页面',
|
||||
@@ -77,7 +79,7 @@ const CATEGORY_ACTIONS: Partial<Record<MindSpaceCategory['code'], string>> = {
|
||||
const AGENT_JOBS_PAGE_SIZE = 10;
|
||||
const RECENT_PAGES_PAGE_SIZE = 6;
|
||||
const IMAGE_PAGE_SIZE = 10;
|
||||
const MAX_UPLOAD_FILE_BYTES = 2 * 1024 * 1024;
|
||||
const DEFAULT_MAX_UPLOAD_FILE_BYTES = 5 * 1024 * 1024;
|
||||
const UPLOAD_FILE_EXTENSIONS = [
|
||||
'.doc',
|
||||
'.docx',
|
||||
@@ -135,6 +137,35 @@ const PREVIEW_SCHEDULE_ITEMS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
function formatScheduleClock(value: number | null | undefined, timezone?: string | null) {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return '—';
|
||||
try {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: timezone || undefined,
|
||||
}).format(new Date(value));
|
||||
} catch {
|
||||
return new Date(value).toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatDigestSchedule(digest: {
|
||||
hour: number;
|
||||
minute: number;
|
||||
timezone?: string | null;
|
||||
nextRunAt?: number | null;
|
||||
}) {
|
||||
const time = `${String(digest.hour).padStart(2, '0')}:${String(digest.minute).padStart(2, '0')}`;
|
||||
const nextRun = formatScheduleClock(digest.nextRunAt, digest.timezone);
|
||||
return nextRun === '—' ? time : `${time} · 下次 ${nextRun}`;
|
||||
}
|
||||
|
||||
const PREVIEW_PAGE_READERS: Record<string, { kicker: string; body: string[] }> = {
|
||||
'page-1': {
|
||||
kicker: '市场趋势',
|
||||
@@ -182,8 +213,13 @@ const LONG_IMAGE_VIEWPORT_WIDTH = 1280;
|
||||
const LONG_IMAGE_VIEWPORT_HEIGHT = 720;
|
||||
const LONG_IMAGE_RENDER_SCALE = 2;
|
||||
const LONG_IMAGE_MAX_PIXELS = 64_000_000;
|
||||
const SPACE_PURCHASE_PRESETS_MB = [5, 10, 20];
|
||||
let html2CanvasLoader: Promise<Html2CanvasRenderer> | null = null;
|
||||
|
||||
function formatYuan(cents: number) {
|
||||
return `¥${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
async function loadHtml2CanvasRenderer(): Promise<Html2CanvasRenderer> {
|
||||
const existing = (window as Html2CanvasWindow).html2canvas;
|
||||
if (existing) return existing;
|
||||
@@ -246,10 +282,10 @@ function fileExtension(filename: string) {
|
||||
return dotIndex >= 0 ? filename.slice(dotIndex).toLowerCase() : '';
|
||||
}
|
||||
|
||||
function validateSelectedUploadFile(file: File) {
|
||||
function validateSelectedUploadFile(file: File, maxBytes: number) {
|
||||
if (file.size <= 0) return '文件不能为空,请重新选择。';
|
||||
if (file.size > MAX_UPLOAD_FILE_BYTES) {
|
||||
return `文件不能超过 ${formatBytes(MAX_UPLOAD_FILE_BYTES)},当前为 ${formatBytes(file.size)}。`;
|
||||
if (file.size > maxBytes) {
|
||||
return `文件不能超过 ${formatBytes(maxBytes)},当前为 ${formatBytes(file.size)}。`;
|
||||
}
|
||||
const extension = fileExtension(file.name);
|
||||
if (!UPLOAD_FILE_EXTENSIONS.includes(extension as (typeof UPLOAD_FILE_EXTENSIONS)[number])) {
|
||||
@@ -388,18 +424,25 @@ type MindSpaceRouteSync = {
|
||||
};
|
||||
|
||||
function MindSpaceBalanceRing() {
|
||||
const { balanceCents, totalCreditCents, tokensUsed, openRecharge } = useChat();
|
||||
const { balanceCents, totalCreditCents, tokensUsed, subscription, openRecharge, openSubscribe } = useChat();
|
||||
if (typeof balanceCents !== 'number') return null;
|
||||
return (
|
||||
<BalanceRing
|
||||
balanceCents={balanceCents}
|
||||
totalCreditCents={totalCreditCents}
|
||||
tokensUsed={tokensUsed}
|
||||
subscription={subscription}
|
||||
onRecharge={openRecharge}
|
||||
onSubscribe={openSubscribe}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MindSpaceNotificationCenter() {
|
||||
const { openRecharge } = useChat();
|
||||
return <NotificationCenter onOpenRecharge={() => openRecharge(false)} />;
|
||||
}
|
||||
|
||||
export function MindSpaceView({
|
||||
user,
|
||||
previewMode = false,
|
||||
@@ -431,6 +474,8 @@ export function MindSpaceView({
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
||||
const [selectedAssetIds, setSelectedAssetIds] = useState<string[]>([]);
|
||||
const [bulkDeletingAssets, setBulkDeletingAssets] = useState(false);
|
||||
const [newPageOpen, setNewPageOpen] = useState(false);
|
||||
const [newPageTitle, setNewPageTitle] = useState('');
|
||||
const [newPageContent, setNewPageContent] = useState('');
|
||||
@@ -477,7 +522,11 @@ export function MindSpaceView({
|
||||
} | null>(null);
|
||||
const [pageRefreshTrigger, setPageRefreshTrigger] = useState(0);
|
||||
const [pageFullscreenPreviewOpen, setPageFullscreenPreviewOpen] = useState(false);
|
||||
const { chatState, messages, session } = useChat();
|
||||
const [spacePurchaseOpen, setSpacePurchaseOpen] = useState(false);
|
||||
const [spacePurchaseMb, setSpacePurchaseMb] = useState('5');
|
||||
const [spacePurchasePending, setSpacePurchasePending] = useState(false);
|
||||
const [spacePurchaseMessage, setSpacePurchaseMessage] = useState<string | null>(null);
|
||||
const { chatState, messages, session, balanceCents, completeRecharge, openRecharge } = useChat();
|
||||
const prevChatStateRef = useRef(chatState);
|
||||
const h5ApiBase = useMemo(() => resolveH5ApiBase(), []);
|
||||
const location = useLocation();
|
||||
@@ -489,6 +538,7 @@ export function MindSpaceView({
|
||||
const params = new URLSearchParams(location.search);
|
||||
return params.get('return_to') || resolvePlazaHomeUrl();
|
||||
}, [location.search]);
|
||||
const maxUploadFileBytes = space?.quota.maxFileBytes ?? DEFAULT_MAX_UPLOAD_FILE_BYTES;
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPageId) {
|
||||
@@ -688,6 +738,7 @@ export function MindSpaceView({
|
||||
setSelectedCategory(null);
|
||||
setUploadOpen(false);
|
||||
setPendingDeleteId(null);
|
||||
setSelectedAssetIds([]);
|
||||
setImagePage(0);
|
||||
routeSync?.pushHome();
|
||||
};
|
||||
@@ -953,7 +1004,7 @@ export function MindSpaceView({
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (!selectedCategory || !selectedFile) return;
|
||||
const validationError = validateSelectedUploadFile(selectedFile);
|
||||
const validationError = validateSelectedUploadFile(selectedFile, maxUploadFileBytes);
|
||||
if (validationError) {
|
||||
setUploadError(validationError);
|
||||
return;
|
||||
@@ -968,8 +1019,8 @@ export function MindSpaceView({
|
||||
try {
|
||||
const fileToUpload = selectedFile.type.startsWith('image/')
|
||||
? await compressImageForUpload(selectedFile, {
|
||||
maxInputBytes: MAX_UPLOAD_FILE_BYTES,
|
||||
maxOutputBytes: MAX_UPLOAD_FILE_BYTES,
|
||||
maxInputBytes: maxUploadFileBytes,
|
||||
maxOutputBytes: maxUploadFileBytes,
|
||||
maxDimension: 1600,
|
||||
})
|
||||
: selectedFile;
|
||||
@@ -985,6 +1036,34 @@ export function MindSpaceView({
|
||||
}
|
||||
};
|
||||
|
||||
const submitSpacePurchase = async (requestedMb?: number) => {
|
||||
const sizeMb = Math.floor(Number(requestedMb ?? spacePurchaseMb));
|
||||
if (!Number.isFinite(sizeMb) || sizeMb <= 0) {
|
||||
setSpacePurchaseMessage('请输入大于 0 的空间大小(MB)');
|
||||
return;
|
||||
}
|
||||
setSpacePurchasePending(true);
|
||||
setSpacePurchaseMessage(null);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await purchaseSpaceQuota(sizeMb);
|
||||
completeRecharge(result.balanceCents);
|
||||
await refreshMindSpaceSnapshot({ quota: result.quota });
|
||||
setSpacePurchaseMb(String(sizeMb));
|
||||
setSpacePurchaseMessage(
|
||||
`已扩容 ${sizeMb} MB,支付 ¥${(result.costCents / 100).toFixed(2)}。`,
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.code === 'INSUFFICIENT_BALANCE') {
|
||||
setSpacePurchaseMessage('余额不足,请先到余额里充值,再回来购买空间。');
|
||||
} else {
|
||||
setSpacePurchaseMessage(err instanceof Error ? err.message : '购买空间失败');
|
||||
}
|
||||
} finally {
|
||||
setSpacePurchasePending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0] ?? null;
|
||||
if (!file) {
|
||||
@@ -992,7 +1071,7 @@ export function MindSpaceView({
|
||||
setUploadError(null);
|
||||
return;
|
||||
}
|
||||
const validationError = validateSelectedUploadFile(file);
|
||||
const validationError = validateSelectedUploadFile(file, maxUploadFileBytes);
|
||||
if (validationError) {
|
||||
setSelectedFile(null);
|
||||
setUploadError(validationError);
|
||||
@@ -1014,6 +1093,7 @@ export function MindSpaceView({
|
||||
try {
|
||||
await deleteMindSpaceAsset(assetId);
|
||||
setPendingDeleteId(null);
|
||||
setSelectedAssetIds((ids) => ids.filter((id) => id !== assetId));
|
||||
await load();
|
||||
await openCategory(selectedCategory);
|
||||
} catch (err) {
|
||||
@@ -1050,6 +1130,10 @@ export function MindSpaceView({
|
||||
const usedPercent = space
|
||||
? Math.min(100, (occupiedBytes / space.quota.quotaBytes) * 100)
|
||||
: 0;
|
||||
const currentBalanceCents = typeof balanceCents === 'number' ? balanceCents : user.balanceCents;
|
||||
const purchaseMbValue = Math.max(0, Math.floor(Number(spacePurchaseMb) || 0));
|
||||
const purchaseCostCents = purchaseMbValue * 200;
|
||||
const canPurchaseSpace = purchaseMbValue > 0 && currentBalanceCents >= purchaseCostCents;
|
||||
|
||||
const openAssetEditor = async (asset: MindSpaceAsset) => {
|
||||
if (previewMode) {
|
||||
@@ -1186,6 +1270,70 @@ export function MindSpaceView({
|
||||
: assetFilter === 'pages'
|
||||
? workAssets
|
||||
: [];
|
||||
const visibleSelectableAssets = [...pagedImageAssets, ...visibleCardAssets];
|
||||
const selectedAssetSet = useMemo(() => new Set(selectedAssetIds), [selectedAssetIds]);
|
||||
const allVisibleAssetsSelected =
|
||||
visibleSelectableAssets.length > 0 &&
|
||||
visibleSelectableAssets.every((asset) => selectedAssetSet.has(asset.id));
|
||||
|
||||
useEffect(() => {
|
||||
const availableIds = new Set(assets.map((asset) => asset.id));
|
||||
setSelectedAssetIds((ids) => ids.filter((id) => availableIds.has(id)));
|
||||
}, [assets]);
|
||||
|
||||
const toggleAssetSelected = (assetId: string) => {
|
||||
setSelectedAssetIds((ids) =>
|
||||
ids.includes(assetId) ? ids.filter((id) => id !== assetId) : [...ids, assetId],
|
||||
);
|
||||
};
|
||||
|
||||
const toggleVisibleAssetsSelected = () => {
|
||||
const visibleIds = visibleSelectableAssets.map((asset) => asset.id);
|
||||
if (allVisibleAssetsSelected) {
|
||||
setSelectedAssetIds((ids) => ids.filter((id) => !visibleIds.includes(id)));
|
||||
return;
|
||||
}
|
||||
setSelectedAssetIds((ids) => Array.from(new Set([...ids, ...visibleIds])));
|
||||
};
|
||||
|
||||
const removeSelectedAssets = async () => {
|
||||
if (!selectedCategory || selectedAssetIds.length === 0) return;
|
||||
if (previewMode) {
|
||||
setError(previewBlocked().message);
|
||||
return;
|
||||
}
|
||||
setBulkDeletingAssets(true);
|
||||
setError(null);
|
||||
setAssetDeleteDialog(null);
|
||||
const deletingIds = [...selectedAssetIds];
|
||||
const failedIds: string[] = [];
|
||||
const failedNames: string[] = [];
|
||||
try {
|
||||
for (const assetId of deletingIds) {
|
||||
try {
|
||||
await deleteMindSpaceAsset(assetId);
|
||||
} catch (err) {
|
||||
const assetName = assets.find((asset) => asset.id === assetId)?.displayName ?? assetId;
|
||||
failedIds.push(assetId);
|
||||
failedNames.push(assetName);
|
||||
}
|
||||
}
|
||||
setSelectedAssetIds((ids) => ids.filter((id) => failedIds.includes(id)));
|
||||
await load();
|
||||
await openCategory(selectedCategory);
|
||||
if (failedNames.length > 0) {
|
||||
setAssetDeleteDialog({
|
||||
message: `有 ${failedNames.length} 个资产未删除,可能正在被页面使用。`,
|
||||
references: failedNames.slice(0, 6).map((title, index) => ({
|
||||
id: `${index}`,
|
||||
title,
|
||||
})),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setBulkDeletingAssets(false);
|
||||
}
|
||||
};
|
||||
|
||||
const previewAsset = useMemo(
|
||||
() => assets.find((asset) => asset.id === previewAssetId) ?? null,
|
||||
@@ -1219,6 +1367,16 @@ export function MindSpaceView({
|
||||
AI 生成页面
|
||||
</button>
|
||||
)}
|
||||
{asset.publicUrl && (
|
||||
<a
|
||||
className="mindspace-asset-share"
|
||||
href={asset.publicUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
公网图
|
||||
</a>
|
||||
)}
|
||||
{asset.status === 'ready' && asset.scanStatus !== 'blocked' ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1315,7 +1473,19 @@ export function MindSpaceView({
|
||||
|
||||
const showHomeModules =
|
||||
space && !selectedCategory && !selectedPageId && !newPageOpen && !agentJobsPanelOpen && !allPagesOpen;
|
||||
const scheduleItems = previewMode ? PREVIEW_SCHEDULE_ITEMS : [];
|
||||
const scheduleItems = previewMode
|
||||
? PREVIEW_SCHEDULE_ITEMS
|
||||
: (space?.schedule?.todayTodoItems ?? []).map((item) => ({
|
||||
id: item.id,
|
||||
time: item.allDay ? '全天' : formatScheduleClock(item.dueAt ?? item.startAt, item.timezone),
|
||||
kind: item.kind === 'event' ? '日程' : '待办',
|
||||
title: item.title,
|
||||
meta:
|
||||
item.description?.trim() ||
|
||||
(item.sourceChannel === 'wechat' ? '来自服务号' : '来自空间同步'),
|
||||
tone: item.kind === 'event' ? 'event' : 'task',
|
||||
}));
|
||||
const scheduleDigests = previewMode ? [] : space?.schedule?.digestSubscriptions ?? [];
|
||||
const selectedPreviewPage = selectedPageId
|
||||
? pages.find((page) => page.id === selectedPageId) ?? null
|
||||
: null;
|
||||
@@ -1369,6 +1539,11 @@ export function MindSpaceView({
|
||||
) : (
|
||||
<MindSpaceBalanceRing />
|
||||
)}
|
||||
{previewMode ? (
|
||||
<NotificationCenter onOpenRecharge={() => {}} />
|
||||
) : (
|
||||
<MindSpaceNotificationCenter />
|
||||
)}
|
||||
<span>{user.displayName}</span>
|
||||
<button type="button" className="ghost-btn logout-btn" onClick={onLogout}>
|
||||
登出
|
||||
@@ -1417,7 +1592,28 @@ export function MindSpaceView({
|
||||
{space && (
|
||||
<aside className="mindspace-hero-quota">
|
||||
<div className="mindspace-hero-quota-top">
|
||||
<span>免费版空间</span>
|
||||
<span className="mindspace-hero-quota-label">
|
||||
<span>空间容量</span>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-hero-quota-buy-trigger"
|
||||
aria-label="购买空间扩容"
|
||||
onClick={() => {
|
||||
setSpacePurchaseMessage(null);
|
||||
setSpacePurchaseOpen(true);
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M12 5v14M5 12h14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>扩容</span>
|
||||
</button>
|
||||
</span>
|
||||
<strong>
|
||||
{formatBytes(occupiedBytes)} / {formatBytes(space.quota.quotaBytes)}
|
||||
</strong>
|
||||
@@ -1438,6 +1634,7 @@ export function MindSpaceView({
|
||||
含上传预留 {formatBytes(space.quota.reservedBytes)} ·{' '}
|
||||
</>
|
||||
)}
|
||||
剩余 {formatBytes(space.quota.availableBytes)} ·{' '}
|
||||
单文件上限 {formatBytes(space.quota.maxFileBytes)} · 可发布页面{' '}
|
||||
{space.quota.publicPageUsed}/{space.quota.publicPageLimit} · 今日 AI{' '}
|
||||
{space.quota.aiDailyUsed}/{space.quota.aiDailyLimit}
|
||||
@@ -1541,8 +1738,30 @@ export function MindSpaceView({
|
||||
</div>
|
||||
<div className="mindspace-schedule-summary">
|
||||
<strong>今天 {scheduleItems.length || 0} 项</strong>
|
||||
<span>{scheduleItems.length ? '1 个提醒即将推送' : '还没有安排'}</span>
|
||||
<span>
|
||||
{scheduleItems.length > 0
|
||||
? '已有待办记录'
|
||||
: scheduleDigests.length > 0
|
||||
? `${scheduleDigests.length} 个服务号待办推送`
|
||||
: '还没有安排'}
|
||||
</span>
|
||||
</div>
|
||||
{scheduleDigests.length > 0 && (
|
||||
<div className="mindspace-schedule-list">
|
||||
{scheduleDigests.map((digest) => (
|
||||
<article className="mindspace-schedule-item" key={digest.id}>
|
||||
<time>{String(digest.hour).padStart(2, '0')}:{String(digest.minute).padStart(2, '0')}</time>
|
||||
<span className={`mindspace-schedule-kind is-${digest.status === 'active' ? 'event' : 'reminder'}`}>
|
||||
订阅
|
||||
</span>
|
||||
<div>
|
||||
<strong>每天待办摘要</strong>
|
||||
<small>{formatDigestSchedule(digest)}</small>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{scheduleItems.length > 0 ? (
|
||||
<div className="mindspace-schedule-list">
|
||||
{scheduleItems.map((item) => (
|
||||
@@ -1859,6 +2078,33 @@ export function MindSpaceView({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mindspace-asset-bulkbar">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleVisibleAssetsSelected}
|
||||
disabled={visibleSelectableAssets.length === 0 || bulkDeletingAssets}
|
||||
>
|
||||
{allVisibleAssetsSelected ? '取消当前列表' : '选择当前列表'}
|
||||
</button>
|
||||
<span>已选 {selectedAssetIds.length} 项</span>
|
||||
{selectedAssetIds.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedAssetIds([])}
|
||||
disabled={bulkDeletingAssets}
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-asset-bulk-delete"
|
||||
onClick={() => void removeSelectedAssets()}
|
||||
disabled={selectedAssetIds.length === 0 || bulkDeletingAssets}
|
||||
>
|
||||
{bulkDeletingAssets ? '删除中…' : '删除选中'}
|
||||
</button>
|
||||
</div>
|
||||
{visibleImageAssets.length > 0 && (
|
||||
<>
|
||||
{assetFilter === 'all' && (
|
||||
@@ -1875,6 +2121,14 @@ export function MindSpaceView({
|
||||
className={`mindspace-image-card${imagePaginationEnabled ? ' is-compact' : ''}`}
|
||||
key={asset.id}
|
||||
>
|
||||
<label className="mindspace-asset-select">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAssetSet.has(asset.id)}
|
||||
onChange={() => toggleAssetSelected(asset.id)}
|
||||
/>
|
||||
<span>选择</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-image-thumb"
|
||||
@@ -1941,6 +2195,14 @@ export function MindSpaceView({
|
||||
className={`mindspace-item-card${isWorkAsset(asset) ? ' is-work' : ' is-file'}`}
|
||||
key={asset.id}
|
||||
>
|
||||
<label className="mindspace-asset-select is-card">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAssetSet.has(asset.id)}
|
||||
onChange={() => toggleAssetSelected(asset.id)}
|
||||
/>
|
||||
<span>选择</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-item-card-main"
|
||||
@@ -2184,7 +2446,7 @@ export function MindSpaceView({
|
||||
</button>
|
||||
</div>
|
||||
<p className="mindspace-upload-dialog-desc">
|
||||
支持 {UPLOAD_FILE_TYPE_LABEL},单文件最大 {formatBytes(MAX_UPLOAD_FILE_BYTES)}。
|
||||
支持 {UPLOAD_FILE_TYPE_LABEL},单文件最大 {formatBytes(maxUploadFileBytes)}。
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
@@ -2337,6 +2599,107 @@ export function MindSpaceView({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<MindSpaceModal
|
||||
open={spacePurchaseOpen && Boolean(space)}
|
||||
onClose={() => setSpacePurchaseOpen(false)}
|
||||
title="购买空间"
|
||||
eyebrow="SPACE UPGRADE"
|
||||
className="mindspace-space-purchase-dialog"
|
||||
disableClose={spacePurchasePending}
|
||||
>
|
||||
{space && (
|
||||
<>
|
||||
<p className="mindspace-upload-dialog-desc">
|
||||
仅支持使用余额支付。购买成功后会立即同步后台空间配额,并在当前页面实时生效。
|
||||
</p>
|
||||
<div className="mindspace-space-purchase-summary">
|
||||
<div>
|
||||
<span>当前余额</span>
|
||||
<strong>{formatYuan(currentBalanceCents)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>当前剩余空间</span>
|
||||
<strong>{formatBytes(space.quota.availableBytes)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>购买价格</span>
|
||||
<strong>{purchaseMbValue > 0 ? `${formatYuan(purchaseCostCents)} / ${purchaseMbValue} MB` : '请选择容量'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mindspace-hero-purchase">
|
||||
<div className="mindspace-hero-purchase-row">
|
||||
{SPACE_PURCHASE_PRESETS_MB.map((sizeMb) => (
|
||||
<button
|
||||
key={sizeMb}
|
||||
type="button"
|
||||
className={`mindspace-hero-purchase-chip${purchaseMbValue === sizeMb ? ' is-active' : ''}`}
|
||||
disabled={spacePurchasePending}
|
||||
onClick={() => {
|
||||
setSpacePurchaseMb(String(sizeMb));
|
||||
setSpacePurchaseMessage(null);
|
||||
}}
|
||||
>
|
||||
{sizeMb} MB
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mindspace-hero-purchase-row">
|
||||
<input
|
||||
className="mindspace-hero-purchase-input"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={spacePurchaseMb}
|
||||
onChange={(event) => {
|
||||
setSpacePurchaseMb(event.target.value);
|
||||
setSpacePurchaseMessage(null);
|
||||
}}
|
||||
placeholder="自定义 MB"
|
||||
/>
|
||||
</div>
|
||||
<p className="mindspace-hero-purchase-meta">
|
||||
价格为每 1 MB = ¥2,购买后直接增加总容量,不展示在首页主区。
|
||||
</p>
|
||||
{spacePurchaseMessage && (
|
||||
<p className="mindspace-hero-purchase-message">{spacePurchaseMessage}</p>
|
||||
)}
|
||||
</div>
|
||||
{!canPurchaseSpace && purchaseMbValue > 0 && (
|
||||
<div className="mindspace-space-purchase-recharge">
|
||||
<span>当前余额不足以支付这次扩容。</span>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-secondary"
|
||||
onClick={() => {
|
||||
setSpacePurchaseOpen(false);
|
||||
openRecharge(false);
|
||||
}}
|
||||
>
|
||||
去余额充值
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mindspace-page-editor-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSpacePurchaseOpen(false)}
|
||||
disabled={spacePurchasePending}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mindspace-primary"
|
||||
onClick={() => void submitSpacePurchase()}
|
||||
disabled={spacePurchasePending || purchaseMbValue <= 0 || !canPurchaseSpace}
|
||||
>
|
||||
{spacePurchasePending ? '支付中…' : '确认购买'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</MindSpaceModal>
|
||||
|
||||
<MindSpaceModal
|
||||
open={Boolean(pageDeleteTarget)}
|
||||
onClose={closePageDeleteDialog}
|
||||
|
||||
Reference in New Issue
Block a user