sawAdmin/src/views/system/slide-code-edit.vue

358 lines
7.3 KiB
Vue
Raw Normal View History

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>
<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">
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";
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);
const highlightRef = ref<HTMLElement | null>(null);
2025-05-21 14:25:26 +08:00
const localContent = ref(props.content);
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
};
// 处理输入事件
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;
highlightCode();
2025-05-21 14:25:26 +08:00
}
);
// 当面板显示时自动聚焦到textarea
watch(
() => props.show,
async (newVal) => {
if (newVal) {
await nextTick();
textareaRef.value?.focus();
highlightCode();
2025-05-21 14:25:26 +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%;
background-color: #282c34;
2025-05-21 14:25:26 +08:00
z-index: 1001;
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;
color: #abb2bf;
2025-05-21 14:25:26 +08:00
}
.close-btn {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
2025-05-21 14:25:26 +08:00
background: none;
border: none;
cursor: pointer;
z-index: 1;
}
.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;
}
.content h2 {
color: #e5e5e5;
margin-bottom: 15px;
}
.editor-container {
position: relative;
2025-05-21 14:25:26 +08:00
flex: 1;
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%;
height: 100%;
padding: 12px;
border: 1px solid #3e4451;
2025-05-21 14:25:26 +08:00
border-radius: 4px;
resize: none;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
2025-05-21 14:25:26 +08:00
font-size: 14px;
line-height: 1.5;
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;
}
.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-21 14:25:26 +08:00
.slide-enter-from,
.slide-leave-to {
transform: translateX(100%);
}
</style>