fix:锁定文件只读
This commit is contained in:
@@ -10,10 +10,12 @@ import type { MouseEvent as ReactMouseEvent } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { useRef, useLayoutEffect, useState, useEffect } from "react";
|
||||
|
||||
import { useScriptWorkspaceStore, type PythonEditorBuffer } from "./state/scriptWorkspaceStore";
|
||||
import { PythonEditor } from "./PythonEditor";
|
||||
import { useAuth } from "~/context/AuthContext";
|
||||
import { getScriptContent } from "~/services/api";
|
||||
import { useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import { getScriptContent } from "../../services/api";
|
||||
|
||||
// 模块级变量存储刷新版本号,用于检测只读内容刷新
|
||||
let _lastRefreshVersion = 0;
|
||||
|
||||
type ToastState = {
|
||||
tone: "success" | "error" | "info";
|
||||
@@ -73,11 +75,131 @@ function shortHash(value: string) {
|
||||
return value ? `${value.slice(0, 8)}…${value.slice(-6)}` : "—";
|
||||
}
|
||||
|
||||
function confineJupyterFrame(frame: HTMLIFrameElement): void {
|
||||
// Notebook 单元格类型
|
||||
type NotebookCell = {
|
||||
cell_type: "code" | "markdown" | "raw";
|
||||
source: string[] | string;
|
||||
execution_count?: number | null;
|
||||
outputs?: Array<{
|
||||
name?: "stdout" | "stderr" | string;
|
||||
output_type: "stream" | "execute_result" | "display_data" | "error";
|
||||
text?: string[] | string;
|
||||
data?: Record<string, unknown>;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Notebook Viewer 组件
|
||||
function NotebookViewer({ content }: { content: object }) {
|
||||
const notebook = content as {
|
||||
cells?: NotebookCell[];
|
||||
metadata?: Record<string, unknown>;
|
||||
nbformat?: number;
|
||||
nbformat_minor?: number;
|
||||
};
|
||||
|
||||
const cells = notebook.cells ?? [];
|
||||
|
||||
return (
|
||||
<div className="notebook-read-only">
|
||||
{cells.map((cell, index) => {
|
||||
const sourceText = Array.isArray(cell.source)
|
||||
? cell.source.join("")
|
||||
: (cell.source ?? "");
|
||||
|
||||
if (cell.cell_type === "markdown") {
|
||||
return (
|
||||
<div key={index} className="notebook-cell notebook-cell--markdown">
|
||||
<div className="notebook-cell__content">
|
||||
{renderMarkdown(sourceText)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.cell_type === "code") {
|
||||
const hasSource = sourceText.trim().length > 0;
|
||||
return (
|
||||
<div key={index} className="notebook-cell notebook-cell--code">
|
||||
<div className="notebook-cell__prompt">
|
||||
In [{cell.execution_count ?? " "}]:
|
||||
</div>
|
||||
<div className="notebook-cell__content">
|
||||
<div className={`notebook-cell__input${!hasSource ? ' notebook-cell__input--empty' : ''}`}>
|
||||
<pre><code>{sourceText}</code></pre>
|
||||
</div>
|
||||
{cell.outputs && cell.outputs.length > 0 && (
|
||||
<div className="notebook-cell__outputs">
|
||||
{cell.outputs.map((output, outputIndex) => {
|
||||
// 处理 stream 类型的输出
|
||||
if (output.output_type === "stream" && output.text) {
|
||||
const text = Array.isArray(output.text)
|
||||
? output.text.join("")
|
||||
: output.text;
|
||||
return (
|
||||
<div key={outputIndex} className={`notebook-output notebook-output--${output.name}`}>
|
||||
<pre>{text}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 处理 execute_result / display_data 类型
|
||||
if (output.output_type === "execute_result" || output.output_type === "display_data") {
|
||||
const text = output.data?.["text/plain"];
|
||||
if (text) {
|
||||
const textStr = Array.isArray(text) ? text.join("") : String(text);
|
||||
return (
|
||||
<div key={outputIndex} className="notebook-output">
|
||||
<pre>{textStr}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 简单的 markdown 渲染函数
|
||||
function renderMarkdown(text: string) {
|
||||
const lines = text.split("\n");
|
||||
return lines.map((line, i) => {
|
||||
if (line.startsWith("# ")) {
|
||||
return <h1 key={i}>{line.slice(2)}</h1>;
|
||||
}
|
||||
if (line.startsWith("## ")) {
|
||||
return <h2 key={i}>{line.slice(3)}</h2>;
|
||||
}
|
||||
if (line.startsWith("### ")) {
|
||||
return <h3 key={i}>{line.slice(4)}</h3>;
|
||||
}
|
||||
if (line.startsWith("- ") || line.startsWith("* ")) {
|
||||
return <li key={i}>{line.slice(2)}</li>;
|
||||
}
|
||||
if (line.match(/^\d+\. /)) {
|
||||
return <li key={i}>{line.replace(/^\d+\. /, "")}</li>;
|
||||
}
|
||||
if (line.trim() === "") {
|
||||
return <br key={i} />;
|
||||
}
|
||||
return <p key={i}>{line}</p>;
|
||||
});
|
||||
}
|
||||
|
||||
function confineJupyterFrame(frame: HTMLIFrameElement, readOnly: boolean = false): void {
|
||||
try {
|
||||
const document = frame.contentDocument;
|
||||
if (!document?.documentElement) return;
|
||||
const keepInside = (): void => {
|
||||
// 隐藏 "Open in..." 按钮
|
||||
document.querySelectorAll<HTMLElement>("button,[role='button']").forEach(
|
||||
(element) => {
|
||||
const label = `${element.getAttribute("aria-label") ?? ""} ${
|
||||
@@ -86,8 +208,15 @@ function confineJupyterFrame(frame: HTMLIFrameElement): void {
|
||||
if (/^open in\b/i.test(label) || /\bopen in\.\.\./i.test(label)) {
|
||||
element.style.setProperty("display", "none", "important");
|
||||
}
|
||||
// 只读模式下隐藏保存/编辑相关的按钮
|
||||
if (readOnly) {
|
||||
if (/\bsave\b|\bedit\b|\brun\b/i.test(label)) {
|
||||
element.style.setProperty("display", "none", "important");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
// 阻止新窗口打开链接
|
||||
document.querySelectorAll<HTMLAnchorElement>("a[target]").forEach((link) => {
|
||||
if (["_blank", "_top", "_parent"].includes(link.target)) {
|
||||
link.target = "_self";
|
||||
@@ -141,6 +270,8 @@ export function ScriptWorkspace({
|
||||
const tabbarRef = useRef<HTMLDivElement | null>(null);
|
||||
const isNotebook = script.script_type === "notebook";
|
||||
const isPython = script.script_type === "python";
|
||||
// 订阅 store 的刷新版本号,用于触发只读内容刷新
|
||||
const readOnlyRefreshVersion = useScriptWorkspaceStore((s) => s.readOnlyRefreshVersion);
|
||||
// 判断当前选中的文件是否正在编辑(需要同时检查 session_status 和 script_id)
|
||||
const isEditing = editSession?.session_status === "active"
|
||||
&& editSession?.script_id === script.script_id;
|
||||
@@ -154,8 +285,8 @@ export function ScriptWorkspace({
|
||||
);
|
||||
const hasEmbeddedContent = sessionCache.size > 0 || pythonTabBuffers.length > 0;
|
||||
|
||||
// 只读模式状态
|
||||
const [readOnlyContent, setReadOnlyContent] = useState<string | null>(null);
|
||||
// 只读模式状态(用于 Python 文件和 notebook 的 JSON 内容)
|
||||
const [readOnlyContent, setReadOnlyContent] = useState<string | object | null>(null);
|
||||
const [readOnlyLoading, setReadOnlyLoading] = useState(false);
|
||||
const [readOnlyError, setReadOnlyError] = useState<string | null>(null);
|
||||
|
||||
@@ -201,13 +332,7 @@ export function ScriptWorkspace({
|
||||
|
||||
getScriptContent(script.workspace_id, script.script_id)
|
||||
.then((data) => {
|
||||
if (data.script_type === 'notebook') {
|
||||
// Notebook 转为 JSON 字符串显示
|
||||
setReadOnlyContent(JSON.stringify(data.content, null, 2));
|
||||
} else {
|
||||
// Python 文件直接显示
|
||||
setReadOnlyContent(data.content as string);
|
||||
}
|
||||
setReadOnlyContent(data.content);
|
||||
})
|
||||
.catch((err) => {
|
||||
setReadOnlyError(err instanceof Error ? err.message : '加载失败');
|
||||
@@ -215,7 +340,27 @@ export function ScriptWorkspace({
|
||||
.finally(() => {
|
||||
setReadOnlyLoading(false);
|
||||
});
|
||||
}, [isReadOnlyMode, script.script_id, script.workspace_id, script.script_type]);
|
||||
}, [isReadOnlyMode, script.script_id, script.workspace_id]);
|
||||
|
||||
// 监听只读内容刷新
|
||||
useEffect(() => {
|
||||
if (readOnlyRefreshVersion > _lastRefreshVersion && isReadOnlyMode) {
|
||||
_lastRefreshVersion = readOnlyRefreshVersion;
|
||||
// 重新加载只读内容
|
||||
setReadOnlyLoading(true);
|
||||
setReadOnlyError(null);
|
||||
getScriptContent(script.workspace_id, script.script_id)
|
||||
.then((data) => {
|
||||
setReadOnlyContent(data.content);
|
||||
})
|
||||
.catch((err) => {
|
||||
setReadOnlyError(err instanceof Error ? err.message : '加载失败');
|
||||
})
|
||||
.finally(() => {
|
||||
setReadOnlyLoading(false);
|
||||
});
|
||||
}
|
||||
}, [script.script_id, script.workspace_id, isReadOnlyMode, readOnlyRefreshVersion]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -337,35 +482,41 @@ export function ScriptWorkspace({
|
||||
<Icon name="lock" size={16} />
|
||||
<span>此文件已被锁定,您当前处于只读模式</span>
|
||||
</div>
|
||||
{readOnlyLoading ? (
|
||||
<div className="readonly-editor-loading">
|
||||
<span className="button-spinner button-spinner--blue" />
|
||||
<span>加载内容中...</span>
|
||||
</div>
|
||||
) : readOnlyError ? (
|
||||
<div className="readonly-editor-error">
|
||||
<Icon name="info" size={28} />
|
||||
<strong>加载失败</strong>
|
||||
<p>{readOnlyError}</p>
|
||||
</div>
|
||||
) : (
|
||||
<Editor
|
||||
height="calc(100% - 50px)"
|
||||
language={script.script_type === 'notebook' ? 'json' : 'python'}
|
||||
value={readOnlyContent ?? ''}
|
||||
theme="vs"
|
||||
options={{
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
minimap: { enabled: true },
|
||||
lineNumbers: "on",
|
||||
folding: true,
|
||||
wordWrap: "on",
|
||||
contextmenu: false,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="readonly-editor-content">
|
||||
{readOnlyLoading ? (
|
||||
<div className="readonly-editor-loading">
|
||||
<span className="button-spinner button-spinner--blue" />
|
||||
<span>加载内容中...</span>
|
||||
</div>
|
||||
) : readOnlyError ? (
|
||||
<div className="readonly-editor-error">
|
||||
<Icon name="info" size={28} />
|
||||
<strong>加载失败</strong>
|
||||
<p>{readOnlyError}</p>
|
||||
</div>
|
||||
) : script.script_type === 'notebook' && readOnlyContent && typeof readOnlyContent === 'object' ? (
|
||||
// Notebook 使用单元格渲染
|
||||
<NotebookViewer content={readOnlyContent} />
|
||||
) : (
|
||||
// Python 文件使用 Monaco Editor 显示
|
||||
<Editor
|
||||
height="100%"
|
||||
language="python"
|
||||
value={typeof readOnlyContent === 'string' ? readOnlyContent : ''}
|
||||
theme="vs"
|
||||
options={{
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
minimap: { enabled: true },
|
||||
lineNumbers: "on",
|
||||
folding: true,
|
||||
wordWrap: "on",
|
||||
contextmenu: false,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 正常编辑模式:原有的 iframe + Monaco 预览逻辑
|
||||
|
||||
Reference in New Issue
Block a user