Improve MindSpace visual editor UX and default RMB billing.
Add color-coded edit highlights with an in-preview legend, strip CSP meta for srcdoc editing, and simplify fullscreen preview to immersive visual edit only. Default billing to CNY token rates unless H5_USE_BACKEND_COST=1. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+7
-5
@@ -23,6 +23,7 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:8081
|
|||||||
DATABASE_URL=mysql://boot:password@localhost:3306/tkmind
|
DATABASE_URL=mysql://boot:password@localhost:3306/tkmind
|
||||||
# 或分别设置 MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE
|
# 或分别设置 MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE
|
||||||
H5_USERS_ROOT=/root/tkmind_go/users
|
H5_USERS_ROOT=/root/tkmind_go/users
|
||||||
|
# 新用户赠送余额(人民币分,500 = ¥5.00;勿与美元 cents 混淆)
|
||||||
H5_SIGNUP_BALANCE_CENTS=500
|
H5_SIGNUP_BALANCE_CENTS=500
|
||||||
H5_ADMIN_USERNAME=admin
|
H5_ADMIN_USERNAME=admin
|
||||||
H5_ADMIN_PASSWORD=change-me-admin
|
H5_ADMIN_PASSWORD=change-me-admin
|
||||||
@@ -47,12 +48,13 @@ H5_ADMIN_PASSWORD=change-me-admin
|
|||||||
# H5_LOCAL_LLM_NAME=Local Ollama 7B
|
# H5_LOCAL_LLM_NAME=Local Ollama 7B
|
||||||
H5_ACCESS_PASSWORD=change-me
|
H5_ACCESS_PASSWORD=change-me
|
||||||
|
|
||||||
# 计费(Phase 2)
|
# 计费(Phase 2,金额单位均为人民币分)
|
||||||
# H5_USE_BACKEND_COST=1 # 优先用后端 accumulatedCost(USD)计费
|
# 默认按 Token 单价扣费(人民币结算);勿开启美元成本换算除非明确需要
|
||||||
# H5_USD_CNY_RATE=7.2
|
# H5_USE_BACKEND_COST=0
|
||||||
# H5_BILL_INPUT_CENTS_PER_1K=2
|
# H5_BILL_INPUT_CENTS_PER_1K=2 # 输入 Token:2 分/1k(¥0.02/1k)
|
||||||
# H5_BILL_OUTPUT_CENTS_PER_1K=6
|
# H5_BILL_OUTPUT_CENTS_PER_1K=6 # 输出 Token:6 分/1k(¥0.06/1k)
|
||||||
# H5_MIN_BILL_CENTS=1
|
# H5_MIN_BILL_CENTS=1
|
||||||
|
# 仅调试:H5_USE_BACKEND_COST=1 + H5_USD_CNY_RATE=7.2(上游 USD × 汇率 → 人民币分)
|
||||||
|
|
||||||
# 用户自助充值(微信支付)
|
# 用户自助充值(微信支付)
|
||||||
# H5_RECHARGE_TIERS_CENTS=500,1000,3000,5000,10000,20000
|
# H5_RECHARGE_TIERS_CENTS=500,1000,3000,5000,10000,20000
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,6 @@
|
|||||||
export function loadBillingConfig() {
|
export function loadBillingConfig() {
|
||||||
const useBackendCost = process.env.H5_USE_BACKEND_COST !== '0';
|
// 默认按人民币分(CNY cents)计费;仅当 H5_USE_BACKEND_COST=1 时才用上游 USD 成本换算。
|
||||||
|
const useBackendCost = process.env.H5_USE_BACKEND_COST === '1';
|
||||||
return {
|
return {
|
||||||
useBackendCost,
|
useBackendCost,
|
||||||
usdCnyRate: Number(process.env.H5_USD_CNY_RATE ?? 7.2),
|
usdCnyRate: Number(process.env.H5_USD_CNY_RATE ?? 7.2),
|
||||||
@@ -53,6 +54,8 @@ export function computeDeltaCostCents(previous, current, config = loadBillingCon
|
|||||||
return Math.max(config.minBillCents, Math.ceil(currCost * config.usdCnyRate * 100));
|
return Math.max(config.minBillCents, Math.ceil(currCost * config.usdCnyRate * 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 默认路径:按 Token 增量 × 人民币单价(分/1k tokens)扣费。
|
||||||
|
|
||||||
const prevIn = Number(previous?.lastInputTokens ?? 0);
|
const prevIn = Number(previous?.lastInputTokens ?? 0);
|
||||||
const prevOut = Number(previous?.lastOutputTokens ?? 0);
|
const prevOut = Number(previous?.lastOutputTokens ?? 0);
|
||||||
const deltaIn = Math.max(0, current.accumulatedInputTokens - prevIn);
|
const deltaIn = Math.max(0, current.accumulatedInputTokens - prevIn);
|
||||||
|
|||||||
@@ -39,3 +39,16 @@ test('computeDeltaCostCents returns zero when no usage delta', () => {
|
|||||||
});
|
});
|
||||||
assert.equal(computeDeltaCostCents(previous, current, config), 0);
|
assert.equal(computeDeltaCostCents(previous, current, config), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('computeDeltaCostCents ignores backend USD cost unless explicitly enabled', () => {
|
||||||
|
const previous = { lastInputTokens: 0, lastOutputTokens: 0, lastAccumulatedCost: 0.01 };
|
||||||
|
const current = normalizeTokenState({
|
||||||
|
accumulatedInputTokens: 1000,
|
||||||
|
accumulatedOutputTokens: 500,
|
||||||
|
accumulatedCost: 0.02,
|
||||||
|
});
|
||||||
|
const rmbConfig = { ...config, useBackendCost: false };
|
||||||
|
assert.equal(computeDeltaCostCents(previous, current, rmbConfig), 5);
|
||||||
|
const usdConfig = { ...config, useBackendCost: true };
|
||||||
|
assert.equal(computeDeltaCostCents(previous, current, usdConfig), 8);
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,3 +26,14 @@ test('buildEditablePreviewDocument provides blank fallback', () => {
|
|||||||
const result = buildEditablePreviewDocument('');
|
const result = buildEditablePreviewDocument('');
|
||||||
assert.match(result, /空白页面/);
|
assert.match(result, /空白页面/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildEditablePreviewDocument strips CSP meta so editor script can run', () => {
|
||||||
|
const html = `<!DOCTYPE html><html><head>
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'none'">
|
||||||
|
</head><body><h1>探索世界</h1></body></html>`;
|
||||||
|
const result = buildEditablePreviewDocument(html);
|
||||||
|
|
||||||
|
assert.doesNotMatch(result, /Content-Security-Policy/);
|
||||||
|
assert.match(result, /mindspace-visual-editor-script/);
|
||||||
|
assert.match(result, /<h1>探索世界<\/h1>/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { resolvePlazaPostUrl, resolvePublicPageUrl } from '../utils/publicUrl';
|
|||||||
import { MindSpacePageDraftPreviewFrame, type MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame';
|
import { MindSpacePageDraftPreviewFrame, type MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame';
|
||||||
import {
|
import {
|
||||||
MindSpacePageFullscreenPreview,
|
MindSpacePageFullscreenPreview,
|
||||||
type MindSpacePageFullscreenPreviewChat,
|
|
||||||
} from './MindSpacePageFullscreenPreview';
|
} from './MindSpacePageFullscreenPreview';
|
||||||
import { MindSpacePagePreviewPanel } from './MindSpacePagePreviewPanel';
|
import { MindSpacePagePreviewPanel } from './MindSpacePagePreviewPanel';
|
||||||
import { MindSpaceModal } from './MindSpaceModal';
|
import { MindSpaceModal } from './MindSpaceModal';
|
||||||
@@ -96,7 +95,6 @@ export function MindSpacePageDetail({
|
|||||||
onContextUpdate,
|
onContextUpdate,
|
||||||
autoOpenPlaza = false,
|
autoOpenPlaza = false,
|
||||||
refreshTrigger = 0,
|
refreshTrigger = 0,
|
||||||
overlayChat = null,
|
|
||||||
onFullscreenPreviewChange,
|
onFullscreenPreviewChange,
|
||||||
}: {
|
}: {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@@ -110,7 +108,6 @@ export function MindSpacePageDetail({
|
|||||||
}) => void;
|
}) => void;
|
||||||
autoOpenPlaza?: boolean;
|
autoOpenPlaza?: boolean;
|
||||||
refreshTrigger?: number;
|
refreshTrigger?: number;
|
||||||
overlayChat?: MindSpacePageFullscreenPreviewChat | null;
|
|
||||||
onFullscreenPreviewChange?: (open: boolean) => void;
|
onFullscreenPreviewChange?: (open: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const [page, setPage] = useState<MindSpacePage | null>(null);
|
const [page, setPage] = useState<MindSpacePage | null>(null);
|
||||||
@@ -550,6 +547,9 @@ export function MindSpacePageDetail({
|
|||||||
|
|
||||||
const openFullscreenPreview = () => {
|
const openFullscreenPreview = () => {
|
||||||
if (!page || page.contentFormat !== 'html' || !content.trim()) return;
|
if (!page || page.contentFormat !== 'html' || !content.trim()) return;
|
||||||
|
setPreviewContent(content);
|
||||||
|
setPreviewRefreshPending(false);
|
||||||
|
setPreviewKey((value) => value + 1);
|
||||||
setFullscreenPreviewOpen(true);
|
setFullscreenPreviewOpen(true);
|
||||||
onFullscreenPreviewChange?.(true);
|
onFullscreenPreviewChange?.(true);
|
||||||
};
|
};
|
||||||
@@ -1071,7 +1071,7 @@ export function MindSpacePageDetail({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={openFullscreenPreview}
|
onClick={openFullscreenPreview}
|
||||||
disabled={page.contentFormat !== 'html' || !content.trim()}
|
disabled={page.contentFormat !== 'html' || !content.trim()}
|
||||||
title={page.contentFormat === 'html' ? '全屏可视化编辑,可配合 Agent 改页' : '仅 HTML 页面支持全屏编辑'}
|
title={page.contentFormat === 'html' ? '进入沉浸式可视化编辑' : '仅 HTML 页面支持全屏编辑'}
|
||||||
>
|
>
|
||||||
全屏预览编辑
|
全屏预览编辑
|
||||||
</button>
|
</button>
|
||||||
@@ -1156,12 +1156,9 @@ export function MindSpacePageDetail({
|
|||||||
|
|
||||||
{fullscreenPreviewOpen && page && page.contentFormat === 'html' ? (
|
{fullscreenPreviewOpen && page && page.contentFormat === 'html' ? (
|
||||||
<MindSpacePageFullscreenPreview
|
<MindSpacePageFullscreenPreview
|
||||||
pageId={page.id}
|
|
||||||
pageTitle={title || page.title}
|
pageTitle={title || page.title}
|
||||||
title={title}
|
title={title}
|
||||||
summary={summary}
|
content={content}
|
||||||
content={previewContent}
|
|
||||||
templateId={templateId}
|
|
||||||
reloadKey={previewKey}
|
reloadKey={previewKey}
|
||||||
changeHighlight={changeHighlight}
|
changeHighlight={changeHighlight}
|
||||||
canUndo={canUndo}
|
canUndo={canUndo}
|
||||||
@@ -1172,7 +1169,6 @@ export function MindSpacePageDetail({
|
|||||||
onRefresh={() => handleManualPreviewRefresh()}
|
onRefresh={() => handleManualPreviewRefresh()}
|
||||||
refreshPending={previewRefreshPending}
|
refreshPending={previewRefreshPending}
|
||||||
onContentChange={handlePreviewContentChange}
|
onContentChange={handlePreviewContentChange}
|
||||||
chat={overlayChat}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export function MindSpacePageEditablePreviewFrame({
|
|||||||
reloadKey,
|
reloadKey,
|
||||||
minHeight = 680,
|
minHeight = 680,
|
||||||
maxHeight = 12000,
|
maxHeight = 12000,
|
||||||
className = 'mindspace-page-preview-frame',
|
className,
|
||||||
|
compact = false,
|
||||||
|
fillViewport = false,
|
||||||
showHint = true,
|
showHint = true,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
}: {
|
}: {
|
||||||
@@ -21,9 +23,14 @@ export function MindSpacePageEditablePreviewFrame({
|
|||||||
minHeight?: number;
|
minHeight?: number;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
compact?: boolean;
|
||||||
|
fillViewport?: boolean;
|
||||||
showHint?: boolean;
|
showHint?: boolean;
|
||||||
onContentChange: (html: string) => void;
|
onContentChange: (html: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const scrollInsideFrame = compact || fillViewport;
|
||||||
|
const frameClassName =
|
||||||
|
className ?? (compact ? 'page-save-mini-page-frame' : 'mindspace-page-preview-frame');
|
||||||
const [previewFailed, setPreviewFailed] = useState(false);
|
const [previewFailed, setPreviewFailed] = useState(false);
|
||||||
const [srcdoc, setSrcdoc] = useState(() => buildEditablePreviewDocument(content));
|
const [srcdoc, setSrcdoc] = useState(() => buildEditablePreviewDocument(content));
|
||||||
const [height, setHeight] = useState(minHeight);
|
const [height, setHeight] = useState(minHeight);
|
||||||
@@ -62,6 +69,11 @@ export function MindSpacePageEditablePreviewFrame({
|
|||||||
}, [onContentChange]);
|
}, [onContentChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (scrollInsideFrame) {
|
||||||
|
syncHeightRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const iframe = iframeRef.current;
|
const iframe = iframeRef.current;
|
||||||
if (!iframe || !srcdoc) return;
|
if (!iframe || !srcdoc) return;
|
||||||
|
|
||||||
@@ -85,7 +97,7 @@ export function MindSpacePageEditablePreviewFrame({
|
|||||||
syncHeightRef.current = null;
|
syncHeightRef.current = null;
|
||||||
unbind();
|
unbind();
|
||||||
};
|
};
|
||||||
}, [maxHeight, minHeight, srcdoc]);
|
}, [maxHeight, minHeight, scrollInsideFrame, srcdoc]);
|
||||||
|
|
||||||
if (previewFailed) {
|
if (previewFailed) {
|
||||||
return <div className="mindspace-state mindspace-error">页面预览加载失败</div>;
|
return <div className="mindspace-state mindspace-error">页面预览加载失败</div>;
|
||||||
@@ -95,7 +107,14 @@ export function MindSpacePageEditablePreviewFrame({
|
|||||||
<>
|
<>
|
||||||
{showHint ? (
|
{showHint ? (
|
||||||
<p className="mindspace-page-editable-hint">
|
<p className="mindspace-page-editable-hint">
|
||||||
文字直接改 · 图片/音视频点击换 · 背景点空白区 · 链接双击改地址
|
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-text" />
|
||||||
|
文字
|
||||||
|
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-media" />
|
||||||
|
图片/媒体
|
||||||
|
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-link" />
|
||||||
|
链接
|
||||||
|
<span className="mindspace-page-editable-legend-swatch mindspace-page-editable-legend-swatch-bg" />
|
||||||
|
背景 — 预览内左上角有完整图例,虚线框即可直接修改
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<iframe
|
<iframe
|
||||||
@@ -103,9 +122,9 @@ export function MindSpacePageEditablePreviewFrame({
|
|||||||
title={`${title || '页面'} 预览`}
|
title={`${title || '页面'} 预览`}
|
||||||
srcDoc={srcdoc}
|
srcDoc={srcdoc}
|
||||||
sandbox="allow-same-origin allow-scripts"
|
sandbox="allow-same-origin allow-scripts"
|
||||||
scrolling="no"
|
scrolling={scrollInsideFrame ? 'yes' : 'no'}
|
||||||
className={className}
|
className={frameClassName}
|
||||||
style={{ height: `${height}px` }}
|
style={scrollInsideFrame ? { height: '100%' } : { height: `${height}px` }}
|
||||||
onError={() => setPreviewFailed(true)}
|
onError={() => setPreviewFailed(true)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,32 +1,11 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { usePageEditSubChat } from '../hooks/usePageEditSubChat';
|
|
||||||
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types';
|
|
||||||
import { MindSpacePageEditablePreviewFrame } from './MindSpacePageEditablePreviewFrame';
|
import { MindSpacePageEditablePreviewFrame } from './MindSpacePageEditablePreviewFrame';
|
||||||
import type { MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame';
|
import type { MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame';
|
||||||
import { MindSpaceSpaceChat } from './MindSpaceSpaceChat';
|
|
||||||
|
|
||||||
export type MindSpacePageFullscreenPreviewChat = {
|
|
||||||
context: MindSpaceChatContext;
|
|
||||||
user: PortalUser;
|
|
||||||
parentSessionId?: string | null;
|
|
||||||
h5ApiBase?: string | null;
|
|
||||||
onOpenFullChat: () => void;
|
|
||||||
onPageSaved?: (result: {
|
|
||||||
kind: 'page' | 'category';
|
|
||||||
pageId?: string;
|
|
||||||
categoryCode?: MindSpaceSaveCategory;
|
|
||||||
}) => void;
|
|
||||||
onSubSessionChange?: (sessionId: string | null) => void;
|
|
||||||
onForkUnavailable?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function MindSpacePageFullscreenPreview({
|
export function MindSpacePageFullscreenPreview({
|
||||||
pageId,
|
|
||||||
pageTitle,
|
pageTitle,
|
||||||
title,
|
title,
|
||||||
summary: _summary,
|
|
||||||
content,
|
content,
|
||||||
templateId: _templateId,
|
|
||||||
reloadKey,
|
reloadKey,
|
||||||
changeHighlight: _changeHighlight = null,
|
changeHighlight: _changeHighlight = null,
|
||||||
canUndo = false,
|
canUndo = false,
|
||||||
@@ -37,14 +16,10 @@ export function MindSpacePageFullscreenPreview({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
refreshPending = false,
|
refreshPending = false,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
chat,
|
|
||||||
}: {
|
}: {
|
||||||
pageId: string;
|
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
title: string;
|
title: string;
|
||||||
summary: string;
|
|
||||||
content: string;
|
content: string;
|
||||||
templateId: string;
|
|
||||||
reloadKey: number;
|
reloadKey: number;
|
||||||
changeHighlight?: MindSpacePreviewChangeHighlight | null;
|
changeHighlight?: MindSpacePreviewChangeHighlight | null;
|
||||||
canUndo?: boolean;
|
canUndo?: boolean;
|
||||||
@@ -55,40 +30,7 @@ export function MindSpacePageFullscreenPreview({
|
|||||||
onRefresh: () => void;
|
onRefresh: () => void;
|
||||||
refreshPending?: boolean;
|
refreshPending?: boolean;
|
||||||
onContentChange: (html: string) => void;
|
onContentChange: (html: string) => void;
|
||||||
chat?: MindSpacePageFullscreenPreviewChat | null;
|
|
||||||
}) {
|
}) {
|
||||||
const [chatOpen, setChatOpen] = useState(true);
|
|
||||||
const [subChatFallback, setSubChatFallback] = useState(false);
|
|
||||||
const subChatEnabled = Boolean(chat?.parentSessionId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSubChatFallback(false);
|
|
||||||
}, [pageId, chat?.parentSessionId]);
|
|
||||||
|
|
||||||
const subChat = usePageEditSubChat({
|
|
||||||
pageId,
|
|
||||||
pageTitle: pageTitle || title,
|
|
||||||
parentSessionId: chat?.parentSessionId,
|
|
||||||
h5ApiBase: chat?.h5ApiBase,
|
|
||||||
user: chat?.user,
|
|
||||||
enabled: subChatEnabled && !subChatFallback,
|
|
||||||
onSessionChange: chat?.onSubSessionChange,
|
|
||||||
onForkUnavailable: () => {
|
|
||||||
setSubChatFallback(true);
|
|
||||||
chat?.onForkUnavailable?.();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const subChatContext = useMemo(() => {
|
|
||||||
if (!chat?.context) return null;
|
|
||||||
return {
|
|
||||||
...chat.context,
|
|
||||||
pageEditMode: true,
|
|
||||||
parentAgentSessionId: chat.parentSessionId ?? chat.context.parentAgentSessionId,
|
|
||||||
agentSessionId: subChat.session?.id ?? chat.context.agentSessionId,
|
|
||||||
} satisfies MindSpaceChatContext;
|
|
||||||
}, [chat, subChat.session?.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previousOverflow = document.body.style.overflow;
|
const previousOverflow = document.body.style.overflow;
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
@@ -118,13 +60,13 @@ export function MindSpacePageFullscreenPreview({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key !== 'Escape') return;
|
if (event.key === 'Escape') {
|
||||||
if (chatOpen) return;
|
onClose();
|
||||||
onClose();
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [canRedo, canUndo, chatOpen, onClose, onRedo, onUndo]);
|
}, [canRedo, canUndo, onClose, onRedo, onUndo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mindspace-page-fullscreen-preview" role="dialog" aria-modal="true" aria-label="页面全屏预览编辑">
|
<div className="mindspace-page-fullscreen-preview" role="dialog" aria-modal="true" aria-label="页面全屏预览编辑">
|
||||||
@@ -133,16 +75,10 @@ export function MindSpacePageFullscreenPreview({
|
|||||||
<span className="mindspace-eyebrow">VISUAL EDIT</span>
|
<span className="mindspace-eyebrow">VISUAL EDIT</span>
|
||||||
<strong>{pageTitle || '全屏预览编辑'}</strong>
|
<strong>{pageTitle || '全屏预览编辑'}</strong>
|
||||||
<span className="mindspace-page-fullscreen-preview-hint">
|
<span className="mindspace-page-fullscreen-preview-hint">
|
||||||
文字直接改 · 图片/音视频点击换 · 背景点空白区 · 链接双击改地址
|
蓝框=文字 · 橙框=图片/媒体 · 紫框=链接 · 绿框=背景(预览内左上角可看完整图例)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mindspace-page-fullscreen-preview-actions">
|
<div className="mindspace-page-fullscreen-preview-actions">
|
||||||
{subChatEnabled && !subChatFallback && subChat.chatState === 'loading' ? (
|
|
||||||
<span className="mindspace-page-preview-sync">启动编辑 Agent…</span>
|
|
||||||
) : null}
|
|
||||||
{subChatFallback ? (
|
|
||||||
<span className="mindspace-page-preview-sync">使用主对话编辑(子 Agent 未就绪)</span>
|
|
||||||
) : null}
|
|
||||||
<button type="button" onClick={onUndo} disabled={!canUndo} title="撤销 (⌘Z)">
|
<button type="button" onClick={onUndo} disabled={!canUndo} title="撤销 (⌘Z)">
|
||||||
撤销
|
撤销
|
||||||
</button>
|
</button>
|
||||||
@@ -168,28 +104,12 @@ export function MindSpacePageFullscreenPreview({
|
|||||||
title={title}
|
title={title}
|
||||||
content={content}
|
content={content}
|
||||||
reloadKey={reloadKey}
|
reloadKey={reloadKey}
|
||||||
minHeight={720}
|
fillViewport
|
||||||
maxHeight={12000}
|
|
||||||
className="mindspace-page-fullscreen-preview-frame"
|
className="mindspace-page-fullscreen-preview-frame"
|
||||||
showHint={false}
|
showHint={false}
|
||||||
onContentChange={onContentChange}
|
onContentChange={onContentChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{chat && subChatContext ? (
|
|
||||||
<MindSpaceSpaceChat
|
|
||||||
context={subChatContext}
|
|
||||||
user={chat.user}
|
|
||||||
onOpenFullChat={chat.onOpenFullChat}
|
|
||||||
onPageSaved={chat.onPageSaved}
|
|
||||||
open={chatOpen}
|
|
||||||
onOpenChange={setChatOpen}
|
|
||||||
variant="overlay"
|
|
||||||
chatBridge={subChatEnabled && !subChatFallback ? subChat : undefined}
|
|
||||||
hideOpenFullChat={!subChatFallback}
|
|
||||||
title={subChatFallback ? 'TKMind' : '页面编辑 Agent'}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,19 +118,24 @@ export function MindSpacePagePreviewPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-save-preview-dual mindspace-page-preview-dual">
|
<div className="page-save-preview-dual mindspace-page-preview-dual">
|
||||||
<div className="page-save-preview-pane page-save-preview-pane-page">
|
<div
|
||||||
|
className="page-save-preview-pane page-save-preview-pane-page"
|
||||||
|
onWheelCapture={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
<span className="page-save-preview-label">页面预览编辑</span>
|
<span className="page-save-preview-label">页面预览编辑</span>
|
||||||
<p className="page-save-preview-hint">
|
<p className="page-save-preview-hint">
|
||||||
文字直接改 · 图片/音视频点击换 · 背景点空白区 · 链接双击改地址
|
微型页面,可上下滑动查看完整效果 · 虚线框:蓝=文字 · 橙=图片 · 紫=链接 · 绿=背景
|
||||||
</p>
|
</p>
|
||||||
<MindSpacePageEditablePreviewFrame
|
<div className="page-save-mini-page">
|
||||||
title={title}
|
<MindSpacePageEditablePreviewFrame
|
||||||
content={content}
|
title={title}
|
||||||
reloadKey={reloadKey}
|
content={content}
|
||||||
showHint={false}
|
reloadKey={reloadKey}
|
||||||
className="mindspace-page-preview-frame"
|
compact
|
||||||
onContentChange={onContentChange}
|
showHint={false}
|
||||||
/>
|
onContentChange={onContentChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-save-preview-pane page-save-preview-pane-thumb">
|
<div className="page-save-preview-pane page-save-preview-pane-thumb">
|
||||||
<span className="page-save-preview-label">卡片预览图</span>
|
<span className="page-save-preview-label">卡片预览图</span>
|
||||||
|
|||||||
@@ -199,8 +199,6 @@ export function MindSpaceView({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [pageRefreshTrigger, setPageRefreshTrigger] = useState(0);
|
const [pageRefreshTrigger, setPageRefreshTrigger] = useState(0);
|
||||||
const [pageFullscreenPreviewOpen, setPageFullscreenPreviewOpen] = useState(false);
|
const [pageFullscreenPreviewOpen, setPageFullscreenPreviewOpen] = useState(false);
|
||||||
const [pageEditSubSessionId, setPageEditSubSessionId] = useState<string | null>(null);
|
|
||||||
const [pageEditSubSessionActive, setPageEditSubSessionActive] = useState(false);
|
|
||||||
const { chatState, messages, session } = useChat();
|
const { chatState, messages, session } = useChat();
|
||||||
const prevChatStateRef = useRef(chatState);
|
const prevChatStateRef = useRef(chatState);
|
||||||
const h5ApiBase = useMemo(() => resolveH5ApiBase(), []);
|
const h5ApiBase = useMemo(() => resolveH5ApiBase(), []);
|
||||||
@@ -218,22 +216,13 @@ export function MindSpaceView({
|
|||||||
if (!selectedPageId) {
|
if (!selectedPageId) {
|
||||||
setPageLiveContext(null);
|
setPageLiveContext(null);
|
||||||
setPageFullscreenPreviewOpen(false);
|
setPageFullscreenPreviewOpen(false);
|
||||||
setPageEditSubSessionId(null);
|
|
||||||
setPageEditSubSessionActive(false);
|
|
||||||
}
|
}
|
||||||
}, [selectedPageId]);
|
}, [selectedPageId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedPageId || !session?.id || previewMode) return;
|
if (!selectedPageId || !session?.id || previewMode || pageFullscreenPreviewOpen) return;
|
||||||
if (pageFullscreenPreviewOpen && pageEditSubSessionActive) return;
|
|
||||||
void bindMindSpacePageLiveEdit(selectedPageId, session.id).catch(() => {});
|
void bindMindSpacePageLiveEdit(selectedPageId, session.id).catch(() => {});
|
||||||
}, [
|
}, [selectedPageId, session?.id, previewMode, pageFullscreenPreviewOpen]);
|
||||||
selectedPageId,
|
|
||||||
session?.id,
|
|
||||||
previewMode,
|
|
||||||
pageFullscreenPreviewOpen,
|
|
||||||
pageEditSubSessionActive,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const mindspaceChatContext = useMemo(() => {
|
const mindspaceChatContext = useMemo(() => {
|
||||||
if (!space) return null;
|
if (!space) return null;
|
||||||
@@ -247,10 +236,8 @@ export function MindSpaceView({
|
|||||||
assets,
|
assets,
|
||||||
focusedAsset: agentAsset,
|
focusedAsset: agentAsset,
|
||||||
route: `${location.pathname}${location.search}`,
|
route: `${location.pathname}${location.search}`,
|
||||||
agentSessionId: pageEditSubSessionId ?? session?.id ?? null,
|
agentSessionId: session?.id ?? null,
|
||||||
h5ApiBase,
|
h5ApiBase,
|
||||||
pageEditMode: pageFullscreenPreviewOpen,
|
|
||||||
parentAgentSessionId: pageFullscreenPreviewOpen ? session?.id ?? null : null,
|
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
space,
|
space,
|
||||||
@@ -265,8 +252,6 @@ export function MindSpaceView({
|
|||||||
location.search,
|
location.search,
|
||||||
session?.id,
|
session?.id,
|
||||||
h5ApiBase,
|
h5ApiBase,
|
||||||
pageFullscreenPreviewOpen,
|
|
||||||
pageEditSubSessionId,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const truncateLabel = (text: string, max = 48) => {
|
const truncateLabel = (text: string, max = 48) => {
|
||||||
@@ -537,10 +522,10 @@ export function MindSpaceView({
|
|||||||
const previous = prevChatStateRef.current;
|
const previous = prevChatStateRef.current;
|
||||||
prevChatStateRef.current = chatState;
|
prevChatStateRef.current = chatState;
|
||||||
const wasBusy = previous === 'streaming' || previous === 'waiting';
|
const wasBusy = previous === 'streaming' || previous === 'waiting';
|
||||||
if (!wasBusy || chatState !== 'idle' || !selectedPageId || previewMode) return;
|
if (!wasBusy || chatState !== 'idle' || !selectedPageId || previewMode || pageFullscreenPreviewOpen) return;
|
||||||
setPageRefreshTrigger((value) => value + 1);
|
setPageRefreshTrigger((value) => value + 1);
|
||||||
void refreshPagesSilently();
|
void refreshPagesSilently();
|
||||||
}, [chatState, selectedPageId, previewMode]);
|
}, [chatState, pageFullscreenPreviewOpen, previewMode, selectedPageId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
@@ -1174,47 +1159,7 @@ export function MindSpaceView({
|
|||||||
onContextUpdate={setPageLiveContext}
|
onContextUpdate={setPageLiveContext}
|
||||||
autoOpenPlaza={selectedPageAutoPlaza}
|
autoOpenPlaza={selectedPageAutoPlaza}
|
||||||
refreshTrigger={pageRefreshTrigger}
|
refreshTrigger={pageRefreshTrigger}
|
||||||
onFullscreenPreviewChange={(open) => {
|
onFullscreenPreviewChange={setPageFullscreenPreviewOpen}
|
||||||
setPageFullscreenPreviewOpen(open);
|
|
||||||
if (!open) {
|
|
||||||
setPageEditSubSessionId(null);
|
|
||||||
setPageEditSubSessionActive(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
overlayChat={
|
|
||||||
mindspaceChatContext && session?.id
|
|
||||||
? {
|
|
||||||
context: mindspaceChatContext,
|
|
||||||
user,
|
|
||||||
parentSessionId: session.id,
|
|
||||||
h5ApiBase,
|
|
||||||
onOpenFullChat: onBack,
|
|
||||||
onSubSessionChange: (subSessionId) => {
|
|
||||||
setPageEditSubSessionId(subSessionId);
|
|
||||||
setPageEditSubSessionActive(Boolean(subSessionId));
|
|
||||||
},
|
|
||||||
onForkUnavailable: () => {
|
|
||||||
setPageEditSubSessionId(null);
|
|
||||||
setPageEditSubSessionActive(false);
|
|
||||||
if (selectedPageId && session?.id) {
|
|
||||||
void bindMindSpacePageLiveEdit(selectedPageId, session.id).catch(
|
|
||||||
() => {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPageSaved: (result) => {
|
|
||||||
if (result.kind === 'page' && result.pageId) {
|
|
||||||
void refreshPagesSilently();
|
|
||||||
if (result.pageId === selectedPageId) {
|
|
||||||
setPageRefreshTrigger((value) => value + 1);
|
|
||||||
} else {
|
|
||||||
showPage(result.pageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : newPageOpen ? (
|
) : newPageOpen ? (
|
||||||
|
|||||||
+39
-2
@@ -5442,6 +5442,10 @@ body,
|
|||||||
width: 180px;
|
width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mindspace-page-preview-dual .page-save-mini-page {
|
||||||
|
height: min(380px, 46vh);
|
||||||
|
}
|
||||||
|
|
||||||
.mindspace-page-preview-dual + .mindspace-html-source-editor {
|
.mindspace-page-preview-dual + .mindspace-html-source-editor {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
@@ -5537,6 +5541,34 @@ body,
|
|||||||
padding: 10px 18px 0;
|
padding: 10px 18px 0;
|
||||||
color: #6d7771;
|
color: #6d7771;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindspace-page-editable-legend-swatch {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindspace-page-editable-legend-swatch-text {
|
||||||
|
background: rgba(61, 139, 253, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindspace-page-editable-legend-swatch-media {
|
||||||
|
background: rgba(201, 132, 24, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindspace-page-editable-legend-swatch-link {
|
||||||
|
background: rgba(124, 92, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindspace-page-editable-legend-swatch-bg {
|
||||||
|
background: rgba(47, 111, 87, 0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mindspace-page-preview-loading {
|
.mindspace-page-preview-loading {
|
||||||
@@ -6095,14 +6127,19 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mindspace-page-fullscreen-preview-body {
|
.mindspace-page-fullscreen-preview-body {
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
background: #f4f1e8;
|
background: #f4f1e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mindspace-page-fullscreen-preview-frame {
|
.mindspace-page-fullscreen-preview-frame {
|
||||||
display: block;
|
display: block;
|
||||||
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100%;
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,92 @@
|
|||||||
export const MINDSPACE_PAGE_CONTENT_MESSAGE = 'mindspace:page-content';
|
export const MINDSPACE_PAGE_CONTENT_MESSAGE = 'mindspace:page-content';
|
||||||
|
|
||||||
const EDITOR_STYLE = `<style id="mindspace-visual-editor-style">
|
const EDITOR_STYLE = `<style id="mindspace-visual-editor-style">
|
||||||
[data-mindspace-editing="true"] [contenteditable="true"] {
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind] {
|
||||||
outline: 1px dashed rgba(61, 139, 253, 0.45);
|
position: relative;
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"][data-mindspace-highlights="off"] [data-mindspace-edit-kind] {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"][data-mindspace-highlights="off"] [data-mindspace-edit-kind]::after,
|
||||||
|
[data-mindspace-editing="true"][data-mindspace-highlights="off"] [data-mindspace-edit-kind]::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="text"] {
|
||||||
|
outline: 1px dashed rgba(61, 139, 253, 0.72);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(61, 139, 253, 0.12);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
[data-mindspace-editing="true"] [contenteditable="true"]:focus {
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="text"]:focus {
|
||||||
outline: 2px solid rgba(61, 139, 253, 0.95);
|
outline: 2px solid rgba(61, 139, 253, 0.95);
|
||||||
background: rgba(61, 139, 253, 0.06);
|
background: rgba(61, 139, 253, 0.08);
|
||||||
}
|
}
|
||||||
[data-mindspace-editing="true"] img,
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="media"] {
|
||||||
[data-mindspace-editing="true"] video,
|
outline: 2px dashed rgba(238, 176, 78, 0.82);
|
||||||
[data-mindspace-editing="true"] audio,
|
outline-offset: 3px;
|
||||||
[data-mindspace-editing="true"] iframe[data-mindspace-media-target="true"] {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: 1px dashed transparent;
|
box-shadow: 0 0 0 1px rgba(238, 176, 78, 0.18);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="media"]:hover {
|
||||||
|
outline-color: rgba(238, 176, 78, 1);
|
||||||
|
box-shadow: 0 0 0 4px rgba(238, 176, 78, 0.16);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="link"] {
|
||||||
|
outline: 1px dotted rgba(124, 92, 255, 0.72);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
transition: outline-color 0.15s ease;
|
text-decoration: underline dotted rgba(124, 92, 255, 0.55);
|
||||||
}
|
|
||||||
[data-mindspace-editing="true"] img:hover,
|
|
||||||
[data-mindspace-editing="true"] video:hover,
|
|
||||||
[data-mindspace-editing="true"] audio:hover,
|
|
||||||
[data-mindspace-editing="true"] iframe[data-mindspace-media-target="true"]:hover {
|
|
||||||
outline-color: rgba(238, 176, 78, 0.95);
|
|
||||||
}
|
|
||||||
[data-mindspace-editing="true"] a[data-mindspace-link-target="true"] {
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
[data-mindspace-editing="true"] a[data-mindspace-link-target="true"]:hover {
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="link"]:hover {
|
||||||
text-decoration: underline dotted rgba(238, 176, 78, 0.85);
|
outline-color: rgba(124, 92, 255, 0.95);
|
||||||
|
background: rgba(124, 92, 255, 0.08);
|
||||||
}
|
}
|
||||||
[data-mindspace-editing="true"] [data-mindspace-bg-target="true"] {
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"] {
|
||||||
|
outline: 1px dashed rgba(47, 111, 87, 0.55);
|
||||||
|
outline-offset: -2px;
|
||||||
cursor: copy;
|
cursor: copy;
|
||||||
|
box-shadow: inset 0 0 0 9999px rgba(47, 111, 87, 0.04);
|
||||||
}
|
}
|
||||||
[data-mindspace-editing="true"] [data-mindspace-bg-target="true"]:hover {
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"]:hover {
|
||||||
box-shadow: inset 0 0 0 2px rgba(238, 176, 78, 0.45);
|
outline-color: rgba(47, 111, 87, 0.95);
|
||||||
|
box-shadow: inset 0 0 0 9999px rgba(47, 111, 87, 0.1);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind]::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2147483640;
|
||||||
|
max-width: calc(100% - 8px);
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 0 0 8px 0;
|
||||||
|
color: #fffaf0;
|
||||||
|
font: 10px/1.35 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.92;
|
||||||
|
content: attr(data-mindspace-edit-label);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="text"]::after {
|
||||||
|
background: rgba(61, 139, 253, 0.92);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="media"]::after {
|
||||||
|
background: rgba(201, 132, 24, 0.94);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="link"]::after {
|
||||||
|
background: rgba(124, 92, 255, 0.92);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"]::after {
|
||||||
|
background: rgba(47, 111, 87, 0.92);
|
||||||
|
}
|
||||||
|
[data-mindspace-editing="true"] [data-mindspace-edit-kind="background"][data-mindspace-bg-large="true"]::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 10px;
|
||||||
|
border: 1px dashed rgba(47, 111, 87, 0.35);
|
||||||
|
border-radius: 8px;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.mindspace-editor-popover {
|
.mindspace-editor-popover {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -119,6 +170,54 @@ const EDITOR_STYLE = `<style id="mindspace-visual-editor-style">
|
|||||||
box-shadow: 0 8px 28px rgba(24, 33, 29, 0.12);
|
box-shadow: 0 8px 28px rgba(24, 33, 29, 0.12);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
.mindspace-editor-legend {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2147483645;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
width: min(280px, calc(100vw - 24px));
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(24, 33, 29, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 252, 244, 0.96);
|
||||||
|
color: #18211d;
|
||||||
|
box-shadow: 0 10px 28px rgba(24, 33, 29, 0.12);
|
||||||
|
font: 11px/1.45 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
.mindspace-editor-legend strong {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.mindspace-editor-legend-items {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.mindspace-editor-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.mindspace-editor-legend-swatch {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mindspace-editor-legend-swatch[data-kind="text"] { background: rgba(61, 139, 253, 0.92); }
|
||||||
|
.mindspace-editor-legend-swatch[data-kind="media"] { background: rgba(201, 132, 24, 0.94); }
|
||||||
|
.mindspace-editor-legend-swatch[data-kind="link"] { background: rgba(124, 92, 255, 0.92); }
|
||||||
|
.mindspace-editor-legend-swatch[data-kind="background"] { background: rgba(47, 111, 87, 0.92); }
|
||||||
|
.mindspace-editor-legend-toggle {
|
||||||
|
justify-self: start;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: #52605a;
|
||||||
|
font: inherit;
|
||||||
|
background: rgba(24, 33, 29, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>`;
|
</style>`;
|
||||||
|
|
||||||
const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
||||||
@@ -140,19 +239,30 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stripEditorArtifacts(root) {
|
function stripEditorArtifacts(root) {
|
||||||
root.querySelectorAll('#mindspace-visual-editor-style,#mindspace-visual-editor-script,.mindspace-editor-popover,.mindspace-editor-guide').forEach(function (node) {
|
root.querySelectorAll('#mindspace-visual-editor-style,#mindspace-visual-editor-script,.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend').forEach(function (node) {
|
||||||
node.remove();
|
node.remove();
|
||||||
});
|
});
|
||||||
root.querySelectorAll('[contenteditable]').forEach(function (node) {
|
root.querySelectorAll('[contenteditable]').forEach(function (node) {
|
||||||
node.removeAttribute('contenteditable');
|
node.removeAttribute('contenteditable');
|
||||||
node.removeAttribute('spellcheck');
|
node.removeAttribute('spellcheck');
|
||||||
});
|
});
|
||||||
root.querySelectorAll('[data-mindspace-bg-target],[data-mindspace-link-target],[data-mindspace-media-target]').forEach(function (node) {
|
root.querySelectorAll('[data-mindspace-bg-target],[data-mindspace-link-target],[data-mindspace-media-target],[data-mindspace-edit-kind],[data-mindspace-edit-label],[data-mindspace-bg-large]').forEach(function (node) {
|
||||||
node.removeAttribute('data-mindspace-bg-target');
|
node.removeAttribute('data-mindspace-bg-target');
|
||||||
node.removeAttribute('data-mindspace-link-target');
|
node.removeAttribute('data-mindspace-link-target');
|
||||||
node.removeAttribute('data-mindspace-media-target');
|
node.removeAttribute('data-mindspace-media-target');
|
||||||
|
node.removeAttribute('data-mindspace-edit-kind');
|
||||||
|
node.removeAttribute('data-mindspace-edit-label');
|
||||||
|
node.removeAttribute('data-mindspace-bg-large');
|
||||||
});
|
});
|
||||||
if (root.body) root.body.removeAttribute('data-mindspace-editing');
|
if (root.body) {
|
||||||
|
root.body.removeAttribute('data-mindspace-editing');
|
||||||
|
root.body.removeAttribute('data-mindspace-highlights');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markEditable(el, kind, label) {
|
||||||
|
el.setAttribute('data-mindspace-edit-kind', kind);
|
||||||
|
el.setAttribute('data-mindspace-edit-label', label);
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeDocument() {
|
function serializeDocument() {
|
||||||
@@ -368,61 +478,103 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
|||||||
function markBackgroundTargets() {
|
function markBackgroundTargets() {
|
||||||
var nodes = document.querySelectorAll('body, main, section, article, header, footer, div');
|
var nodes = document.querySelectorAll('body, main, section, article, header, footer, div');
|
||||||
nodes.forEach(function (el) {
|
nodes.forEach(function (el) {
|
||||||
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide')) return;
|
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
|
if (el.isContentEditable) return;
|
||||||
var style = window.getComputedStyle(el);
|
var style = window.getComputedStyle(el);
|
||||||
var cls = String(el.className || '');
|
var cls = String(el.className || '');
|
||||||
var structural = /\\b(page|hero|cover|banner|bg|background|section|card|panel|wrap|container|shell|stage|canvas|frame|viewport)\\b/i.test(cls);
|
var structural = /\\b(page|hero|cover|banner|bg|background|section|card|panel|wrap|container|shell|stage|canvas|frame|viewport)\\b/i.test(cls);
|
||||||
var painted = style.backgroundImage !== 'none' || (style.backgroundColor && style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor !== 'transparent');
|
var painted = style.backgroundImage !== 'none' || (style.backgroundColor && style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor !== 'transparent');
|
||||||
if (el.tagName === 'BODY' || structural || (painted && el.offsetWidth > 64 && el.offsetHeight > 64)) {
|
if (el.tagName === 'BODY' || structural || (painted && el.offsetWidth > 64 && el.offsetHeight > 64)) {
|
||||||
el.setAttribute('data-mindspace-bg-target', 'true');
|
el.setAttribute('data-mindspace-bg-target', 'true');
|
||||||
|
markEditable(el, 'background', '点击换背景');
|
||||||
|
if (el.offsetWidth > 180 && el.offsetHeight > 120) {
|
||||||
|
el.setAttribute('data-mindspace-bg-large', 'true');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function markMediaTargets() {
|
function markMediaTargets() {
|
||||||
|
document.querySelectorAll('img').forEach(function (img) {
|
||||||
|
if (img.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
|
markEditable(img, 'media', '点击换图片');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('video').forEach(function (video) {
|
||||||
|
if (video.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
|
markEditable(video, 'media', '点击换视频');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('audio').forEach(function (audio) {
|
||||||
|
if (audio.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
|
markEditable(audio, 'media', '点击换音频');
|
||||||
|
});
|
||||||
document.querySelectorAll('iframe').forEach(function (iframe) {
|
document.querySelectorAll('iframe').forEach(function (iframe) {
|
||||||
if (iframe.closest('.mindspace-editor-popover,.mindspace-editor-guide')) return;
|
if (iframe.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
iframe.setAttribute('data-mindspace-media-target', 'true');
|
iframe.setAttribute('data-mindspace-media-target', 'true');
|
||||||
|
markEditable(iframe, 'media', '点击换嵌入');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function markLinkTargets() {
|
function markLinkTargets() {
|
||||||
document.querySelectorAll('a[href]').forEach(function (anchor) {
|
document.querySelectorAll('a[href]').forEach(function (anchor) {
|
||||||
if (anchor.closest('.mindspace-editor-popover,.mindspace-editor-guide')) return;
|
if (anchor.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
anchor.setAttribute('data-mindspace-link-target', 'true');
|
anchor.setAttribute('data-mindspace-link-target', 'true');
|
||||||
|
markEditable(anchor, 'link', '双击改链接');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableTextEditing() {
|
function enableTextEditing() {
|
||||||
document.querySelectorAll(TEXT_SELECTOR).forEach(function (el) {
|
document.querySelectorAll(TEXT_SELECTOR).forEach(function (el) {
|
||||||
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide')) return;
|
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
if (el.querySelector && el.querySelector(MEDIA_BLOCK_SELECTOR)) return;
|
if (el.querySelector && el.querySelector(MEDIA_BLOCK_SELECTOR)) return;
|
||||||
|
if (el.matches('a[data-mindspace-link-target="true"]')) return;
|
||||||
|
var textParent = el.closest('[data-mindspace-edit-kind="text"]');
|
||||||
|
if (textParent && textParent !== el) return;
|
||||||
el.setAttribute('contenteditable', 'true');
|
el.setAttribute('contenteditable', 'true');
|
||||||
el.setAttribute('spellcheck', 'true');
|
el.setAttribute('spellcheck', 'true');
|
||||||
|
markEditable(el, 'text', '点击改文字');
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('div').forEach(function (el) {
|
document.querySelectorAll('div').forEach(function (el) {
|
||||||
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide')) return;
|
if (el.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
if (el.hasAttribute('contenteditable')) return;
|
if (el.hasAttribute('contenteditable')) return;
|
||||||
if (el.querySelector(BLOCK_CHILD_SELECTOR)) return;
|
if (el.querySelector(BLOCK_CHILD_SELECTOR)) return;
|
||||||
if (el.querySelector(MEDIA_BLOCK_SELECTOR)) return;
|
if (el.querySelector(MEDIA_BLOCK_SELECTOR)) return;
|
||||||
if (!String(el.textContent || '').trim()) return;
|
if (!String(el.textContent || '').trim()) return;
|
||||||
el.setAttribute('contenteditable', 'true');
|
el.setAttribute('contenteditable', 'true');
|
||||||
el.setAttribute('spellcheck', 'true');
|
el.setAttribute('spellcheck', 'true');
|
||||||
|
markEditable(el, 'text', '点击改文字');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEditGuide() {
|
function addEditLegend() {
|
||||||
var bar = document.createElement('div');
|
var legend = document.createElement('div');
|
||||||
bar.className = 'mindspace-editor-guide';
|
legend.className = 'mindspace-editor-legend';
|
||||||
bar.textContent = '文字:直接点击 · 图片/音视频:点击替换 · 背景:点击空白区域 · 链接:双击改地址';
|
legend.innerHTML =
|
||||||
document.body.appendChild(bar);
|
'<strong>编辑模式 · 可修改区域</strong>' +
|
||||||
|
'<div class="mindspace-editor-legend-items">' +
|
||||||
|
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="text"></span><span>蓝色虚线 = 文字,直接点击修改</span></div>' +
|
||||||
|
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="media"></span><span>橙色虚线 = 图片/音视频,点击替换</span></div>' +
|
||||||
|
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="link"></span><span>紫色虚线 = 链接,双击改地址</span></div>' +
|
||||||
|
'<div class="mindspace-editor-legend-item"><span class="mindspace-editor-legend-swatch" data-kind="background"></span><span>绿色虚线 = 背景区域,点击换背景</span></div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<button type="button" class="mindspace-editor-legend-toggle" data-action="toggle-highlights">隐藏标记</button>';
|
||||||
|
document.body.appendChild(legend);
|
||||||
|
legend.querySelector('[data-action="toggle-highlights"]').addEventListener('click', function () {
|
||||||
|
var hidden = document.body.getAttribute('data-mindspace-highlights') === 'off';
|
||||||
|
if (hidden) {
|
||||||
|
document.body.removeAttribute('data-mindspace-highlights');
|
||||||
|
this.textContent = '隐藏标记';
|
||||||
|
} else {
|
||||||
|
document.body.setAttribute('data-mindspace-highlights', 'off');
|
||||||
|
this.textContent = '显示标记';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(event) {
|
function onClick(event) {
|
||||||
var target = event.target;
|
var target = event.target;
|
||||||
if (!(target instanceof Element)) return;
|
if (!(target instanceof Element)) return;
|
||||||
if (target.closest('.mindspace-editor-popover,.mindspace-editor-guide')) return;
|
if (target.closest('.mindspace-editor-popover,.mindspace-editor-guide,.mindspace-editor-legend')) return;
|
||||||
|
|
||||||
if (target.tagName === 'IMG') {
|
if (target.tagName === 'IMG') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -461,7 +613,7 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
|||||||
markBackgroundTargets();
|
markBackgroundTargets();
|
||||||
markMediaTargets();
|
markMediaTargets();
|
||||||
markLinkTargets();
|
markLinkTargets();
|
||||||
addEditGuide();
|
addEditLegend();
|
||||||
document.addEventListener('input', debounceEmit);
|
document.addEventListener('input', debounceEmit);
|
||||||
document.addEventListener('blur', debounceEmit, true);
|
document.addEventListener('blur', debounceEmit, true);
|
||||||
document.addEventListener('click', onClick, true);
|
document.addEventListener('click', onClick, true);
|
||||||
@@ -483,21 +635,32 @@ const EDITOR_SCRIPT = `<script id="mindspace-visual-editor-script">
|
|||||||
})();
|
})();
|
||||||
</script>`;
|
</script>`;
|
||||||
|
|
||||||
|
/** Remove inline CSP meta tags so preview-edit bootstrap can run inside srcdoc iframes. */
|
||||||
|
export function stripPreviewEditCspMeta(html: string): string {
|
||||||
|
return String(html ?? '').replace(
|
||||||
|
/<meta\s+http-equiv\s*=\s*["']Content-Security-Policy["'][^>]*>/gi,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildEditablePreviewDocument(html: string): string {
|
export function buildEditablePreviewDocument(html: string): string {
|
||||||
const source = String(html ?? '').trim();
|
const source = String(html ?? '').trim();
|
||||||
|
const cleaned = stripPreviewEditCspMeta(source)
|
||||||
|
.replace(/<style id="mindspace-visual-editor-style">[\s\S]*?<\/style>/gi, '')
|
||||||
|
.replace(/<script id="mindspace-visual-editor-script">[\s\S]*?<\/script>/gi, '');
|
||||||
const injection = `${EDITOR_STYLE}${EDITOR_SCRIPT}`;
|
const injection = `${EDITOR_STYLE}${EDITOR_SCRIPT}`;
|
||||||
|
|
||||||
if (!source) {
|
if (!cleaned) {
|
||||||
return buildEditablePreviewDocument(
|
return buildEditablePreviewDocument(
|
||||||
'<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"></head><body><p>空白页面</p></body></html>',
|
'<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"></head><body><p>空白页面</p></body></html>',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/<\/body>/i.test(source)) {
|
if (/<\/body>/i.test(cleaned)) {
|
||||||
return source.replace(/<\/body>/i, `${injection}</body>`);
|
return cleaned.replace(/<\/body>/i, `${injection}</body>`);
|
||||||
}
|
}
|
||||||
if (/<\/html>/i.test(source)) {
|
if (/<\/html>/i.test(cleaned)) {
|
||||||
return source.replace(/<\/html>/i, `${injection}</html>`);
|
return cleaned.replace(/<\/html>/i, `${injection}</html>`);
|
||||||
}
|
}
|
||||||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"></head><body>${source}${injection}</body></html>`;
|
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"></head><body>${cleaned}${injection}</body></html>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user