479 lines
15 KiB
HTML
479 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>配图编辑器 - draw.io</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg">
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
|
||
|
||
body {
|
||
margin:0;
|
||
background: linear-gradient(135deg, #f3f6fa 0%, #e4edf9 100%);
|
||
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
overflow: hidden;
|
||
user-select: none;
|
||
}
|
||
|
||
.topbar {
|
||
position: fixed; top: 0; left: 0; width: 100vw; height: 54px;
|
||
background: #fff; border-bottom: 1px solid #e2e8f0; z-index: 100;
|
||
display: flex; align-items: center; justify-content: space-between; padding: 0 20px;
|
||
box-sizing: border-box;
|
||
box-shadow: 0 4px 24px 0 rgba(59,130,246,0.08), 0 1.5px 6px 0 rgba(59,130,246,0.04);
|
||
border-radius: 0 0 18px 18px;
|
||
}
|
||
|
||
.topbar-title {
|
||
display: flex;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
font-size: 1.1em;
|
||
color: #2563eb;
|
||
gap: 12px;
|
||
}
|
||
|
||
.container {
|
||
position: fixed;
|
||
top: 54px;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
display: flex;
|
||
transition: all 0.3s ease;
|
||
border-radius: 18px;
|
||
box-shadow: 0 8px 32px 0 rgba(31, 41, 55, 0.10), 0 1.5px 6px 0 rgba(59,130,246,0.06);
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.editor-container {
|
||
width: 25%;
|
||
min-width: 200px;
|
||
height: 100%;
|
||
background: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border-right: 1px solid #e0e7ef;
|
||
margin: 12px;
|
||
border-radius: 18px;
|
||
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.06);
|
||
background: linear-gradient(135deg, #fff 60%, #f3f6fa 100%);
|
||
}
|
||
|
||
.editor-header {
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: linear-gradient(90deg, #f9fafb 80%, #e0e7ef 100%);
|
||
padding: 0 16px;
|
||
font-size: 0.9em;
|
||
font-weight: 600;
|
||
color: #334155;
|
||
border-bottom: 1px dashed #e2e8f0;
|
||
flex-shrink: 0;
|
||
border-radius: 18px 18px 0 0;
|
||
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.04);
|
||
}
|
||
|
||
.splitter {
|
||
width: 6px;
|
||
background: repeating-linear-gradient(90deg, #e0e7ef, #e0e7ef 4px, #f3f6fa 4px, #f3f6fa 8px);
|
||
cursor: ew-resize;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 50;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.splitter::before {
|
||
content: "";
|
||
height: 20px;
|
||
border-left: 2px dashed #b0bfd0;
|
||
}
|
||
|
||
.drawio-container {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
border-radius: 18px;
|
||
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.06);
|
||
margin: 12px;
|
||
background: linear-gradient(135deg, #fff 60%, #f3f6fa 100%);
|
||
border: 1.5px dashed #e0e7ef;
|
||
}
|
||
|
||
.drawio-header {
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
background: linear-gradient(90deg, #f9fafb 80%, #e0e7ef 100%);
|
||
padding: 0 16px;
|
||
border-bottom: 1px dashed #e2e8f0;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 10;
|
||
border-radius: 18px 18px 0 0;
|
||
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.04);
|
||
}
|
||
|
||
.button {
|
||
background: linear-gradient(90deg, #3b82f6 60%, #60a5fa 100%);
|
||
color: #fff;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.08);
|
||
}
|
||
|
||
.button.outline {
|
||
background: #f8fafc;
|
||
color: #2563eb;
|
||
border: 1.5px dashed #2563eb;
|
||
}
|
||
|
||
.button.small {
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.panel-control {
|
||
background: transparent;
|
||
color: #64748b;
|
||
border: 1.5px dashed #cbd5e1;
|
||
padding: 4px 8px;
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
letter-spacing: 0.02em;
|
||
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.08);
|
||
}
|
||
|
||
.panel-control:hover {
|
||
color: #3b82f6;
|
||
border-color: #3b82f6;
|
||
background: rgba(59, 130, 246, 0.05);
|
||
}
|
||
|
||
.editor-textarea {
|
||
flex: 1;
|
||
border: none;
|
||
outline: none;
|
||
resize: none;
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
background: transparent;
|
||
color: #0f172a;
|
||
box-sizing: border-box;
|
||
white-space: pre;
|
||
overflow: auto;
|
||
}
|
||
|
||
#drawio-frame {
|
||
position: absolute;
|
||
top: 40px;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: calc(100% - 40px);
|
||
border: none;
|
||
}
|
||
|
||
.hide-editor .editor-container, .hide-editor .splitter {
|
||
width: 0 !important;
|
||
min-width: 0 !important;
|
||
overflow: hidden;
|
||
border-right: none;
|
||
}
|
||
|
||
.hide-editor .splitter { display: none !important; }
|
||
|
||
.hide-drawio .drawio-container {
|
||
display: none !important;
|
||
width: 0 !important;
|
||
min-width: 0 !important;
|
||
flex: 0 0 0 !important;
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
.hide-drawio .splitter { display: none !important; }
|
||
|
||
.hide-drawio .editor-container {
|
||
width: 100% !important;
|
||
flex: 1 1 auto !important;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
flex-direction: column;
|
||
}
|
||
.editor-container, .drawio-container {
|
||
width: 100% !important;
|
||
height: 50%;
|
||
}
|
||
.splitter {
|
||
width: 100%;
|
||
height: 6px;
|
||
cursor: ns-resize;
|
||
}
|
||
.splitter::before {
|
||
height: auto;
|
||
width: 20px;
|
||
border-left: none;
|
||
border-top: 2px dashed #b0bfd0;
|
||
}
|
||
.hide-editor .editor-container, .hide-drawio .drawio-container {
|
||
height: 0 !important;
|
||
}
|
||
.hide-editor .splitter, .hide-drawio .splitter { display: none !important; }
|
||
.container, .editor-container, .drawio-container {
|
||
border-radius: 0;
|
||
margin: 0;
|
||
box-shadow: none;
|
||
}
|
||
.topbar { border-radius: 0; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="topbar">
|
||
<div class="topbar-title">
|
||
<span style="font-size:1.2em;font-weight:700;letter-spacing:0.02em;">配图编辑器(draw.io)</span>
|
||
</div>
|
||
<div style="display:flex;gap:8px;align-items:center;">
|
||
<button id="toggle-layout-btn" class="button small outline">⇄ 切换布局</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container" id="main-container">
|
||
<div class="editor-container" id="editor-area">
|
||
<div class="editor-header">
|
||
<span>draw.io XML</span>
|
||
<div style="display:flex;align-items:center;gap:10px;">
|
||
<small id="xml-status" style="color:#64748b;">只读预览</small>
|
||
<button id="toggle-editor-btn" class="panel-control">隐藏</button>
|
||
</div>
|
||
</div>
|
||
<textarea id="xml-editor" class="editor-textarea" spellcheck="false"></textarea>
|
||
</div>
|
||
|
||
<div class="splitter" id="splitter"></div>
|
||
|
||
<div class="drawio-container" id="drawio-area">
|
||
<div class="drawio-header">
|
||
<button id="toggle-drawio-btn" class="panel-control">隐藏</button>
|
||
</div>
|
||
<iframe id="drawio-frame" title="draw.io editor"
|
||
src="https://embed.diagrams.net/?embed=1&proto=json&ui=min"
|
||
allow="fullscreen"></iframe>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function getQueryParam(name) {
|
||
const params = new URLSearchParams(window.location.search);
|
||
return params.get(name) || '';
|
||
}
|
||
|
||
function loadXml() {
|
||
const docId = getQueryParam('docId') || 'unknown';
|
||
const key = 'drawioData_' + docId;
|
||
const xml = window.localStorage.getItem(key) || '<mxfile></mxfile>';
|
||
return xml;
|
||
}
|
||
|
||
function initLayout() {
|
||
const container = document.getElementById('main-container');
|
||
const editor = document.getElementById('editor-area');
|
||
const splitter = document.getElementById('splitter');
|
||
const drawio = document.getElementById('drawio-area');
|
||
|
||
let isDragging = false;
|
||
|
||
splitter.addEventListener('mousedown', function(e) {
|
||
isDragging = true;
|
||
document.body.style.userSelect = 'none';
|
||
});
|
||
|
||
window.addEventListener('mousemove', function(e) {
|
||
if (!isDragging) return;
|
||
const rect = container.getBoundingClientRect();
|
||
const offsetX = e.clientX - rect.left;
|
||
const minWidth = 200;
|
||
const maxWidth = rect.width - 200;
|
||
const newWidth = Math.min(Math.max(offsetX, minWidth), maxWidth);
|
||
editor.style.width = newWidth + 'px';
|
||
});
|
||
|
||
window.addEventListener('mouseup', function() {
|
||
isDragging = false;
|
||
document.body.style.userSelect = '';
|
||
});
|
||
|
||
document.getElementById('toggle-layout-btn').addEventListener('click', function() {
|
||
if (container.classList.contains('reverse-layout')) {
|
||
container.classList.remove('reverse-layout');
|
||
container.style.flexDirection = 'row';
|
||
} else {
|
||
container.classList.add('reverse-layout');
|
||
container.style.flexDirection = 'row-reverse';
|
||
}
|
||
});
|
||
|
||
document.getElementById('toggle-editor-btn').addEventListener('click', function() {
|
||
container.classList.toggle('hide-editor');
|
||
});
|
||
|
||
document.getElementById('toggle-drawio-btn').addEventListener('click', function() {
|
||
container.classList.toggle('hide-drawio');
|
||
});
|
||
}
|
||
|
||
function initDrawioCommunication(xml) {
|
||
const iframe = document.getElementById('drawio-frame');
|
||
if (!iframe) return;
|
||
|
||
let hasReceivedInit = false; // 标记是否收到 init 事件
|
||
let initTimeout = null;
|
||
|
||
// 验证 XML 格式
|
||
function validateXml(xmlString) {
|
||
try {
|
||
const parser = new DOMParser();
|
||
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
|
||
const parserError = xmlDoc.querySelector('parsererror');
|
||
if (parserError) {
|
||
return {
|
||
valid: false,
|
||
error: parserError.textContent || '未知的 XML 解析错误'
|
||
};
|
||
}
|
||
return { valid: true };
|
||
} catch (e) {
|
||
return { valid: false, error: e.message };
|
||
}
|
||
}
|
||
|
||
// 显示错误提示
|
||
function showError(errorMsg, xmlContent) {
|
||
const container = document.querySelector('.drawio-container');
|
||
const errorHtml = `
|
||
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:80%;max-width:600px;background:#fff;padding:24px;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.1);text-align:center;z-index:1000;">
|
||
<div style="font-size:48px;margin-bottom:12px;">⚠️</div>
|
||
<div style="font-size:18px;font-weight:600;color:#dc2626;margin-bottom:12px;">配图加载失败</div>
|
||
<div style="font-size:14px;color:#64748b;margin-bottom:20px;max-height:200px;overflow-y:auto;background:#f8fafc;padding:12px;border-radius:6px;text-align:left;word-break:break-word;">${errorMsg}</div>
|
||
<div style="display:flex;gap:12px;justify-content:center;">
|
||
<button onclick="navigator.clipboard.writeText(decodeURIComponent('${encodeURIComponent(xmlContent)}'));this.textContent='✓ 已复制'" style="padding:8px 16px;background:#3b82f6;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">📋 复制 XML 到剪贴板</button>
|
||
<button onclick="window.location.reload()" style="padding:8px 16px;background:#64748b;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">🔄 刷新重试</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
container.insertAdjacentHTML('beforeend', errorHtml);
|
||
}
|
||
|
||
// 验证加载的 XML
|
||
const validation = validateXml(xml);
|
||
if (!validation.valid) {
|
||
console.error('[drawio.html] XML 验证失败:', validation.error);
|
||
showError(`XML 格式错误:${validation.error}`, xml);
|
||
return;
|
||
}
|
||
|
||
// diagrams.net embed 模式使用 postMessage + proto=json 协议
|
||
function postDrawioMessage(msg) {
|
||
if (!iframe.contentWindow) {
|
||
console.error('[drawio.html] iframe.contentWindow 不可用');
|
||
return;
|
||
}
|
||
iframe.contentWindow.postMessage(JSON.stringify(msg), '*');
|
||
console.log('[drawio.html] 发送消息到 draw.io:', msg);
|
||
}
|
||
|
||
// 监听来自 diagrams.net 的消息
|
||
window.addEventListener('message', function (evt) {
|
||
if (!evt.data) return;
|
||
let data = null;
|
||
try {
|
||
data = typeof evt.data === 'string' ? JSON.parse(evt.data) : evt.data;
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
if (!data || !data.event) return;
|
||
|
||
console.log('[drawio.html] 收到 draw.io 事件:', data);
|
||
|
||
// 等待 diagrams.net 发送 'init' 事件,表示已准备好接收消息
|
||
if (data.event === 'init') {
|
||
hasReceivedInit = true;
|
||
if (initTimeout) clearTimeout(initTimeout);
|
||
|
||
console.log('[drawio.html] draw.io 已初始化,发送 XML 数据');
|
||
// 发送 load 指令加载 XML
|
||
postDrawioMessage({
|
||
action: 'load',
|
||
autosave: 1,
|
||
xml: xml
|
||
});
|
||
} else if (data.event === 'save') {
|
||
// 处理保存事件
|
||
console.log('[drawio.html] 用户保存了图表');
|
||
const docId = getQueryParam('docId') || 'unknown';
|
||
const key = 'drawioData_' + docId;
|
||
|
||
try {
|
||
window.localStorage.setItem(key, data.xml);
|
||
// 同步更新左侧编辑器
|
||
document.getElementById('xml-editor').value = data.xml;
|
||
console.log('[drawio.html] 已保存到 localStorage:', key);
|
||
} catch (e) {
|
||
console.error('[drawio.html] 保存失败:', e);
|
||
}
|
||
} else if (data.event === 'export') {
|
||
// 处理导出事件
|
||
console.log('[drawio.html] 导出事件:', data);
|
||
} else if (data.event === 'load') {
|
||
console.log('[drawio.html] 图表加载完成');
|
||
}
|
||
});
|
||
|
||
// 超时检测:如果 5 秒内没有收到 init 事件,显示错误
|
||
initTimeout = setTimeout(() => {
|
||
if (!hasReceivedInit) {
|
||
console.error('[drawio.html] 超时:未收到 draw.io 初始化事件');
|
||
showError('draw.io 编辑器初始化超时,可能是网络问题或 embed.diagrams.net 服务不可用。', xml);
|
||
}
|
||
}, 5000);
|
||
}
|
||
|
||
window.addEventListener('DOMContentLoaded', function() {
|
||
const xml = loadXml();
|
||
document.getElementById('xml-editor').value = xml;
|
||
initLayout();
|
||
initDrawioCommunication(xml);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|