2025-05-21 14:25:26 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<transition name="slide">
|
2025-05-21 20:00:58 +08:00
|
|
|
|
<div class="side-panel">
|
2025-05-21 14:25:26 +08:00
|
|
|
|
<button @click="handleClose" class="close-btn"></button>
|
|
|
|
|
|
<el-button type="primary" @click="UpdateFileCOntent">保存修改</el-button>
|
|
|
|
|
|
<div class="content">
|
|
|
|
|
|
<h2>{{ fileName }}</h2>
|
2025-05-29 14:20:59 +08:00
|
|
|
|
<div class="editor-container">
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
ref="textareaRef"
|
|
|
|
|
|
v-model="localContent"
|
|
|
|
|
|
@input="handleInput"
|
|
|
|
|
|
@scroll="handleScroll"
|
|
|
|
|
|
spellcheck="false"
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
<pre class="highlight-container" ref="highlightRef"><code v-html="highlightedCode"></code></pre>
|
|
|
|
|
|
</div>
|
2025-05-21 14:25:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</transition>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-05-29 14:20:59 +08:00
|
|
|
|
import { ref, watch, nextTick, onMounted } from "vue";
|
2025-05-21 14:25:26 +08:00
|
|
|
|
import { UpdateFileContentService } from "@/api/file";
|
|
|
|
|
|
import { ElMessage } from "element-plus";
|
2025-05-29 14:20:59 +08:00
|
|
|
|
import hljs from 'highlight.js';
|
|
|
|
|
|
import 'highlight.js/styles/atom-one-dark.css';
|
2025-05-21 14:25:26 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
content: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: "",
|
|
|
|
|
|
},
|
|
|
|
|
|
show: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
fileName: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: "编辑代码",
|
|
|
|
|
|
},
|
|
|
|
|
|
fileID: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
(e: "close"): void;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const textareaRef = ref<HTMLTextAreaElement | null>(null);
|
2025-05-29 14:20:59 +08:00
|
|
|
|
const highlightRef = ref<HTMLElement | null>(null);
|
2025-05-21 14:25:26 +08:00
|
|
|
|
const localContent = ref(props.content);
|
2025-05-29 14:20:59 +08:00
|
|
|
|
const highlightedCode = ref('');
|
|
|
|
|
|
|
|
|
|
|
|
// 根据文件名判断语言
|
|
|
|
|
|
const detectLanguage = (fileName: string) => {
|
|
|
|
|
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
const languageMap: Record<string, string> = {
|
|
|
|
|
|
'js': 'javascript',
|
|
|
|
|
|
'jsx': 'javascript',
|
|
|
|
|
|
'ts': 'typescript',
|
|
|
|
|
|
'tsx': 'typescript',
|
|
|
|
|
|
'html': 'html',
|
|
|
|
|
|
'css': 'css',
|
|
|
|
|
|
'scss': 'scss',
|
|
|
|
|
|
'less': 'less',
|
|
|
|
|
|
'py': 'python',
|
|
|
|
|
|
'java': 'java',
|
|
|
|
|
|
'php': 'php',
|
|
|
|
|
|
'go': 'go',
|
|
|
|
|
|
'rb': 'ruby',
|
|
|
|
|
|
'rs': 'rust',
|
|
|
|
|
|
'c': 'c',
|
|
|
|
|
|
'cpp': 'cpp',
|
|
|
|
|
|
'cs': 'csharp',
|
|
|
|
|
|
'json': 'json',
|
|
|
|
|
|
'md': 'markdown',
|
|
|
|
|
|
'xml': 'xml',
|
|
|
|
|
|
'yaml': 'yaml',
|
|
|
|
|
|
'yml': 'yaml',
|
|
|
|
|
|
'sh': 'bash',
|
|
|
|
|
|
'bash': 'bash',
|
|
|
|
|
|
'sql': 'sql',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return extension && languageMap[extension] ? languageMap[extension] : 'plaintext';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 高亮代码
|
|
|
|
|
|
const highlightCode = () => {
|
|
|
|
|
|
if (!localContent.value) {
|
|
|
|
|
|
highlightedCode.value = '';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const language = detectLanguage(props.fileName);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (language !== 'plaintext') {
|
|
|
|
|
|
const highlighted = hljs.highlight(localContent.value, { language });
|
|
|
|
|
|
highlightedCode.value = highlighted.value;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果无法检测语言,尝试自动检测
|
|
|
|
|
|
const highlighted = hljs.highlightAuto(localContent.value);
|
|
|
|
|
|
highlightedCode.value = highlighted.value;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Highlight error:', error);
|
|
|
|
|
|
// 降级处理:转义HTML并保留换行
|
|
|
|
|
|
highlightedCode.value = localContent.value
|
|
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
|
|
.replace(/</g, '<')
|
|
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
|
.replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理输入事件
|
|
|
|
|
|
const handleInput = () => {
|
|
|
|
|
|
highlightCode();
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (textareaRef.value && highlightRef.value) {
|
|
|
|
|
|
// 同步滚动位置
|
|
|
|
|
|
highlightRef.value.scrollTop = textareaRef.value.scrollTop;
|
|
|
|
|
|
highlightRef.value.scrollLeft = textareaRef.value.scrollLeft;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理滚动事件
|
|
|
|
|
|
const handleScroll = () => {
|
|
|
|
|
|
if (textareaRef.value && highlightRef.value) {
|
|
|
|
|
|
highlightRef.value.scrollTop = textareaRef.value.scrollTop;
|
|
|
|
|
|
highlightRef.value.scrollLeft = textareaRef.value.scrollLeft;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-05-21 14:25:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 当props.content变化时同步到本地
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.content,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
localContent.value = newVal;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
highlightCode();
|
2025-05-21 14:25:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 当面板显示时自动聚焦到textarea
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.show,
|
|
|
|
|
|
async (newVal) => {
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
textareaRef.value?.focus();
|
2025-05-29 14:20:59 +08:00
|
|
|
|
highlightCode();
|
2025-05-21 14:25:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-05-29 14:20:59 +08:00
|
|
|
|
// 初始化
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
highlightCode();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-05-21 14:25:26 +08:00
|
|
|
|
const handleClose = () => {
|
|
|
|
|
|
emit("close");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const UpdateFileCOntent = async () => {
|
|
|
|
|
|
let req = {
|
|
|
|
|
|
token: localStorage.getItem("token"),
|
|
|
|
|
|
file_id: props.fileID,
|
|
|
|
|
|
file_content: localContent.value,
|
|
|
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
|
|
|
let result = await UpdateFileContentService(req);
|
|
|
|
|
|
if (result["code"] === 0) {
|
|
|
|
|
|
ElMessage.success("修改成功");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error("修改失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="css">
|
|
|
|
|
|
.side-panel {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
width: 50%;
|
|
|
|
|
|
height: 100%;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
background-color: #282c34;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
z-index: 1001;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
|
2025-05-21 14:25:26 +08:00
|
|
|
|
padding: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
color: #abb2bf;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
right: 10px;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
width: 30px;
|
|
|
|
|
|
height: 30px;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-29 14:20:59 +08:00
|
|
|
|
.close-btn::before,
|
|
|
|
|
|
.close-btn::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
background-color: #abb2bf;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn::before {
|
|
|
|
|
|
transform: translate(-50%, -50%) rotate(45deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn::after {
|
|
|
|
|
|
transform: translate(-50%, -50%) rotate(-45deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.close-btn:hover::before,
|
|
|
|
|
|
.close-btn:hover::after {
|
|
|
|
|
|
background-color: #61afef;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-29 14:20:59 +08:00
|
|
|
|
.content h2 {
|
|
|
|
|
|
color: #e5e5e5;
|
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-container {
|
|
|
|
|
|
position: relative;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
flex: 1;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background-color: #282c34;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-container textarea {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
width: 100%;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border: 1px solid #3e4451;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
resize: none;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.5;
|
2025-05-29 14:20:59 +08:00
|
|
|
|
color: transparent;
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
caret-color: #61afef;
|
|
|
|
|
|
z-index: 2;
|
|
|
|
|
|
white-space: pre;
|
|
|
|
|
|
overflow: auto;
|
2025-05-21 14:25:26 +08:00
|
|
|
|
tab-size: 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-29 14:20:59 +08:00
|
|
|
|
.editor-container textarea:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: #61afef;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.highlight-container {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
white-space: pre;
|
|
|
|
|
|
tab-size: 2;
|
|
|
|
|
|
background-color: #282c34;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.highlight-container code {
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 滚动条样式 */
|
|
|
|
|
|
.editor-container textarea::-webkit-scrollbar,
|
|
|
|
|
|
.highlight-container::-webkit-scrollbar {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-container textarea::-webkit-scrollbar-track,
|
|
|
|
|
|
.highlight-container::-webkit-scrollbar-track {
|
|
|
|
|
|
background: #21252b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-container textarea::-webkit-scrollbar-thumb,
|
|
|
|
|
|
.highlight-container::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #3e4451;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-container textarea::-webkit-scrollbar-thumb:hover,
|
|
|
|
|
|
.highlight-container::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: #4b5363;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 保存按钮样式 */
|
|
|
|
|
|
:deep(.el-button--primary) {
|
|
|
|
|
|
background-color: #61afef;
|
|
|
|
|
|
border-color: #61afef;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-button--primary:hover) {
|
|
|
|
|
|
background-color: #528bbc;
|
|
|
|
|
|
border-color: #528bbc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-21 14:25:26 +08:00
|
|
|
|
.slide-enter-active,
|
|
|
|
|
|
.slide-leave-active {
|
|
|
|
|
|
transition: transform 0.3s ease;
|
|
|
|
|
|
}
|
2025-05-29 14:20:59 +08:00
|
|
|
|
|
2025-05-21 14:25:26 +08:00
|
|
|
|
.slide-enter-from,
|
|
|
|
|
|
.slide-leave-to {
|
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
|
}
|
2025-05-29 14:20:59 +08:00
|
|
|
|
</style>
|