Files
suanfa/src/components/CodeViewer.vue
T

181 lines
3.8 KiB
Vue
Raw Normal View History

2026-06-15 09:00:38 +08:00
<template>
<div class="code-viewer">
<div class="code-toolbar">
<div class="file-info">
<span class="file-icon">📄</span>
<span class="file-name">{{ fileName }}</span>
<span v-if="fileLabel" class="file-label">{{ fileLabel }}</span>
</div>
<div class="toolbar-actions">
<span v-if="description" class="file-desc-tip" :title="description"></span>
<button class="copy-btn" @click="copyCode" :title="copied ? '已复制!' : '复制代码'">
{{ copied ? '✅' : '📋' }}
</button>
</div>
</div>
<div class="code-content" ref="codeRef">
<pre><code v-html="highlightedCode"></code></pre>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">加载中...</div>
</div>
<div v-if="error" class="error-overlay">
<p> {{ error }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { loadCodeFile, highlightC } from '../utils/codeUtils.js'
const props = defineProps({
filePath: String,
fileName: String,
fileLabel: String,
description: String
})
const codeContent = ref('')
const loading = ref(false)
const error = ref('')
const copied = ref(false)
const highlightedCode = computed(() => {
if (!codeContent.value) return ''
return highlightC(codeContent.value)
})
async function loadCode() {
if (!props.filePath) return
loading.value = true
error.value = ''
codeContent.value = ''
try {
codeContent.value = await loadCodeFile(props.filePath)
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
watch(() => props.filePath, loadCode, { immediate: true })
async function copyCode() {
try {
await navigator.clipboard.writeText(codeContent.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
} catch {
// fallback
const textarea = document.createElement('textarea')
textarea.value = codeContent.value
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
}
}
</script>
<style scoped>
.code-viewer {
background: var(--code-bg);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border-color);
margin-bottom: 20px;
}
.code-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: var(--code-header-bg);
border-bottom: 1px solid var(--border-color);
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
}
.file-icon {
font-size: 16px;
}
.file-name {
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
font-size: 14px;
font-weight: 600;
color: var(--primary-color);
}
.file-label {
font-size: 12px;
color: var(--text-tertiary);
background: var(--tag-bg);
padding: 2px 8px;
border-radius: 4px;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.file-desc-tip {
cursor: help;
font-size: 16px;
}
.copy-btn {
background: var(--hover-bg);
border: 1px solid var(--border-color);
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.copy-btn:hover {
background: var(--active-bg);
}
.code-content {
position: relative;
overflow-x: auto;
}
.code-content pre {
padding: 16px;
margin: 0;
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--code-text);
}
.code-content code {
font-family: inherit;
}
.loading-overlay,
.error-overlay {
padding: 20px;
text-align: center;
color: var(--text-tertiary);
}
.error-overlay {
color: #e74c3c;
}
</style>