2026-01-18 12:21:49 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
ReflowRepaintDemo.vue
|
|
|
|
|
|
重排与重绘演示
|
|
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="reflow-demo">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="title">重排与重绘对比</div>
|
|
|
|
|
|
<div class="subtitle">观察不同操作对性能的影响</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="demo-container">
|
|
|
|
|
|
<div class="canvas-area">
|
|
|
|
|
|
<div class="box-container">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="box in boxes"
|
|
|
|
|
|
:key="box.id"
|
|
|
|
|
|
class="box"
|
|
|
|
|
|
:class="box.selected ? 'selected' : ''"
|
|
|
|
|
|
:style="getBoxStyle(box)"
|
|
|
|
|
|
@click="selectBox(box.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ box.id }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="performance-meter">
|
|
|
|
|
|
<div class="meter-label">性能影响</div>
|
|
|
|
|
|
<div class="meter-bar">
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<div
|
|
|
|
|
|
class="meter-fill"
|
|
|
|
|
|
:class="performanceLevel.class"
|
|
|
|
|
|
:style="{ width: performanceImpact + '%' }"
|
|
|
|
|
|
></div>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="meter-value" :class="performanceLevel.class">
|
|
|
|
|
|
{{ performanceLevel.text }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="stats">
|
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
|
<div class="stat-label">操作类型</div>
|
|
|
|
|
|
<div class="stat-value">{{ currentOperation }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
|
<div class="stat-label">影响范围</div>
|
|
|
|
|
|
<div class="stat-value">{{ affectedElements }} 个元素</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<div class="control-section">
|
|
|
|
|
|
<h4>重排操作 (Reflow)</h4>
|
|
|
|
|
|
<p class="control-desc">改变元素尺寸或位置,触发布局计算</p>
|
|
|
|
|
|
<button @click="changeWidth" class="btn reflow">改变宽度</button>
|
|
|
|
|
|
<button @click="changePosition" class="btn reflow">改变位置</button>
|
|
|
|
|
|
<button @click="addBox" class="btn reflow">添加元素</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="control-section">
|
|
|
|
|
|
<h4>重绘操作 (Repaint)</h4>
|
|
|
|
|
|
<p class="control-desc">只改变外观,不触发布局</p>
|
|
|
|
|
|
<button @click="changeColor" class="btn repaint">改变颜色</button>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<button @click="changeBackground" class="btn repaint">
|
|
|
|
|
|
改变背景
|
|
|
|
|
|
</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
<button @click="toggleBorder" class="btn repaint">切换边框</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="control-section">
|
|
|
|
|
|
<h4>合成操作 (Composite)</h4>
|
|
|
|
|
|
<p class="control-desc">只触发合成,性能最佳</p>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<button @click="transformTranslate" class="btn composite">
|
|
|
|
|
|
Transform 位移
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="transformRotate" class="btn composite">
|
|
|
|
|
|
Transform 旋转
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button @click="changeOpacity" class="btn composite">
|
|
|
|
|
|
改变透明度
|
|
|
|
|
|
</button>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="info-section">
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<h4>什么是重排 (Reflow)?</h4>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
当元素的位置、尺寸发生变化时,浏览器需要重新计算布局,这个过程叫重排。重排开销最大,因为要重新计算所有受影响元素的位置。
|
|
|
|
|
|
</p>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<h4>什么是重绘 (Repaint)?</h4>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
当元素的外观(颜色、背景)发生变化,但位置不变时,浏览器只需要重新绘制像素,这个过程叫重绘。比重排快,但仍有开销。
|
|
|
|
|
|
</p>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="info-card">
|
|
|
|
|
|
<h4>什么是合成 (Composite)?</h4>
|
2026-02-01 23:42:12 +08:00
|
|
|
|
<p>
|
|
|
|
|
|
使用 transform 和 opacity
|
|
|
|
|
|
等属性,浏览器可以在合成层上完成变化,完全不触发布局和绘制。性能最佳,推荐优先使用。
|
|
|
|
|
|
</p>
|
2026-01-18 12:21:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
const boxes = ref([
|
2026-02-01 23:42:12 +08:00
|
|
|
|
{
|
|
|
|
|
|
id: 1,
|
|
|
|
|
|
x: 0,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
color: '#3b82f6',
|
|
|
|
|
|
bg: '#dbeafe',
|
|
|
|
|
|
rotation: 0,
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
border: false,
|
|
|
|
|
|
selected: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 2,
|
|
|
|
|
|
x: 100,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
color: '#8b5cf6',
|
|
|
|
|
|
bg: '#ede9fe',
|
|
|
|
|
|
rotation: 0,
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
border: false,
|
|
|
|
|
|
selected: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 3,
|
|
|
|
|
|
x: 0,
|
|
|
|
|
|
y: 100,
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
color: '#ec4899',
|
|
|
|
|
|
bg: '#fce7f3',
|
|
|
|
|
|
rotation: 0,
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
border: false,
|
|
|
|
|
|
selected: false
|
|
|
|
|
|
}
|
2026-01-18 12:21:49 +08:00
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const currentOperation = ref('无')
|
|
|
|
|
|
const performanceImpact = ref(0)
|
|
|
|
|
|
const affectedElements = ref(0)
|
|
|
|
|
|
|
|
|
|
|
|
const performanceLevel = computed(() => {
|
|
|
|
|
|
if (performanceImpact.value <= 33) {
|
|
|
|
|
|
return { class: 'good', text: '低' }
|
|
|
|
|
|
} else if (performanceImpact.value <= 66) {
|
|
|
|
|
|
return { class: 'medium', text: '中' }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return { class: 'high', text: '高' }
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function getBoxStyle(box) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
left: box.x + 'px',
|
|
|
|
|
|
top: box.y + 'px',
|
|
|
|
|
|
width: box.width + 'px',
|
|
|
|
|
|
height: box.height + 'px',
|
|
|
|
|
|
backgroundColor: box.bg,
|
|
|
|
|
|
borderColor: box.color,
|
|
|
|
|
|
borderWidth: box.border ? '3px' : '0px',
|
|
|
|
|
|
color: box.color,
|
|
|
|
|
|
transform: `rotate(${box.rotation}deg)`,
|
|
|
|
|
|
opacity: box.opacity
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectBox(id) {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((b) => (b.selected = b.id === id))
|
2026-01-18 12:21:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateMetrics(operation, impact, affected) {
|
|
|
|
|
|
currentOperation.value = operation
|
|
|
|
|
|
performanceImpact.value = impact
|
|
|
|
|
|
affectedElements.value = affected
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeWidth() {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.width = 60 + Math.random() * 60
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('改变宽度', 90, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changePosition() {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.x = Math.random() * 150
|
|
|
|
|
|
box.y = Math.random() * 150
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('改变位置', 85, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addBox() {
|
|
|
|
|
|
const newId = boxes.value.length + 1
|
|
|
|
|
|
boxes.value.push({
|
|
|
|
|
|
id: newId,
|
|
|
|
|
|
x: Math.random() * 100,
|
|
|
|
|
|
y: Math.random() * 100,
|
|
|
|
|
|
width: 80,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
color: '#10b981',
|
|
|
|
|
|
bg: '#d1fae5',
|
|
|
|
|
|
rotation: 0,
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
border: false,
|
|
|
|
|
|
selected: false
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('添加元素', 95, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeColor() {
|
|
|
|
|
|
const colors = ['#3b82f6', '#8b5cf6', '#ec4899', '#10b981', '#f59e0b']
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.color = colors[Math.floor(Math.random() * colors.length)]
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('改变颜色', 50, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeBackground() {
|
|
|
|
|
|
const bgs = ['#dbeafe', '#ede9fe', '#fce7f3', '#d1fae5', '#fef3c7']
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.bg = bgs[Math.floor(Math.random() * bgs.length)]
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('改变背景', 45, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleBorder() {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.border = !box.border
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('切换边框', 55, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function transformTranslate() {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.x += Math.random() * 20 - 10
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('Transform 位移', 10, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function transformRotate() {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.rotation += Math.random() * 30 - 15
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('Transform 旋转', 10, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function changeOpacity() {
|
2026-02-01 23:42:12 +08:00
|
|
|
|
boxes.value.forEach((box) => {
|
2026-01-18 12:21:49 +08:00
|
|
|
|
box.opacity = 0.5 + Math.random() * 0.5
|
|
|
|
|
|
})
|
|
|
|
|
|
updateMetrics('改变透明度', 10, boxes.value.length)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.reflow-demo {
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
|
margin: 1.5rem 0;
|
|
|
|
|
|
font-family: var(--vp-font-family-base);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 1.05rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.subtitle {
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
margin-top: 0.3rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.demo-container {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 300px;
|
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.demo-container {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-area {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.box-container {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 250px;
|
|
|
|
|
|
margin-bottom: 1.5rem;
|
|
|
|
|
|
border: 2px dashed var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.box {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.box:hover {
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.box.selected {
|
|
|
|
|
|
box-shadow: 0 0 0 3px var(--vp-c-brand);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.performance-meter {
|
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-label {
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-bar {
|
|
|
|
|
|
height: 12px;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
transition: all 0.5s ease;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-fill.good {
|
|
|
|
|
|
background: linear-gradient(90deg, #22c55e, #14b8a6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-fill.medium {
|
|
|
|
|
|
background: linear-gradient(90deg, #f59e0b, #f97316);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-fill.high {
|
|
|
|
|
|
background: linear-gradient(90deg, #ef4444, #dc2626);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-value {
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-value.good {
|
|
|
|
|
|
color: #22c55e;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-value.medium {
|
|
|
|
|
|
color: #f59e0b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.meter-value.high {
|
|
|
|
|
|
color: #ef4444;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
background: var(--vp-c-bg-soft);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 0.8rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
margin-bottom: 0.3rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control-section {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control-section h4 {
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 0.3rem;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.control-desc {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
margin-bottom: 0.8rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 0.6rem;
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.reflow {
|
|
|
|
|
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.reflow:hover {
|
|
|
|
|
|
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.repaint {
|
|
|
|
|
|
background: linear-gradient(135deg, #f59e0b, #f97316);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.repaint:hover {
|
|
|
|
|
|
background: linear-gradient(135deg, #f97316, #ea580c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.composite {
|
|
|
|
|
|
background: linear-gradient(135deg, #22c55e, #14b8a6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn.composite:hover {
|
|
|
|
|
|
background: linear-gradient(135deg, #14b8a6, #0d9488);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-section {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-card {
|
|
|
|
|
|
background: var(--vp-c-bg);
|
|
|
|
|
|
border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-card h4 {
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
|
color: var(--vp-c-text-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-card p {
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
color: var(--vp-c-text-2);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|