修改了前端样式,现在可以调整输入输出框的大小

This commit is contained in:
lhx-666-cool
2026-03-05 15:27:34 +08:00
parent fcda3e83ef
commit 669973460f

View File

@@ -1,69 +1,126 @@
<template>
<div class="bg-[#282c34] text-md flex justify-center items-center w-[100vw] flex-1 min-h-0">
<div class="w-3/4 h-3/4 bg-[#282c34] text-sm scale-133 flex">
<div class="w-3/4 h-full min-w-0 ">
<div ref="editorContainer" class="w-full h-full">
<div class="code-page">
<div class="workbench" :style="workbenchStyle">
<div
ref="workspaceRef"
class="workspace"
:class="panelPosition === 'right' ? 'workspace-right' : 'workspace-bottom'"
>
<div class="editor-pane">
<div ref="editorContainer" class="editor-surface"></div>
</div>
<div
class="splitter"
:class="[
panelPosition === 'right' ? 'splitter-vertical' : 'splitter-horizontal',
{ 'is-active': isResizing }
]"
@pointerdown="startResize"
></div>
<div
ref="ioPaneRef"
class="io-pane"
:class="panelPosition === 'bottom' ? 'io-pane-horizontal' : 'io-pane-vertical'"
:style="ioPaneStyle"
>
<div class="io-section" :style="ioInputStyle">
<div class="io-header">
<span>输入</span>
<div class="header-spacer"></div>
<button class="action-btn" type="button" @click="handleRun(false)">
<img :src="saveIcon" alt="save" />
</button>
<button class="action-btn" type="button" @click="handleRun(true)">
<img :src="runIcon" alt="run" />
</button>
</div>
<div class="io-content">
<textarea
id="stdin"
name="stdin"
class="input-area"
placeholder="请输入测试样例"
v-model="stdin"
></textarea>
</div>
</div>
<div
class="io-splitter"
:class="[
panelPosition === 'bottom' ? 'io-splitter-vertical' : 'io-splitter-horizontal',
{ 'is-active': isIoResizing }
]"
@pointerdown="startIoResize"
></div>
<div class="io-section" :style="ioOutputStyle">
<div class="io-header">
<span>输出</span>
</div>
<div class="io-content output-area">
<div v-show="time !== 0" class="run-time">运行时间{{ time }} ms</div>
<pre class="output-text">{{ status !== 'completed' ? status : (stdout === '' ? 'Empty' : stdout) }}</pre>
<pre v-show="stderr !== ''" class="output-text output-error">{{ stderr }}</pre>
<pre v-show="log !== ''" class="output-text output-error">{{ log }}</pre>
</div>
</div>
</div>
</div>
<div class="flex-1 bg-stone-900 min-w-0 text-xs">
<div class="h-1/2 text-white flex flex-col">
<div class="bg-stone-700 p-1 px-2 flex items-center">
<div>
输入
</div>
<div class="flex-1 min-w-0">
</div>
<div class="h-4 w-4 mr-2" @click="handleRun(false)">
<img :src="saveIcon" alt="run">
</div>
<div class="h-4 w-4" @click="handleRun(true)">
<img :src="runIcon" alt="run">
</div>
</div>
<div class="flex-1 min-h-0 ">
<div class="w-full h-full overflow-hidden">
<textarea name="stdin" id="stdin"
class="w-full h-full resize-none outline-none border-0 p-2 whitespace-pre-wrap" placeholder="请输入测试样例"
v-model="stdin"></textarea>
</div>
</div>
</div>
<div class="h-1/2 text-white flex flex-col">
<div class="bg-stone-700 p-1 px-2">
输出
</div>
<div class="flex-1 min-h-0 overflow-auto">
<div class="w-full h-full p-2">
<!-- {{ stdout === '' ? 'Empty' : stdout }} -->
<div v-show="time !== 0">
运行时间{{ time }} ms
</div>
<pre class="whitespace-pre-wrap break-words">{{ status !== 'completed' ? status : (stdout === '' ? 'Empty' : stdout) }}</pre>
<pre v-show="stderr !== ''" class="text-red-500 whitespace-pre-wrap break-words">{{ stderr }}</pre>
<pre v-show="log !== ''" class="text-red-500 whitespace-pre-wrap break-words">{{ log }}</pre>
</div>
</div>
</div>
</div>
<aside class="right-toolbar">
<button
class="tool-btn"
:class="{ active: panelPosition === 'right' }"
type="button"
@click="setPanelPosition('right')"
title="输出区在右侧"
>
</button>
<button
class="tool-btn"
:class="{ active: panelPosition === 'bottom' }"
type="button"
@click="setPanelPosition('bottom')"
title="输出区在下方"
>
</button>
<div class="toolbar-divider"></div>
<button class="tool-btn" type="button" @click="adjustFontScale(ZOOM_STEP)" title="放大字体">
A+
</button>
<button class="tool-btn" type="button" @click="adjustFontScale(-ZOOM_STEP)" title="缩小字体">
A-
</button>
<button class="tool-btn" type="button" @click="resetFontScale" title="重置字体">
100
</button>
<div class="zoom-label">{{ fontScale }}%</div>
</aside>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, defineProps, onUnmounted } from 'vue'
import { ref, onMounted, defineProps, onUnmounted, computed, watch, nextTick } from 'vue'
import { EditorView } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { Compartment, EditorState } from '@codemirror/state'
import { cpp } from '@codemirror/lang-cpp'
import { basicSetup } from 'codemirror'
import { languageServer } from 'codemirror-languageserver';
import { keymap } from '@codemirror/view'
import { indentMore, indentLess, insertTab, indentWithTab } from "@codemirror/commands";
import { oneDark } from "@codemirror/theme-one-dark";
import { indentMore, indentLess, insertTab } from "@codemirror/commands";
import { oneDark, oneDarkHighlightStyle } from "@codemirror/theme-one-dark";
import runIcon from '../assets/run.svg'
import saveIcon from '../assets/save.svg'
import { useRouter } from 'vue-router'
import { indentUnit } from '@codemirror/language';
import { defaultHighlightStyle, indentUnit, syntaxHighlighting } from '@codemirror/language';
const router = useRouter()
@@ -82,6 +139,27 @@ const time = ref(0)
const log = ref('')
const isLoading = ref(false)
const editorView = ref<EditorView | null>(null)
const workspaceRef = ref<HTMLElement | null>(null)
const ioPaneRef = ref<HTMLElement | null>(null)
const isResizing = ref(false)
const isIoResizing = ref(false)
type PanelPosition = 'right' | 'bottom'
const panelPosition = ref<PanelPosition>('right')
const panelSize = ref(36)
const ioSplit = ref(50)
const fontScale = ref(100)
const ZOOM_STEP = 5
const ZOOM_MIN = 70
const ZOOM_MAX = 180
const PANEL_POSITION_KEY = 'code_panel_position'
const PANEL_SIZE_KEY = 'code_panel_size'
const IO_SPLIT_KEY = 'code_io_split_ratio'
const FONT_SCALE_KEY = 'code_font_scale'
const editorThemeCompartment = new Compartment()
const serverUri = window.CONFIG.LSP_SERVER !== '__LSP_SERVER_URL_PLACEHOLDER__' ? window.CONFIG.LSP_SERVER : import.meta.env.VITE_LSP_SERVER;
const backend = window.CONFIG.BACKEND !== '__BACKEND_URL_PLACEHOLDER__' ? window.CONFIG.BACKEND : import.meta.env.VITE_BACKEND;
@@ -94,8 +172,238 @@ const ls = languageServer({
});
const editorContainer = ref<HTMLElement | null>(null)
const workbenchStyle = computed(() => ({
'--ui-font-size': `${Math.max(11, Math.round((16 * fontScale.value) / 100))}px`,
} as Record<string, string>))
const ioPaneStyle = computed(() => ({
flexBasis: `${panelSize.value}%`,
}))
const ioInputStyle = computed(() => ({
flex: `0 0 ${ioSplit.value}%`,
}))
const ioOutputStyle = computed(() => ({
flex: `0 0 ${100 - ioSplit.value}%`,
}))
function clampPanelSize(value: number, position: PanelPosition = panelPosition.value) {
const min = position === 'right' ? 12 : 14
const max = position === 'right' ? 56 : 70
return Math.min(max, Math.max(min, value))
}
function clampIoSplit(value: number) {
return Math.min(80, Math.max(20, value))
}
function clampFontScale(value: number) {
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, value))
}
function createEditorTheme(scale: number) {
const fontPx = Math.max(12, Math.round((16 * scale) / 100))
return EditorView.theme({
"&": { height: "100%" },
".cm-scroller": { overflow: "auto" },
".cm-content, .cm-gutters": {
fontSize: `${fontPx}px`,
lineHeight: '1.6',
fontFamily: '"JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace'
}
})
}
function requestEditorMeasure() {
if (!editorView.value) {
return
}
editorView.value.requestMeasure()
}
function applyEditorTheme(scale: number = fontScale.value) {
if (!editorView.value) {
return
}
editorView.value.dispatch({
effects: editorThemeCompartment.reconfigure(createEditorTheme(scale))
})
}
function setPanelPosition(position: PanelPosition) {
if (panelPosition.value === position) {
return
}
panelPosition.value = position
}
function setFontScale(value: number) {
const next = clampFontScale(value)
if (next === fontScale.value) {
return
}
fontScale.value = next
}
function adjustFontScale(delta: number) {
setFontScale(fontScale.value + delta)
}
function resetFontScale() {
setFontScale(100)
}
function handleZoomKeydown(event: KeyboardEvent) {
const hasModifier = event.ctrlKey || event.metaKey
if (!hasModifier) {
return
}
if (event.key === '+' || event.key === '=' || event.code === 'NumpadAdd') {
event.preventDefault()
adjustFontScale(ZOOM_STEP)
return
}
if (event.key === '-' || event.code === 'NumpadSubtract') {
event.preventDefault()
adjustFontScale(-ZOOM_STEP)
return
}
if (event.key === '0' || event.code === 'Digit0' || event.code === 'Numpad0') {
event.preventDefault()
resetFontScale()
}
}
let lastWheelZoomTime = 0
function handleZoomWheel(event: WheelEvent) {
if (!(event.ctrlKey || event.metaKey)) {
return
}
event.preventDefault()
const now = Date.now()
if (now - lastWheelZoomTime < 35) {
return
}
lastWheelZoomTime = now
adjustFontScale(event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP)
}
let resizeCleanup: (() => void) | null = null
let ioResizeCleanup: (() => void) | null = null
function stopResize() {
if (!resizeCleanup) {
return
}
resizeCleanup()
resizeCleanup = null
}
function stopIoResize() {
if (!ioResizeCleanup) {
return
}
ioResizeCleanup()
ioResizeCleanup = null
}
function startResize(event: PointerEvent) {
if (!workspaceRef.value) {
return
}
event.preventDefault()
stopResize()
isResizing.value = true
document.body.classList.add('is-resizing-panel')
document.body.style.cursor = panelPosition.value === 'right' ? 'col-resize' : 'row-resize'
const handlePointerMove = (moveEvent: PointerEvent) => {
if (!workspaceRef.value) {
return
}
const rect = workspaceRef.value.getBoundingClientRect()
if (rect.width <= 0 || rect.height <= 0) {
return
}
if (panelPosition.value === 'right') {
const size = ((rect.right - moveEvent.clientX) / rect.width) * 100
panelSize.value = clampPanelSize(size, 'right')
} else {
const size = ((rect.bottom - moveEvent.clientY) / rect.height) * 100
panelSize.value = clampPanelSize(size, 'bottom')
}
}
const handlePointerUp = () => {
stopResize()
}
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp, { once: true })
resizeCleanup = () => {
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
document.body.classList.remove('is-resizing-panel')
document.body.style.removeProperty('cursor')
isResizing.value = false
requestEditorMeasure()
}
}
function startIoResize(event: PointerEvent) {
if (!ioPaneRef.value) {
return
}
event.preventDefault()
stopIoResize()
isIoResizing.value = true
document.body.classList.add('is-resizing-panel')
document.body.style.cursor = panelPosition.value === 'bottom' ? 'col-resize' : 'row-resize'
const handlePointerMove = (moveEvent: PointerEvent) => {
if (!ioPaneRef.value) {
return
}
const rect = ioPaneRef.value.getBoundingClientRect()
if (rect.width <= 0 || rect.height <= 0) {
return
}
if (panelPosition.value === 'bottom') {
const size = ((moveEvent.clientX - rect.left) / rect.width) * 100
ioSplit.value = clampIoSplit(size)
} else {
const size = ((moveEvent.clientY - rect.top) / rect.height) * 100
ioSplit.value = clampIoSplit(size)
}
}
const handlePointerUp = () => {
stopIoResize()
}
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp, { once: true })
ioResizeCleanup = () => {
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
document.body.classList.remove('is-resizing-panel')
document.body.style.removeProperty('cursor')
isIoResizing.value = false
}
}
function getStatus(id: string) {
fetch(backend + `/api/pastes/${props.id}`)
fetch(backend + `/api/pastes/${id}`)
.then(res => res.json())
.then(res => {
stdin.value = res.stdin
@@ -110,7 +418,50 @@ function getStatus(id: string) {
})
}
watch(panelPosition, (position) => {
panelSize.value = clampPanelSize(panelSize.value, position)
localStorage.setItem(PANEL_POSITION_KEY, position)
nextTick(() => requestEditorMeasure())
})
watch(panelSize, (size) => {
localStorage.setItem(PANEL_SIZE_KEY, String(size))
requestEditorMeasure()
})
watch(ioSplit, (size) => {
localStorage.setItem(IO_SPLIT_KEY, String(size))
})
watch(fontScale, (size) => {
localStorage.setItem(FONT_SCALE_KEY, String(size))
applyEditorTheme(size)
requestEditorMeasure()
})
onMounted(() => {
const savedPosition = localStorage.getItem(PANEL_POSITION_KEY)
if (savedPosition === 'right' || savedPosition === 'bottom') {
panelPosition.value = savedPosition
}
const savedSize = Number(localStorage.getItem(PANEL_SIZE_KEY))
if (!Number.isNaN(savedSize)) {
panelSize.value = clampPanelSize(savedSize, panelPosition.value)
}
const savedIoSplit = Number(localStorage.getItem(IO_SPLIT_KEY))
if (!Number.isNaN(savedIoSplit)) {
ioSplit.value = clampIoSplit(savedIoSplit)
}
const savedScale = Number(localStorage.getItem(FONT_SCALE_KEY))
if (!Number.isNaN(savedScale)) {
fontScale.value = clampFontScale(savedScale)
}
window.addEventListener('keydown', handleZoomKeydown)
window.addEventListener('wheel', handleZoomWheel, { passive: false })
const oldCode = localStorage.getItem('code')
stdin.value = localStorage.getItem('stdin') || ''
@@ -121,14 +472,11 @@ onMounted(() => {
cpp(),
ls,
indentUnit.of(" "),
EditorView.theme({
"&": { height: "100%" },
".cm-scroller": { overflow: "auto" }
}),
editorThemeCompartment.of(createEditorTheme(fontScale.value)),
EditorView.updateListener.of((update) => {
if (update.docChanged && update.selectionSet) {
const cursorParams = update.state.selection.main.head;
update.view.dispatch({
effects: EditorView.scrollIntoView(cursorParams, { y: "nearest" })
});
@@ -140,6 +488,8 @@ onMounted(() => {
// 如果需要在行中插入真实 Tab
{ key: "Mod-Tab", run: insertTab },
]),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
syntaxHighlighting(oneDarkHighlightStyle),
oneDark,
]
})
@@ -147,6 +497,9 @@ onMounted(() => {
state,
parent: editorContainer.value as HTMLElement
})
nextTick(() => requestEditorMeasure())
console.log(props.id)
if (props.id !== null && props.id !== undefined && props.id !== '') {
isLoading.value = true
@@ -170,7 +523,7 @@ onMounted(() => {
time.value = res.execution_time_ms
log.value = res.compile_log
if (status.value === 'pending' || status.value === 'running') {
let timer = setInterval(() => {
const timer = setInterval(() => {
getStatus(props.id)
if (status.value !== 'pending' && status.value !== 'running') {
clearInterval(timer)
@@ -189,6 +542,11 @@ onMounted(() => {
})
onUnmounted(() => {
stopResize()
stopIoResize()
window.removeEventListener('keydown', handleZoomKeydown)
window.removeEventListener('wheel', handleZoomWheel)
if (editorView.value) {
console.log("Destroying CodeMirror EditorView and closing LSP connection...");
editorView.value.destroy();
@@ -243,6 +601,344 @@ setInterval(() => {
}
}, 1000)
</script>
<style scoped>
.code-page {
flex: 1;
min-height: 0;
width: 100%;
background:
radial-gradient(circle at 10% 6%, rgba(59, 67, 102, 0.32), transparent 36%),
radial-gradient(circle at 88% 88%, rgba(0, 122, 204, 0.15), transparent 33%),
#1e1e1e;
color: #d4d4d4;
overflow: hidden;
}
.workbench {
width: 100%;
height: 100%;
min-height: 0;
min-width: 0;
display: flex;
background: #1e1e1e;
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
}
.workspace {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
}
.workspace-right {
flex-direction: row;
}
.workspace-bottom {
flex-direction: column;
}
.editor-pane {
flex: 1;
min-width: 0;
min-height: 0;
background: #1e1e1e;
}
.editor-surface {
width: 100%;
height: 100%;
}
.splitter {
flex-shrink: 0;
background: #2d2d30;
position: relative;
transition: background-color 120ms ease;
}
.splitter:hover,
.splitter.is-active {
background: #007acc;
}
.splitter-vertical {
width: 6px;
cursor: col-resize;
}
.splitter-vertical::after {
content: "";
position: absolute;
left: 2px;
right: 2px;
top: 50%;
height: 52px;
transform: translateY(-50%);
border-radius: 999px;
background: repeating-linear-gradient(to bottom, transparent, transparent 6px, rgba(255, 255, 255, 0.24) 6px, rgba(255, 255, 255, 0.24) 8px);
}
.splitter-horizontal {
height: 6px;
cursor: row-resize;
}
.splitter-horizontal::after {
content: "";
position: absolute;
top: 2px;
bottom: 2px;
left: 50%;
width: 52px;
transform: translateX(-50%);
border-radius: 999px;
background: repeating-linear-gradient(to right, transparent, transparent 6px, rgba(255, 255, 255, 0.24) 6px, rgba(255, 255, 255, 0.24) 8px);
}
.io-pane {
min-height: 110px;
min-width: 130px;
display: flex;
background: #252526;
border-left: 1px solid #2d2d30;
}
.io-pane-vertical {
flex-direction: column;
}
.io-pane-horizontal {
flex-direction: row;
}
.workspace-bottom .io-pane {
border-left: 0;
border-top: 1px solid #2d2d30;
}
.io-section {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.io-splitter {
flex-shrink: 0;
background: #2d2d30;
position: relative;
transition: background-color 120ms ease;
}
.io-splitter:hover,
.io-splitter.is-active {
background: #007acc;
}
.io-splitter-horizontal {
height: 6px;
cursor: row-resize;
}
.io-splitter-horizontal::after {
content: "";
position: absolute;
top: 2px;
bottom: 2px;
left: 50%;
width: 40px;
transform: translateX(-50%);
border-radius: 999px;
background: repeating-linear-gradient(to right, transparent, transparent 5px, rgba(255, 255, 255, 0.24) 5px, rgba(255, 255, 255, 0.24) 7px);
}
.io-splitter-vertical {
width: 6px;
cursor: col-resize;
}
.io-splitter-vertical::after {
content: "";
position: absolute;
left: 2px;
right: 2px;
top: 50%;
height: 40px;
transform: translateY(-50%);
border-radius: 999px;
background: repeating-linear-gradient(to bottom, transparent, transparent 5px, rgba(255, 255, 255, 0.24) 5px, rgba(255, 255, 255, 0.24) 7px);
}
.io-pane-horizontal .io-section + .io-section .io-header {
border-left: 1px solid #2d2d30;
}
.io-header {
height: 32px;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
padding: 0 10px;
font-size: 16px;
font-weight: 600;
color: #c5c8ce;
background: #2d2d30;
border-bottom: 1px solid #222;
}
.header-spacer {
flex: 1;
min-width: 0;
}
.action-btn {
width: 24px;
height: 24px;
padding: 0;
border: 0;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
cursor: pointer;
}
.action-btn:hover {
background: #3c3c40;
}
.action-btn img {
width: 18px;
height: 18px;
}
.io-content {
flex: 1;
min-height: 0;
}
.input-area {
width: 100%;
height: 100%;
border: 0;
outline: 0;
resize: none;
padding: 10px;
background: #1e1e1e;
color: #d4d4d4;
font-family: inherit;
font-size: var(--ui-font-size);
line-height: 1.6;
white-space: pre-wrap;
}
.output-area {
overflow: auto;
padding: 10px;
background: #1e1e1e;
}
.run-time {
color: #4fc1ff;
margin-bottom: 8px;
font-size: var(--ui-font-size);
}
.output-text {
margin: 0;
font-size: var(--ui-font-size);
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.output-error {
color: #f48771;
}
.right-toolbar {
width: 60px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 10px 6px;
border-left: 1px solid #2d2d30;
background: linear-gradient(180deg, #2a2a2d 0%, #252526 30%, #252526 100%);
}
.tool-btn {
width: 100%;
min-height: 30px;
border: 1px solid #3a3a3d;
border-radius: 6px;
background: #2f2f32;
color: #c9ccd3;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.tool-btn:hover {
background: #38383c;
border-color: #4b4b4f;
}
.tool-btn.active {
background: #0e639c;
border-color: #1177bb;
color: #ffffff;
}
.toolbar-divider {
width: 100%;
height: 1px;
background: #3b3b3d;
margin: 4px 0;
}
.zoom-label {
width: 100%;
text-align: center;
padding: 4px 0;
border-radius: 6px;
background: #1f1f21;
color: #9fa4ad;
font-size: 11px;
border: 1px solid #353538;
}
:deep(.cm-editor) {
height: 100%;
}
:global(body.is-resizing-panel) {
user-select: none;
}
@media (max-width: 900px) {
.right-toolbar {
width: 54px;
padding: 8px 4px;
gap: 6px;
}
.io-pane {
min-width: 110px;
min-height: 100px;
}
.io-splitter-vertical {
width: 5px;
}
}
</style>