efca70ecb2
首次推送
376 lines
17 KiB
PHP
376 lines
17 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>正在编辑 - <?php echo isset($_GET['file']) ? htmlspecialchars(basename($_GET['file'])) : 'N/A'; ?></title>
|
|
<link rel="stylesheet" href="/assets/css/ui.css?t=<?php echo time();?>">
|
|
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100%;
|
|
width: 100%;
|
|
overflow: hidden; /* 防止出现滚动条 */
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background-color: #f8f9fa;
|
|
}
|
|
.editor-page-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh; /* 占满整个视口高度 */
|
|
}
|
|
.editor-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 15px;
|
|
background-color: #fff;
|
|
border-bottom: 1px solid #dee2e6;
|
|
flex-shrink: 0; /* 防止头部被压缩 */
|
|
}
|
|
.file-info .file-name {
|
|
font-weight: 600;
|
|
color: #343a40;
|
|
}
|
|
.file-info .full-path {
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
margin-top: 2px;
|
|
}
|
|
#editor-container {
|
|
flex-grow: 1; /* 编辑器占满所有剩余空间 */
|
|
position: relative;
|
|
}
|
|
.checkbox-group { margin: 0; } /* 修正弹窗内边距 */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="editor-page-wrapper">
|
|
<header class="editor-header">
|
|
<div class="file-info">
|
|
<div class="file-path">
|
|
<span class="file-icon">📄</span>
|
|
<span class="file-name"><?php echo isset($_GET['file']) ? htmlspecialchars(basename($_GET['file'])) : '未选择文件'; ?></span>
|
|
</div>
|
|
<?php if(isset($_GET['file'])): ?>
|
|
<div class="full-path"><?php echo htmlspecialchars($_GET['file']); ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="header-actions">
|
|
<div class="btn-group">
|
|
<button id="vscodeBtn" class="btn secondary-btn" title="在VSCode中打开">VSCode</button>
|
|
<button id="fullscreenBtn" class="btn secondary-btn">全屏</button>
|
|
<button id="settingsBtn" class="btn secondary-btn">设置</button>
|
|
<button id="saveBtn" class="btn primary-btn">保存</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="editor-container"></main>
|
|
</div>
|
|
|
|
<script id="settings-modal-template" type="text/x-handlebars-template">
|
|
<div class="form-group">
|
|
<label for="theme-switcher">编辑器主题</label>
|
|
<select id="theme-switcher" class="form-control"></select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="font-size-switcher">字体大小</label>
|
|
<select id="font-size-switcher" class="form-control">
|
|
<option value="12">12px</option>
|
|
<option value="14" selected>14px</option>
|
|
<option value="16">16px</option>
|
|
<option value="18">18px</option>
|
|
<option value="20">20px</option>
|
|
<option value="24">24px</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="soft-wrap-toggle">自动换行</label>
|
|
<select id="soft-wrap-toggle" class="form-control">
|
|
<option value="true">开启</option>
|
|
<option value="false">关闭</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="tab-size-switcher">Tab 宽度</label>
|
|
<select id="tab-size-switcher" class="form-control">
|
|
<option value="2">2空格</option>
|
|
<option value="4">4空格</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="keybinding-switcher">键盘快捷键方案</label>
|
|
<select id="keybinding-switcher" class="form-control">
|
|
<option value="">默认 (Ace)</option>
|
|
<option value="ace/keyboard/vscode">VSCode</option>
|
|
<option value="ace/keyboard/vim">Vim</option>
|
|
<option value="ace/keyboard/emacs">Emacs</option>
|
|
<option value="ace/keyboard/sublime">Sublime</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group checkbox-group">
|
|
<input type="checkbox" id="line-number-toggle" style="width: auto;">
|
|
<label for="line-number-toggle">隐藏行号</label>
|
|
</div>
|
|
<div class="form-group checkbox-group">
|
|
<input type="checkbox" id="highlight-line-toggle" style="width: auto;">
|
|
<label for="highlight-line-toggle">高亮当前行</label>
|
|
</div>
|
|
<div class="form-group checkbox-group">
|
|
<input type="checkbox" id="show-indent-guides-toggle" style="width: auto;">
|
|
<label for="show-indent-guides-toggle">显示缩进向导</label>
|
|
</div>
|
|
<div class="form-group checkbox-group">
|
|
<input type="checkbox" id="live-autocomplete-toggle" style="width: auto;">
|
|
<label for="live-autocomplete-toggle">实时自动补全</label>
|
|
</div>
|
|
<div class="form-group checkbox-group">
|
|
<input type="checkbox" id="enable-snippets-toggle" style="width: auto;">
|
|
<label for="enable-snippets-toggle">启用代码片段</label>
|
|
</div>
|
|
</script>
|
|
|
|
<div class="toast-container"></div>
|
|
|
|
<script src="/assets/js/ace/ace.js"></script>
|
|
<script src="/assets/js/ace/ext-modelist.js"></script>
|
|
<script src="/assets/js/ace/ext-language_tools.js"></script>
|
|
<script src="/assets/js/handlebars.min.js"></script>
|
|
<script src="/assets/js/utils.js?t=<?php echo time();?>"></script>
|
|
|
|
<script>
|
|
const fileContentFromServer = <?php echo $file_content_for_js ?? '""'; ?>;
|
|
const filePathFromServer = <?php echo $file_path_for_js ?? '""'; ?>;
|
|
const fileFullPath = '<?php echo $file_path ?? '""'; ?>';
|
|
|
|
const aceThemes = {
|
|
"亮色主题 (Light)": [
|
|
{ name: "Chrome", path: "ace/theme/chrome" },
|
|
{ name: "GitHub", path: "ace/theme/github" },
|
|
{ name: "Solarized Light", path: "ace/theme/solarized_light" },
|
|
{ name: "Xcode", path: "ace/theme/xcode" },
|
|
],
|
|
"暗色主题 (Dark)": [
|
|
{ name: "Monokai", path: "ace/theme/monokai" },
|
|
{ name: "Dracula", path: "ace/theme/dracula" },
|
|
{ name: "Nord Dark", path: "ace/theme/nord_dark" },
|
|
{ name: "Solarized Dark", path: "ace/theme/solarized_dark" },
|
|
{ name: "Twilight", path: "ace/theme/twilight" },
|
|
]
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
let settingsModal = null;
|
|
const editor = ace.edit("editor-container");
|
|
|
|
/**
|
|
* ---------------------------------------------------------------------
|
|
* 编辑器初始化
|
|
* ---------------------------------------------------------------------
|
|
*/
|
|
editor.setShowPrintMargin(false);
|
|
const modelist = ace.require("ace/ext/modelist");
|
|
const mode = modelist.getModeForPath(filePathFromServer).mode;
|
|
editor.session.setMode(mode);
|
|
|
|
if (fileContentFromServer && !fileContentFromServer.startsWith('错误:')) {
|
|
editor.setValue(fileContentFromServer, -1);
|
|
} else if (fileContentFromServer) {
|
|
editor.setValue(`// ${fileContentFromServer}`);
|
|
}
|
|
|
|
const savedSettings = {
|
|
theme: localStorage.getItem('ace_editor_theme') || 'ace/theme/monokai',
|
|
fontSize: parseInt(localStorage.getItem('ace_editor_fontSize') || '14', 10),
|
|
showGutter: localStorage.getItem('ace_editor_showGutter') !== 'false',
|
|
softWrap: localStorage.getItem('ace_editor_softWrap') === 'true',
|
|
tabSize: parseInt(localStorage.getItem('ace_editor_tabSize') || '4', 10),
|
|
highlightLine: localStorage.getItem('ace_editor_highlightLine') !== 'false',
|
|
keybinding: localStorage.getItem('ace_editor_keybinding') || '',
|
|
liveAutocomplete: localStorage.getItem('ace_editor_liveAutocomplete') === 'true',
|
|
enableSnippets: localStorage.getItem('ace_editor_enableSnippets') !== 'false',
|
|
showIndentGuides: localStorage.getItem('ace_editor_showIndentGuides') !== 'false'
|
|
};
|
|
|
|
editor.setTheme(savedSettings.theme);
|
|
editor.setFontSize(savedSettings.fontSize);
|
|
editor.renderer.setShowGutter(savedSettings.showGutter);
|
|
editor.session.setUseWrapMode(savedSettings.softWrap);
|
|
editor.session.setTabSize(savedSettings.tabSize);
|
|
editor.setHighlightActiveLine(savedSettings.highlightLine);
|
|
editor.setKeyboardHandler(savedSettings.keybinding || null);
|
|
editor.setDisplayIndentGuides(savedSettings.showIndentGuides);
|
|
editor.setOptions({
|
|
enableBasicAutocompletion: true,
|
|
enableLiveAutocompletion: savedSettings.liveAutocomplete,
|
|
enableSnippets: savedSettings.enableSnippets
|
|
});
|
|
|
|
|
|
/**
|
|
* ---------------------------------------------------------------------
|
|
* 页面主要按钮事件绑定
|
|
* ---------------------------------------------------------------------
|
|
*/
|
|
document.getElementById('saveBtn').addEventListener('click', () => {
|
|
if (!filePathFromServer) {
|
|
showToast('文件路径未知,无法保存。', 'error');
|
|
return;
|
|
}
|
|
|
|
const newContent = editor.getValue();
|
|
const formData = new FormData();
|
|
formData.append('filePath', filePathFromServer);
|
|
formData.append('fileContent', newContent);
|
|
|
|
showToast('正在保存...', 'info');
|
|
fetch('/index.php/Edit/save', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(res => res.json())
|
|
.then(result => {
|
|
if (result.success) {
|
|
showToast(result.message, 'success');
|
|
} else {
|
|
throw new Error(result.message);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
showToast(`保存失败: ${err.message}`, 'error');
|
|
});
|
|
});
|
|
|
|
document.getElementById('vscodeBtn')?.addEventListener('click', () => {
|
|
openInVSCode(fileFullPath);
|
|
});
|
|
|
|
document.getElementById('settingsBtn').addEventListener('click', () => {
|
|
if (!settingsModal) {
|
|
const template = Handlebars.compile(document.getElementById('settings-modal-template').innerHTML);
|
|
settingsModal = new Modal({
|
|
id: 'editor-settings-modal',
|
|
title: '编辑器设置',
|
|
content: template(),
|
|
footer: `
|
|
<button type="button" class="btn secondary-btn" data-close-modal>取消</button>
|
|
<button type="button" class="btn primary-btn" id="apply-settings">应用</button>
|
|
`
|
|
});
|
|
|
|
const modalBody = settingsModal.getBodyElement();
|
|
const themeSelect = modalBody.querySelector('#theme-switcher');
|
|
|
|
themeSelect.innerHTML = '';
|
|
for (const groupName in aceThemes) {
|
|
const optgroup = document.createElement('optgroup');
|
|
optgroup.label = groupName;
|
|
aceThemes[groupName].forEach(theme => {
|
|
const option = new Option(theme.name, theme.path);
|
|
optgroup.appendChild(option);
|
|
});
|
|
themeSelect.appendChild(optgroup);
|
|
}
|
|
|
|
settingsModal.getFooterElement().querySelector('#apply-settings').addEventListener('click', () => {
|
|
const newSettings = {
|
|
theme: modalBody.querySelector('#theme-switcher').value,
|
|
fontSize: parseInt(modalBody.querySelector('#font-size-switcher').value, 10),
|
|
showGutter: !modalBody.querySelector('#line-number-toggle').checked,
|
|
softWrap: modalBody.querySelector('#soft-wrap-toggle').value === 'true',
|
|
tabSize: parseInt(modalBody.querySelector('#tab-size-switcher').value, 10),
|
|
highlightLine: modalBody.querySelector('#highlight-line-toggle').checked,
|
|
keybinding: modalBody.querySelector('#keybinding-switcher').value,
|
|
liveAutocomplete: modalBody.querySelector('#live-autocomplete-toggle').checked,
|
|
enableSnippets: modalBody.querySelector('#enable-snippets-toggle').checked,
|
|
showIndentGuides: modalBody.querySelector('#show-indent-guides-toggle').checked
|
|
};
|
|
|
|
editor.setTheme(newSettings.theme);
|
|
editor.setFontSize(newSettings.fontSize);
|
|
editor.renderer.setShowGutter(newSettings.showGutter);
|
|
editor.session.setUseWrapMode(newSettings.softWrap);
|
|
editor.session.setTabSize(newSettings.tabSize);
|
|
editor.setHighlightActiveLine(newSettings.highlightLine);
|
|
editor.setKeyboardHandler(newSettings.keybinding || null);
|
|
editor.setDisplayIndentGuides(newSettings.showIndentGuides);
|
|
editor.setOptions({
|
|
enableBasicAutocompletion: true,
|
|
enableLiveAutocompletion: newSettings.liveAutocomplete,
|
|
enableSnippets: newSettings.enableSnippets
|
|
});
|
|
|
|
for (const key in newSettings) {
|
|
localStorage.setItem(`ace_editor_${key}`, newSettings[key]);
|
|
}
|
|
|
|
showToast('设置已应用', 'success');
|
|
settingsModal.close();
|
|
});
|
|
|
|
settingsModal.getFooterElement().querySelector('[data-close-modal]').addEventListener('click', () => settingsModal.close());
|
|
}
|
|
|
|
const modalBody = settingsModal.getBodyElement();
|
|
modalBody.querySelector('#theme-switcher').value = localStorage.getItem('ace_editor_theme') || 'ace/theme/monokai';
|
|
modalBody.querySelector('#font-size-switcher').value = localStorage.getItem('ace_editor_fontSize') || '14';
|
|
modalBody.querySelector('#line-number-toggle').checked = localStorage.getItem('ace_editor_showGutter') === 'false';
|
|
modalBody.querySelector('#soft-wrap-toggle').value = localStorage.getItem('ace_editor_softWrap') || 'false';
|
|
modalBody.querySelector('#tab-size-switcher').value = localStorage.getItem('ace_editor_tabSize') || '4';
|
|
modalBody.querySelector('#highlight-line-toggle').checked = localStorage.getItem('ace_editor_highlightLine') !== 'false';
|
|
modalBody.querySelector('#keybinding-switcher').value = localStorage.getItem('ace_editor_keybinding') || '';
|
|
modalBody.querySelector('#live-autocomplete-toggle').checked = localStorage.getItem('ace_editor_liveAutocomplete') === 'true';
|
|
modalBody.querySelector('#enable-snippets-toggle').checked = localStorage.getItem('ace_editor_enableSnippets') !== 'false';
|
|
modalBody.querySelector('#show-indent-guides-toggle').checked = localStorage.getItem('ace_editor_showIndentGuides') !== 'false';
|
|
|
|
settingsModal.open();
|
|
});
|
|
|
|
const fullscreenButton = document.getElementById('fullscreenBtn');
|
|
if (fullscreenButton) {
|
|
function toggleFullScreen() {
|
|
const doc = document;
|
|
const docEl = doc.documentElement;
|
|
const requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullscreen || docEl.msRequestFullscreen;
|
|
const cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
|
|
|
|
if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
|
|
if (requestFullScreen) {
|
|
requestFullScreen.call(docEl);
|
|
}
|
|
} else {
|
|
if (cancelFullScreen) {
|
|
cancelFullScreen.call(doc);
|
|
}
|
|
}
|
|
}
|
|
fullscreenButton.addEventListener('click', toggleFullScreen);
|
|
document.addEventListener('fullscreenchange', () => {
|
|
const isFullScreen = !!document.fullscreenElement;
|
|
fullscreenButton.textContent = isFullScreen ? '退出全屏' : '全屏';
|
|
});
|
|
}
|
|
|
|
function openInVSCode(fullPath) {
|
|
if (!fullPath) {
|
|
alert('无效的文件路径');
|
|
return;
|
|
}
|
|
const vscodeUri = `vscode://file${fullPath}`;
|
|
window.location.href = vscodeUri;
|
|
setTimeout(() => {
|
|
if (!document.hidden) {
|
|
alert(`无法自动打开VSCode,请确认:\n\n1. 您已在本地安装了VSCode。\n2. 文件路径对您的本地环境是可访问的。\n\n路径: ${fullPath}`);
|
|
}
|
|
}, 2000);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|