add
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
<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>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import viteLogo from '../assets/vite.svg'
|
||||
import heroImg from '../assets/hero.png'
|
||||
import vueLogo from '../assets/vue.svg'
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="center">
|
||||
<div class="hero">
|
||||
<img :src="heroImg" class="base" width="170" height="179" alt="" />
|
||||
<img :src="vueLogo" class="framework" alt="Vue logo" />
|
||||
<img :src="viteLogo" class="vite" alt="Vite logo" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>Get started</h1>
|
||||
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
|
||||
</div>
|
||||
<button type="button" class="counter" @click="count++">
|
||||
Count is {{ count }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div class="ticks"></div>
|
||||
|
||||
<section id="next-steps">
|
||||
<div id="docs">
|
||||
<svg class="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#documentation-icon"></use>
|
||||
</svg>
|
||||
<h2>Documentation</h2>
|
||||
<p>Your questions, answered</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://vite.dev/" target="_blank">
|
||||
<img class="logo" :src="viteLogo" alt="" />
|
||||
Explore Vite
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img class="button-icon" :src="vueLogo" alt="" />
|
||||
Learn more
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="social">
|
||||
<svg class="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#social-icon"></use>
|
||||
</svg>
|
||||
<h2>Connect with us</h2>
|
||||
<p>Join the Vite community</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#github-icon"></use>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chat.vite.dev/" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#discord-icon"></use>
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/vite_js" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#x-icon"></use>
|
||||
</svg>
|
||||
X.com
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#bluesky-icon"></use>
|
||||
</svg>
|
||||
Bluesky
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="ticks"></div>
|
||||
<section id="spacer"></section>
|
||||
</template>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<aside class="sidebar" :class="{ collapsed }">
|
||||
<div class="sidebar-header">
|
||||
<h2 class="sidebar-title" @click="$router.push('/')">
|
||||
<span class="logo">📚</span>
|
||||
<span v-show="!collapsed">算法分析教学</span>
|
||||
</h2>
|
||||
<button class="toggle-btn" @click="$emit('toggle')">
|
||||
{{ collapsed ? '☰' : '✕' }}
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<router-link
|
||||
v-for="ch in chapters"
|
||||
:key="ch.id"
|
||||
:to="`/chapter/${ch.id}`"
|
||||
class="nav-item"
|
||||
:class="{ active: currentId === ch.id }"
|
||||
>
|
||||
<span class="nav-icon">{{ ch.icon }}</span>
|
||||
<span class="nav-text" v-show="!collapsed">
|
||||
<span class="nav-title">{{ ch.title.split('—')[0].trim() }}</span>
|
||||
<span class="nav-subtitle">{{ ch.subtitle }}</span>
|
||||
</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
<div class="sidebar-footer" v-show="!collapsed">
|
||||
<p>算法分析与设计</p>
|
||||
<p class="footer-sub">教学辅助平台</p>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { chapters } from '../data/chapters.js'
|
||||
|
||||
defineProps({
|
||||
collapsed: Boolean,
|
||||
currentId: String
|
||||
})
|
||||
|
||||
defineEmits(['toggle'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: var(--hover-bg);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 12px 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--hover-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--active-bg);
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nav-subtitle {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.footer-sub {
|
||||
font-size: 10px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div class="sort-visualizer">
|
||||
<div class="viz-header">
|
||||
<h3 class="viz-title">{{ title }}</h3>
|
||||
<div class="viz-controls">
|
||||
<button class="ctrl-btn" @click="randomize" title="随机生成新数组">🎲</button>
|
||||
<button class="ctrl-btn" @click="reset" title="重置">⏮</button>
|
||||
<button class="ctrl-btn" @click="prevStep" :disabled="currentStep <= 0" title="上一步">◀</button>
|
||||
<button class="ctrl-btn play-btn" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
|
||||
{{ isPlaying ? '⏸' : '▶' }}
|
||||
</button>
|
||||
<button class="ctrl-btn" @click="nextStep" :disabled="currentStep >= totalSteps - 1" title="下一步">▶</button>
|
||||
<button class="ctrl-btn" @click="goToEnd" :disabled="currentStep >= totalSteps - 1" title="跳到结束">⏭</button>
|
||||
<span class="step-info">{{ currentStep + 1 }} / {{ totalSteps }}</span>
|
||||
</div>
|
||||
<div class="speed-control">
|
||||
<label>速度</label>
|
||||
<input type="range" min="1" max="10" v-model.number="speed" class="speed-slider" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-canvas-wrapper">
|
||||
<div class="viz-canvas" ref="canvasRef">
|
||||
<div
|
||||
v-for="(val, idx) in currentState.array"
|
||||
:key="idx"
|
||||
class="bar-wrapper"
|
||||
:style="{ height: barHeight(val) + 'px' }"
|
||||
>
|
||||
<div
|
||||
class="bar"
|
||||
:class="barClass(idx)"
|
||||
:style="{ height: '100%' }"
|
||||
>
|
||||
<span class="bar-value" v-if="showValues">{{ val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="viz-description">
|
||||
<span class="desc-text">{{ currentState.description }}</span>
|
||||
<div class="legend">
|
||||
<span class="legend-item"><span class="legend-color legend-default"></span>未处理</span>
|
||||
<span class="legend-item"><span class="legend-color legend-comparing"></span>比较中</span>
|
||||
<span class="legend-item"><span class="legend-color legend-swapping"></span>交换</span>
|
||||
<span class="legend-item"><span class="legend-color legend-pivot"></span>基准</span>
|
||||
<span class="legend-item"><span class="legend-color legend-sorted"></span>已排序</span>
|
||||
<span class="legend-item"><span class="legend-color legend-merging"></span>归并中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch, onBeforeUnmount } from 'vue'
|
||||
import { generateMergeSortSteps, generateQuickSortSteps, generateRandomArray } from '../utils/sortAnimations.js'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '排序可视化' },
|
||||
algorithm: { type: String, default: 'merge' }, // 'merge' | 'quick'
|
||||
arraySize: { type: Number, default: 12 },
|
||||
showValues: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const canvasRef = ref(null)
|
||||
const currentStep = ref(0)
|
||||
const isPlaying = ref(false)
|
||||
const speed = ref(5)
|
||||
let timer = null
|
||||
|
||||
const rawArray = ref(generateRandomArray(props.arraySize))
|
||||
const steps = ref([])
|
||||
const totalSteps = computed(() => steps.value.length)
|
||||
|
||||
const currentState = computed(() => {
|
||||
if (steps.value.length === 0) {
|
||||
return {
|
||||
array: rawArray.value,
|
||||
highlights: {},
|
||||
description: '点击 ▶ 开始演示'
|
||||
}
|
||||
}
|
||||
return steps.value[currentStep.value] || steps.value[0]
|
||||
})
|
||||
|
||||
// 计算最大值用于柱状图高度
|
||||
const maxVal = computed(() => {
|
||||
return Math.max(...currentState.value.array, 1)
|
||||
})
|
||||
|
||||
function barHeight(val) {
|
||||
// 最大高度约 280px
|
||||
return Math.max(20, (val / maxVal.value) * 260)
|
||||
}
|
||||
|
||||
function barClass(idx) {
|
||||
const h = currentState.value.highlights || {}
|
||||
const classes = []
|
||||
|
||||
// 已排序(最终位置)
|
||||
if (h.sorted && h.sorted.includes(idx)) {
|
||||
classes.push('bar-sorted')
|
||||
return classes
|
||||
}
|
||||
if (h.sortedIndices && h.sortedIndices.includes(idx)) {
|
||||
classes.push('bar-sorted')
|
||||
return classes
|
||||
}
|
||||
|
||||
// 基准元素
|
||||
if (h.pivot === idx) {
|
||||
classes.push('bar-pivot')
|
||||
return classes
|
||||
}
|
||||
|
||||
// 交换中
|
||||
if (h.swapping && h.swapping.includes(idx)) {
|
||||
classes.push('bar-swapping')
|
||||
return classes
|
||||
}
|
||||
|
||||
// 比较中
|
||||
if (h.comparing && h.comparing.includes(idx)) {
|
||||
classes.push('bar-comparing')
|
||||
return classes
|
||||
}
|
||||
|
||||
// 归并范围
|
||||
if (h.merging) {
|
||||
const l = h.merging.left
|
||||
const r = h.merging.right
|
||||
if (idx >= l[0] && idx <= l[1]) classes.push('bar-merging-left')
|
||||
else if (idx >= r[0] && idx <= r[1]) classes.push('bar-merging-right')
|
||||
}
|
||||
|
||||
// 范围高亮
|
||||
if (h.range && !classes.length) {
|
||||
if (idx >= h.range[0] && idx <= h.range[1]) {
|
||||
classes.push('bar-in-range')
|
||||
}
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
// 生成动画步骤
|
||||
function generateSteps() {
|
||||
if (props.algorithm === 'merge') {
|
||||
steps.value = generateMergeSortSteps(rawArray.value)
|
||||
} else {
|
||||
steps.value = generateQuickSortSteps(rawArray.value)
|
||||
}
|
||||
currentStep.value = 0
|
||||
}
|
||||
|
||||
function randomize() {
|
||||
stopPlay()
|
||||
rawArray.value = generateRandomArray(props.arraySize)
|
||||
generateSteps()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
stopPlay()
|
||||
rawArray.value = generateRandomArray(props.arraySize)
|
||||
generateSteps()
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (currentStep.value < steps.value.length - 1) {
|
||||
currentStep.value++
|
||||
} else {
|
||||
stopPlay()
|
||||
}
|
||||
}
|
||||
|
||||
function prevStep() {
|
||||
if (currentStep.value > 0) {
|
||||
currentStep.value--
|
||||
}
|
||||
}
|
||||
|
||||
function goToEnd() {
|
||||
stopPlay()
|
||||
currentStep.value = steps.value.length - 1
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (isPlaying.value) {
|
||||
stopPlay()
|
||||
} else {
|
||||
startPlay()
|
||||
}
|
||||
}
|
||||
|
||||
function startPlay() {
|
||||
if (currentStep.value >= steps.value.length - 1) {
|
||||
currentStep.value = 0
|
||||
}
|
||||
isPlaying.value = true
|
||||
const interval = Math.max(50, 600 - speed.value * 50)
|
||||
timer = setInterval(() => {
|
||||
if (currentStep.value < steps.value.length - 1) {
|
||||
currentStep.value++
|
||||
} else {
|
||||
stopPlay()
|
||||
}
|
||||
}, interval)
|
||||
}
|
||||
|
||||
function stopPlay() {
|
||||
isPlaying.value = false
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPlay()
|
||||
})
|
||||
|
||||
// 初始化
|
||||
generateSteps()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sort-visualizer {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.viz-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
background: var(--hover-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.viz-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.viz-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ctrl-btn:hover:not(:disabled) {
|
||||
background: var(--active-bg);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.ctrl-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.play-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
font-family: var(--mono);
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.speed-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.speed-slider {
|
||||
width: 60px;
|
||||
accent-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 画布 */
|
||||
.viz-canvas-wrapper {
|
||||
padding: 24px 20px 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.viz-canvas {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
min-height: 300px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
flex: 1;
|
||||
min-width: 18px;
|
||||
max-width: 48px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
transition: height 0.2s ease;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 100%;
|
||||
border-radius: 4px 4px 0 0;
|
||||
background: var(--primary-color);
|
||||
opacity: 0.75;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar-value {
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* 状态颜色 */
|
||||
.bar-comparing {
|
||||
background: #f59e0b;
|
||||
opacity: 1;
|
||||
transform: scaleY(1.02);
|
||||
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.bar-swapping {
|
||||
background: #ef4444;
|
||||
opacity: 1;
|
||||
transform: scaleY(1.05);
|
||||
box-shadow: 0 0 12px rgba(239, 68, 68, 0.5);
|
||||
animation: pulse 0.3s ease;
|
||||
}
|
||||
|
||||
.bar-pivot {
|
||||
background: #8b5cf6;
|
||||
opacity: 1;
|
||||
transform: scaleY(1.05);
|
||||
box-shadow: 0 0 12px rgba(139, 92, 246, 0.5);
|
||||
}
|
||||
|
||||
.bar-sorted {
|
||||
background: #22c55e;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.bar-in-range {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.bar-merging-left {
|
||||
background: #3b82f6;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.bar-merging-right {
|
||||
background: #06b6d4;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scaleY(1.05); }
|
||||
50% { transform: scaleY(1.15); }
|
||||
}
|
||||
|
||||
/* 描述和图例 */
|
||||
.viz-description {
|
||||
padding: 10px 20px 14px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.desc-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.legend-default {
|
||||
background: var(--primary-color);
|
||||
opacity: 0.75;
|
||||
}
|
||||
.legend-comparing {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.legend-swapping {
|
||||
background: #ef4444;
|
||||
}
|
||||
.legend-pivot {
|
||||
background: #8b5cf6;
|
||||
}
|
||||
.legend-sorted {
|
||||
background: #22c55e;
|
||||
}
|
||||
.legend-merging {
|
||||
background: #06b6d4;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user