feat: update docs and components, fix DLQ demo bug

This commit is contained in:
sanbuphy
2026-01-18 12:21:49 +08:00
parent 26ed39e1eb
commit e41063a1cd
159 changed files with 54236 additions and 2525 deletions
@@ -4,18 +4,55 @@
<div class="controls">
<button @click="init" :disabled="inited" class="btn">初始化</button>
<button @click="commit" :disabled="!inited" class="btn">提交</button>
<button @click="branch" :disabled="!inited || hasBranch" class="btn">创建分支</button>
<button @click="branch" :disabled="!inited || hasBranch" class="btn">
创建分支
</button>
<button @click="merge" :disabled="!hasBranch" class="btn">合并</button>
<button @click="reset" class="btn secondary">重置</button>
</div>
<div class="graph">
<svg viewBox="0 0 400 120">
<line x1="50" y1="40" x2="350" y2="40" stroke="#3b82f6" stroke-width="3"/>
<line v-if="hasBranch" x1="150" y1="40" x2="150" y2="80" stroke="#10b981" stroke-width="3"/>
<line v-if="hasBranch" x1="150" y1="80" x2="300" y2="80" stroke="#10b981" stroke-width="3"/>
<circle v-for="(c,i) in main" :cx="60+i*50" cy="40" r="8" fill="#3b82f6"/>
<circle v-for="(c,i) in feat" :cx="180+i*50" cy="80" r="8" fill="#10b981"/>
<line
x1="50"
y1="40"
x2="350"
y2="40"
stroke="#3b82f6"
stroke-width="3"
/>
<line
v-if="hasBranch"
x1="150"
y1="40"
x2="150"
y2="80"
stroke="#10b981"
stroke-width="3"
/>
<line
v-if="hasBranch"
x1="150"
y1="80"
x2="300"
y2="80"
stroke="#10b981"
stroke-width="3"
/>
<circle
v-for="(c, i) in main"
:cx="60 + i * 50"
cy="40"
r="8"
fill="#3b82f6"
/>
<circle
v-for="(c, i) in feat"
:cx="180 + i * 50"
cy="80"
r="8"
fill="#10b981"
/>
</svg>
</div>
@@ -38,23 +75,91 @@ const hasBranch = ref(false)
const main = ref([])
const feat = ref([])
const init = () => { inited.value = true; main.value = [1] }
const commit = () => { if(inited.value) main.value.push(1) }
const branch = () => { if(inited.value) { hasBranch.value = true; feat.value = [1] } }
const merge = () => { if(hasBranch.value) { main.value.push(1); hasBranch.value = false; feat.value = [] } }
const reset = () => { inited.value = false; hasBranch.value = false; main.value = []; feat.value = [] }
const init = () => {
inited.value = true
main.value = [1]
}
const commit = () => {
if (inited.value) main.value.push(1)
}
const branch = () => {
if (inited.value) {
hasBranch.value = true
feat.value = [1]
}
}
const merge = () => {
if (hasBranch.value) {
main.value.push(1)
hasBranch.value = false
feat.value = []
}
}
const reset = () => {
inited.value = false
hasBranch.value = false
main.value = []
feat.value = []
}
</script>
<style scoped>
.branch-demo { border: 1px solid var(--vp-c-divider); border-radius: 8px; background-color: var(--vp-c-bg-soft); padding: 1.5rem; margin: 1rem 0; }
.controls { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
.btn { padding: 0.5rem 1rem; border: 1px solid var(--vp-c-brand); background: var(--vp-c-bg); color: var(--vp-c-brand); border-radius: 6px; cursor: pointer; }
.btn:hover:not(:disabled) { background: var(--vp-c-brand); color: var(--vp-c-bg); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.secondary { border-color: var(--vp-c-divider); }
.graph { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); margin: 1rem 0; }
.graph svg { width: 100%; height: auto; }
.status { display: flex; gap: 2rem; }
.info-box { padding: 1rem; background: var(--vp-c-bg); border-left: 4px solid var(--vp-c-brand); border-radius: 4px; margin-top: 1rem; }
.info-box p { margin: 0; color: var(--vp-c-text-1); }
.branch-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-brand);
background: var(--vp-c-bg);
color: var(--vp-c-brand);
border-radius: 6px;
cursor: pointer;
}
.btn:hover:not(:disabled) {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn.secondary {
border-color: var(--vp-c-divider);
}
.graph {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
margin: 1rem 0;
}
.graph svg {
width: 100%;
height: auto;
}
.status {
display: flex;
gap: 2rem;
}
.info-box {
padding: 1rem;
background: var(--vp-c-bg);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
margin-top: 1rem;
}
.info-box p {
margin: 0;
color: var(--vp-c-text-1);
}
</style>
@@ -13,7 +13,12 @@
</div>
<div class="input-line">
<span class="prompt">$</span>
<input v-model="cmd" @keyup.enter="execute" placeholder="git status" class="cmd-input" />
<input
v-model="cmd"
@keyup.enter="execute"
placeholder="git status"
class="cmd-input"
/>
<button @click="execute" class="run-btn">运行</button>
</div>
</div>
@@ -22,7 +27,9 @@
<button @click="runCmd('git init')" class="cmd-btn">初始化</button>
<button @click="runCmd('git status')" class="cmd-btn">状态</button>
<button @click="runCmd('git add .')" class="cmd-btn">添加</button>
<button @click="runCmd('git commit -m \'msg\'')" class="cmd-btn">提交</button>
<button @click="runCmd('git commit -m \'msg\'')" class="cmd-btn">
提交
</button>
</div>
</div>
@@ -41,13 +48,19 @@ const output = ref([])
const execute = () => {
const c = cmd.value.trim()
if (!c) return
output.value.push({ type: 'command', text: c })
if (c === 'git init') {
output.value.push({ type: 'success', text: 'Initialized empty Git repository' })
output.value.push({
type: 'success',
text: 'Initialized empty Git repository'
})
} else if (c === 'git status') {
output.value.push({ type: 'info', text: 'On branch main\nnothing to commit' })
output.value.push({
type: 'info',
text: 'On branch main\nnothing to commit'
})
} else if (c === 'git add .') {
output.value.push({ type: 'success', text: 'Files added to staging area' })
} else if (c.startsWith('git commit')) {
@@ -55,7 +68,7 @@ const execute = () => {
} else {
output.value.push({ type: 'error', text: 'Unknown command' })
}
cmd.value = ''
}
@@ -88,11 +101,22 @@ const runCmd = (c) => {
color: #d1d5db;
}
.output .command { color: #10b981; }
.output .success { color: #10b981; }
.output .error { color: #ef4444; }
.output .info { color: #60a5fa; }
.output .welcome { color: #9ca3af; font-style: italic; }
.output .command {
color: #10b981;
}
.output .success {
color: #10b981;
}
.output .error {
color: #ef4444;
}
.output .info {
color: #60a5fa;
}
.output .welcome {
color: #9ca3af;
font-style: italic;
}
.input-line {
display: flex;
@@ -100,7 +124,9 @@ const runCmd = (c) => {
align-items: center;
}
.prompt { color: #10b981; }
.prompt {
color: #10b981;
}
.cmd-input {
flex: 1;
@@ -111,7 +137,9 @@ const runCmd = (c) => {
font-size: 0.875rem;
}
.cmd-input:focus { outline: none; }
.cmd-input:focus {
outline: none;
}
.run-btn {
padding: 0.25rem 0.75rem;
@@ -139,7 +167,10 @@ const runCmd = (c) => {
font-size: 0.875rem;
}
.cmd-btn:hover { background: var(--vp-c-brand); color: var(--vp-c-bg); }
.cmd-btn:hover {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.info-box {
padding: 1rem;
@@ -149,5 +180,9 @@ const runCmd = (c) => {
margin-top: 1rem;
}
.info-box p { margin: 0; color: var(--vp-c-text-1); line-height: 1.6; }
.info-box p {
margin: 0;
color: var(--vp-c-text-1);
line-height: 1.6;
}
</style>
@@ -2,19 +2,35 @@
<div class="conflict-demo">
<div class="panel">
<div class="editor">
<div class="line normal"><span class="ln">1</span>function greet() {</div>
<div class="line normal"><span class="ln">2</span> console.log('Hi');</div>
<div class="line conflict"><span class="ln">3</span>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD</div>
<div class="line current"><span class="ln">4</span> console.log('Welcome') // 当前版本</div>
<div class="line normal">
<span class="ln">1</span>function greet() {
</div>
<div class="line normal">
<span class="ln">2</span> console.log('Hi');
</div>
<div class="line conflict">
<span class="ln">3</span>&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD
</div>
<div class="line current">
<span class="ln">4</span> console.log('Welcome') // 当前版本
</div>
<div class="line conflict"><span class="ln">5</span>=======</div>
<div class="line incoming"><span class="ln">6</span> console.log('Greetings') // 传入版本</div>
<div class="line conflict"><span class="ln">7</span>&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature</div>
<div class="line normal"><span class="ln">8</span> console.log('Bye');</div>
<div class="line incoming">
<span class="ln">6</span> console.log('Greetings') // 传入版本
</div>
<div class="line conflict">
<span class="ln">7</span>&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature
</div>
<div class="line normal">
<span class="ln">8</span> console.log('Bye');
</div>
</div>
<div class="actions">
<button @click="resolve('current')" class="action-btn">保留当前</button>
<button @click="resolve('incoming')" class="action-btn">保留传入</button>
<button @click="resolve('incoming')" class="action-btn">
保留传入
</button>
<button @click="resolve('manual')" class="action-btn">手动合并</button>
</div>
</div>
@@ -28,21 +44,74 @@
<script setup>
import { ref } from 'vue'
const resolved = ref(false)
const resolve = (choice) => { resolved.value = true }
const resolve = (choice) => {
resolved.value = true
}
</script>
<style scoped>
.conflict-demo { border: 1px solid var(--vp-c-divider); border-radius: 8px; background-color: var(--vp-c-bg-soft); padding: 1.5rem; margin: 1rem 0; }
.editor { background: #1f2937; border-radius: 8px; padding: 1rem; font-family: monospace; margin-bottom: 1rem; }
.line { display: flex; gap: 0.5rem; line-height: 1.6; }
.ln { color: #6b7280; min-width: 2rem; }
.line.normal { color: #d1d5db; }
.line.conflict { color: #f59e0b; }
.line.current { color: #60a5fa; background: rgba(96,165,250,0.1); }
.line.incoming { color: #a78bfa; background: rgba(167,139,250,0.1); }
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.action-btn { padding: 0.625rem 1.25rem; border: 1px solid var(--vp-c-brand); background: var(--vp-c-bg); color: var(--vp-c-brand); border-radius: 6px; cursor: pointer; }
.action-btn:hover { background: var(--vp-c-brand); color: var(--vp-c-bg); }
.info-box { padding: 1rem; background: var(--vp-c-bg); border-left: 4px solid var(--vp-c-brand); border-radius: 4px; }
.info-box p { margin: 0; color: var(--vp-c-text-1); }
.conflict-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.editor {
background: #1f2937;
border-radius: 8px;
padding: 1rem;
font-family: monospace;
margin-bottom: 1rem;
}
.line {
display: flex;
gap: 0.5rem;
line-height: 1.6;
}
.ln {
color: #6b7280;
min-width: 2rem;
}
.line.normal {
color: #d1d5db;
}
.line.conflict {
color: #f59e0b;
}
.line.current {
color: #60a5fa;
background: rgba(96, 165, 250, 0.1);
}
.line.incoming {
color: #a78bfa;
background: rgba(167, 139, 250, 0.1);
}
.actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.action-btn {
padding: 0.625rem 1.25rem;
border: 1px solid var(--vp-c-brand);
background: var(--vp-c-bg);
color: var(--vp-c-brand);
border-radius: 6px;
cursor: pointer;
}
.action-btn:hover {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.info-box {
padding: 1rem;
background: var(--vp-c-bg);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
}
.info-box p {
margin: 0;
color: var(--vp-c-text-1);
}
</style>
@@ -7,7 +7,7 @@
<div class="commits">
<div v-for="c in local" :key="c" class="commit-dot">
<span class="dot local"></span>
<span class="hash">{{ c.substring(0,6) }}</span>
<span class="hash">{{ c.substring(0, 6) }}</span>
</div>
<div v-if="local.length === 0" class="empty"></div>
</div>
@@ -20,7 +20,7 @@
<div class="commits">
<div v-for="c in remote" :key="c" class="commit-dot">
<span class="dot remote"></span>
<span class="hash">{{ c.substring(0,6) }}</span>
<span class="hash">{{ c.substring(0, 6) }}</span>
</div>
<div v-if="remote.length === 0" class="empty"></div>
</div>
@@ -29,8 +29,16 @@
<div class="controls">
<button @click="localCommit" class="btn">本地提交</button>
<button @click="push" :disabled="local.length <= remote.length" class="btn">推送 Push</button>
<button @click="pull" :disabled="!hasRemote" class="btn">拉取 Pull</button>
<button
@click="push"
:disabled="local.length <= remote.length"
class="btn"
>
推送 Push
</button>
<button @click="pull" :disabled="!hasRemote" class="btn">
拉取 Pull
</button>
<button @click="reset" class="btn secondary">重置</button>
</div>
</div>
@@ -47,35 +55,125 @@ const local = ref([])
const remote = ref([])
const hasRemote = ref(false)
const localCommit = () => { local.value.push(Math.random().toString(16).substr(2,7)) }
const push = () => { remote.value.push(...local.value.slice(remote.value.length)); hasRemote.value = false }
const pull = () => { if(hasRemote.value) local.value.push(Math.random().toString(16).substr(2,7)); hasRemote.value = false }
const reset = () => { local.value = []; remote.value = []; hasRemote.value = false }
const localCommit = () => {
local.value.push(Math.random().toString(16).substr(2, 7))
}
const push = () => {
remote.value.push(...local.value.slice(remote.value.length))
hasRemote.value = false
}
const pull = () => {
if (hasRemote.value) local.value.push(Math.random().toString(16).substr(2, 7))
hasRemote.value = false
}
const reset = () => {
local.value = []
remote.value = []
hasRemote.value = false
}
</script>
<style scoped>
.remote-demo { border: 1px solid var(--vp-c-divider); border-radius: 8px; background-color: var(--vp-c-bg-soft); padding: 1.5rem; margin: 1rem 0; }
.repos { display: grid; grid-template-columns: 1fr auto 1fr; gap: 1rem; align-items: stretch; margin-bottom: 1rem; }
.repo { border: 1px solid var(--vp-c-divider); border-radius: 8px; padding: 1rem; background: var(--vp-c-bg); }
.header { font-weight: 600; margin-bottom: 0.5rem; }
.commits { min-height: 80px; }
.commit-dot { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; }
.dot.local { background: #3b82f6; }
.dot.remote { background: #10b981; }
.hash { font-family: monospace; font-size: 0.875rem; color: var(--vp-c-text-2); }
.sync { font-size: 2rem; text-align: center; }
.empty { color: var(--vp-c-text-3); text-align: center; padding: 1rem; }
.controls { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.btn { padding: 0.5rem 1rem; border: 1px solid var(--vp-c-brand); background: var(--vp-c-bg); color: var(--vp-c-brand); border-radius: 6px; cursor: pointer; }
.btn:hover:not(:disabled) { background: var(--vp-c-brand); color: var(--vp-c-bg); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.secondary { border-color: var(--vp-c-divider); }
.info-box { padding: 1rem; background: var(--vp-c-bg); border-left: 4px solid var(--vp-c-brand); border-radius: 4px; margin-top: 1rem; }
.info-box p { margin: 0; color: var(--vp-c-text-1); }
.remote-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.repos {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: stretch;
margin-bottom: 1rem;
}
.repo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
}
.header {
font-weight: 600;
margin-bottom: 0.5rem;
}
.commits {
min-height: 80px;
}
.commit-dot {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.dot.local {
background: #3b82f6;
}
.dot.remote {
background: #10b981;
}
.hash {
font-family: monospace;
font-size: 0.875rem;
color: var(--vp-c-text-2);
}
.sync {
font-size: 2rem;
text-align: center;
}
.empty {
color: var(--vp-c-text-3);
text-align: center;
padding: 1rem;
}
.controls {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-brand);
background: var(--vp-c-bg);
color: var(--vp-c-brand);
border-radius: 6px;
cursor: pointer;
}
.btn:hover:not(:disabled) {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn.secondary {
border-color: var(--vp-c-divider);
}
.info-box {
padding: 1rem;
background: var(--vp-c-bg);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
margin-top: 1rem;
}
.info-box p {
margin: 0;
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.repos { grid-template-columns: 1fr; }
.sync { transform: rotate(90deg); }
.repos {
grid-template-columns: 1fr;
}
.sync {
transform: rotate(90deg);
}
}
</style>
@@ -10,8 +10,8 @@
<div class="area">
<div class="header">📚 Stash ({{ stash.length }})</div>
<div v-for="(s,i) in stash" :key="i" class="stash-item">
<span class="num">{{ i+1 }}</span>
<div v-for="(s, i) in stash" :key="i" class="stash-item">
<span class="num">{{ i + 1 }}</span>
<span class="msg">{{ s }}</span>
</div>
<div v-if="stash.length === 0" class="empty"></div>
@@ -19,9 +19,19 @@
</div>
<div class="controls">
<button @click="doWork" :disabled="work.length > 0" class="btn">修改</button>
<button @click="save" :disabled="work.length === 0 || stash.length >= 3" class="btn">保存</button>
<button @click="pop" :disabled="stash.length === 0" class="btn">恢复</button>
<button @click="doWork" :disabled="work.length > 0" class="btn">
修改
</button>
<button
@click="save"
:disabled="work.length === 0 || stash.length >= 3"
class="btn"
>
保存
</button>
<button @click="pop" :disabled="stash.length === 0" class="btn">
恢复
</button>
<button @click="reset" class="btn secondary">重置</button>
</div>
</div>
@@ -36,25 +46,112 @@
import { ref } from 'vue'
const work = ref([])
const stash = ref([])
const doWork = () => { work.value = ['file.js', 'style.css'] }
const save = () => { stash.value.push('WIP'); work.value = [] }
const pop = () => { if(stash.value.length) { stash.value.pop(); work.value = ['file.js'] } }
const reset = () => { work.value = []; stash.value = [] }
const doWork = () => {
work.value = ['file.js', 'style.css']
}
const save = () => {
stash.value.push('WIP')
work.value = []
}
const pop = () => {
if (stash.value.length) {
stash.value.pop()
work.value = ['file.js']
}
}
const reset = () => {
work.value = []
stash.value = []
}
</script>
<style scoped>
.stash-demo { border: 1px solid var(--vp-c-divider); border-radius: 8px; background-color: var(--vp-c-bg-soft); padding: 1.5rem; margin: 1rem 0; }
.areas { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
.area { border: 1px solid var(--vp-c-divider); border-radius: 8px; padding: 1rem; background: var(--vp-c-bg); }
.header { font-weight: 600; margin-bottom: 0.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--vp-c-divider); }
.file, .stash-item { padding: 0.5rem; background: var(--vp-c-bg-soft); margin-bottom: 0.25rem; border-radius: 4px; font-size: 0.875rem; display: flex; gap: 0.5rem; align-items: center; }
.stash-item .num { width: 20px; height: 20px; background: var(--vp-c-brand); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; }
.empty { color: var(--vp-c-text-3); text-align: center; font-style: italic; padding: 1rem; }
.controls { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.btn { padding: 0.5rem 1rem; border: 1px solid var(--vp-c-brand); background: var(--vp-c-bg); color: var(--vp-c-brand); border-radius: 6px; cursor: pointer; }
.btn:hover:not(:disabled) { background: var(--vp-c-brand); color: var(--vp-c-bg); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.secondary { border-color: var(--vp-c-divider); }
.info-box { padding: 1rem; background: var(--vp-c-bg); border-left: 4px solid var(--vp-c-brand); border-radius: 4px; margin-top: 1rem; }
.info-box p { margin: 0; color: var(--vp-c-text-1); }
.stash-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.areas {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.area {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
}
.header {
font-weight: 600;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.file,
.stash-item {
padding: 0.5rem;
background: var(--vp-c-bg-soft);
margin-bottom: 0.25rem;
border-radius: 4px;
font-size: 0.875rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.stash-item .num {
width: 20px;
height: 20px;
background: var(--vp-c-brand);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
.empty {
color: var(--vp-c-text-3);
text-align: center;
font-style: italic;
padding: 1rem;
}
.controls {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-brand);
background: var(--vp-c-bg);
color: var(--vp-c-brand);
border-radius: 6px;
cursor: pointer;
}
.btn:hover:not(:disabled) {
background: var(--vp-c-brand);
color: var(--vp-c-bg);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn.secondary {
border-color: var(--vp-c-divider);
}
.info-box {
padding: 1rem;
background: var(--vp-c-bg);
border-left: 4px solid var(--vp-c-brand);
border-radius: 4px;
margin-top: 1rem;
}
.info-box p {
margin: 0;
color: var(--vp-c-text-1);
}
</style>
@@ -3,16 +3,28 @@
<div class="panel">
<div class="comparison">
<div class="mode-selector">
<button @click="mode = 'full'" :class="{active: mode === 'full'}" class="mode-btn">完整备份</button>
<button @click="mode = 'git'" :class="{active: mode === 'git'}" class="mode-btn">Git 增量</button>
<button
@click="mode = 'full'"
:class="{ active: mode === 'full' }"
class="mode-btn"
>
完整备份
</button>
<button
@click="mode = 'git'"
:class="{ active: mode === 'git' }"
class="mode-btn"
>
Git 增量
</button>
</div>
<div class="visualization">
<div class="bar-container">
<div class="bar full" :style="{height: fullSize + '%'}">
<div class="bar full" :style="{ height: fullSize + '%' }">
<span class="label">完整备份: {{ fullSize }}MB</span>
</div>
<div class="bar git" :style="{height: gitSize + '%'}">
<div class="bar git" :style="{ height: gitSize + '%' }">
<span class="label">Git 存储: {{ gitSize }}MB</span>
</div>
</div>
@@ -45,7 +57,9 @@ const versionCount = ref(5)
const fullSize = ref(500)
const gitSize = ref(50)
const savedPercent = computed(() => Math.round((1 - gitSize.value / fullSize.value) * 100))
const savedPercent = computed(() =>
Math.round((1 - gitSize.value / fullSize.value) * 100)
)
</script>
<style scoped>
@@ -95,8 +109,12 @@ const savedPercent = computed(() => Math.round((1 - gitSize.value / fullSize.val
transition: height 0.5s ease;
}
.bar.full { background: linear-gradient(135deg, #ef4444, #dc2626); }
.bar.git { background: linear-gradient(135deg, #10b981, #059669); }
.bar.full {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.bar.git {
background: linear-gradient(135deg, #10b981, #059669);
}
.stats {
display: flex;
@@ -12,9 +12,9 @@
</div>
<div class="desk-surface">
<transition-group name="file-pop">
<div
v-for="file in workingFiles"
:key="file.id"
<div
v-for="file in workingFiles"
:key="file.id"
class="file-card"
@click="addToStaging(file)"
>
@@ -25,7 +25,9 @@
</transition-group>
<div v-if="workingFiles.length === 0" class="empty-state">
桌上很干净
<button class="create-btn" @click="createNewFile">新建文件 📝</button>
<button class="create-btn" @click="createNewFile">
新建文件 📝
</button>
</div>
</div>
</div>
@@ -49,9 +51,9 @@
<div class="box-container">
<div class="box-body">
<transition-group name="file-drop">
<div
v-for="file in stagedFiles"
:key="file.id"
<div
v-for="file in stagedFiles"
:key="file.id"
class="file-card mini"
@click="unstageFile(file)"
>
@@ -68,8 +70,8 @@
<div class="box-flap right"></div>
</div>
<div class="staging-actions">
<button
class="commit-btn"
<button
class="commit-btn"
:disabled="stagedFiles.length === 0"
@click="commitFiles"
>
@@ -96,9 +98,9 @@
</div>
<div class="cabinet-body">
<transition-group name="drawer-slide">
<div
v-for="commit in commits.slice().reverse()"
:key="commit.hash"
<div
v-for="commit in commits.slice().reverse()"
:key="commit.hash"
class="drawer-item"
>
<div class="drawer-handle"></div>
@@ -107,7 +109,9 @@
<span class="commit-msg">{{ commit.message }}</span>
</div>
<div class="commit-files">
<span v-for="f in commit.files" :key="f" class="tiny-file">📄</span>
<span v-for="f in commit.files" :key="f" class="tiny-file"
>📄</span
>
</div>
</div>
</transition-group>
@@ -151,7 +155,7 @@ const createNewFile = () => {
}
const addToStaging = (file) => {
const index = workingFiles.value.findIndex(f => f.id === file.id)
const index = workingFiles.value.findIndex((f) => f.id === file.id)
if (index !== -1) {
workingFiles.value.splice(index, 1)
stagedFiles.value.push(file)
@@ -159,7 +163,7 @@ const addToStaging = (file) => {
}
const unstageFile = (file) => {
const index = stagedFiles.value.findIndex(f => f.id === file.id)
const index = stagedFiles.value.findIndex((f) => f.id === file.id)
if (index !== -1) {
stagedFiles.value.splice(index, 1)
workingFiles.value.push(file)
@@ -168,17 +172,23 @@ const unstageFile = (file) => {
const commitFiles = () => {
if (stagedFiles.value.length === 0) return
const files = [...stagedFiles.value]
stagedFiles.value = []
const msgs = ['Fix bug', 'Add feature', 'Update docs', 'Refactor code', 'Initial commit']
const msgs = [
'Fix bug',
'Add feature',
'Update docs',
'Refactor code',
'Initial commit'
]
const randomMsg = msgs[Math.floor(Math.random() * msgs.length)]
commits.value.push({
hash: Math.random().toString(16).substr(2, 6),
message: randomMsg,
files: files.map(f => f.name)
files: files.map((f) => f.name)
})
}
</script>
@@ -223,10 +233,21 @@ const commitFiles = () => {
border-bottom: 2px dashed var(--vp-c-divider);
}
.zone-icon { font-size: 1.5rem; }
.zone-info { display: flex; flex-direction: column; }
.zone-title { font-weight: bold; font-size: 0.9rem; }
.zone-desc { font-size: 0.7rem; color: var(--vp-c-text-2); }
.zone-icon {
font-size: 1.5rem;
}
.zone-info {
display: flex;
flex-direction: column;
}
.zone-title {
font-weight: bold;
font-size: 0.9rem;
}
.zone-desc {
font-size: 0.7rem;
color: var(--vp-c-text-2);
}
.empty-state {
text-align: center;
@@ -240,7 +261,10 @@ const commitFiles = () => {
}
/* 1. Working Desk */
.zone.working { border-color: #f59e0b; background: #fffbeb; }
.zone.working {
border-color: #f59e0b;
background: #fffbeb;
}
.desk-surface {
flex: 1;
display: flex;
@@ -265,22 +289,33 @@ const commitFiles = () => {
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.2s;
position: relative;
}
.file-card:hover {
transform: translateY(-4px) rotate(2deg);
box-shadow: 0 8px 12px rgba(0,0,0,0.1);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.1);
border-color: #f59e0b;
}
.file-icon { font-size: 2rem; margin-bottom: 4px; }
.file-name { font-size: 0.7rem; text-align: center; word-break: break-all; line-height: 1.2; }
.file-icon {
font-size: 2rem;
margin-bottom: 4px;
}
.file-name {
font-size: 0.7rem;
text-align: center;
word-break: break-all;
line-height: 1.2;
}
.action-hint {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(245, 158, 11, 0.9);
color: white;
display: flex;
@@ -292,7 +327,9 @@ const commitFiles = () => {
font-weight: bold;
font-size: 0.8rem;
}
.file-card:hover .action-hint { opacity: 1; }
.file-card:hover .action-hint {
opacity: 1;
}
.create-btn {
background: #f59e0b;
@@ -306,7 +343,10 @@ const commitFiles = () => {
}
/* 2. Staging Box */
.zone.staging { border-color: #3b82f6; background: #eff6ff; }
.zone.staging {
border-color: #3b82f6;
background: #eff6ff;
}
.box-container {
flex: 1;
position: relative;
@@ -341,10 +381,19 @@ const commitFiles = () => {
padding: 4px 8px;
gap: 8px;
}
.file-card.mini .file-icon { font-size: 1rem; margin: 0; }
.file-card.mini .file-name { font-size: 0.8rem; }
.file-card.mini:hover { border-color: #ef4444; }
.file-card.mini .action-hint { background: rgba(239, 68, 68, 0.9); }
.file-card.mini .file-icon {
font-size: 1rem;
margin: 0;
}
.file-card.mini .file-name {
font-size: 0.8rem;
}
.file-card.mini:hover {
border-color: #ef4444;
}
.file-card.mini .action-hint {
background: rgba(239, 68, 68, 0.9);
}
.box-flap {
position: absolute;
@@ -356,10 +405,23 @@ const commitFiles = () => {
border-bottom: none;
transition: all 0.5s;
}
.box-flap.left { left: 0; border-radius: 4px 0 0 0; transform-origin: bottom left; transform: rotate(10deg); }
.box-flap.right { right: 0; border-radius: 0 4px 0 0; transform-origin: bottom right; transform: rotate(-10deg); }
.box-flap.left {
left: 0;
border-radius: 4px 0 0 0;
transform-origin: bottom left;
transform: rotate(10deg);
}
.box-flap.right {
right: 0;
border-radius: 0 4px 0 0;
transform-origin: bottom right;
transform: rotate(-10deg);
}
.staging-actions { margin-top: 12px; text-align: center; }
.staging-actions {
margin-top: 12px;
text-align: center;
}
.commit-btn {
background: #3b82f6;
color: white;
@@ -371,11 +433,21 @@ const commitFiles = () => {
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
.commit-btn:disabled { background: #93c5fd; cursor: not-allowed; box-shadow: none; }
.commit-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 8px rgba(59, 130, 246, 0.4); }
.commit-btn:disabled {
background: #93c5fd;
cursor: not-allowed;
box-shadow: none;
}
.commit-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(59, 130, 246, 0.4);
}
/* 3. Repo Cabinet */
.zone.repo { border-color: #10b981; background: #ecfdf5; }
.zone.repo {
border-color: #10b981;
background: #ecfdf5;
}
.cabinet-body {
flex: 1;
display: flex;
@@ -403,11 +475,27 @@ const commitFiles = () => {
border: 1px solid #10b981;
border-radius: 2px;
}
.commit-info { flex: 1; display: flex; flex-direction: column; }
.commit-hash { font-size: 0.6rem; color: #10b981; font-family: monospace; }
.commit-msg { font-size: 0.8rem; font-weight: bold; }
.commit-files { display: flex; gap: 2px; }
.tiny-file { font-size: 0.6rem; }
.commit-info {
flex: 1;
display: flex;
flex-direction: column;
}
.commit-hash {
font-size: 0.6rem;
color: #10b981;
font-family: monospace;
}
.commit-msg {
font-size: 0.8rem;
font-weight: bold;
}
.commit-files {
display: flex;
gap: 2px;
}
.tiny-file {
font-size: 0.6rem;
}
/* Arrows */
.flow-arrow {
@@ -418,25 +506,70 @@ const commitFiles = () => {
width: 40px;
color: var(--vp-c-text-3);
}
.arrow-line { width: 100%; height: 2px; background: currentColor; }
.arrow-label { font-size: 0.7rem; margin: 4px 0; font-weight: bold; white-space: nowrap; }
.arrow-head { font-size: 0.8rem; }
.arrow-line {
width: 100%;
height: 2px;
background: currentColor;
}
.arrow-label {
font-size: 0.7rem;
margin: 4px 0;
font-weight: bold;
white-space: nowrap;
}
.arrow-head {
font-size: 0.8rem;
}
/* Transitions */
.file-pop-enter-active, .file-pop-leave-active { transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.file-pop-enter-from { opacity: 0; transform: scale(0.5); }
.file-pop-leave-to { opacity: 0; transform: scale(0); }
.file-pop-enter-active,
.file-pop-leave-active {
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.file-pop-enter-from {
opacity: 0;
transform: scale(0.5);
}
.file-pop-leave-to {
opacity: 0;
transform: scale(0);
}
.file-drop-enter-active, .file-drop-leave-active { transition: all 0.3s ease; }
.file-drop-enter-from { opacity: 0; transform: translateY(-20px); }
.file-drop-leave-to { opacity: 0; transform: translateX(20px); }
.file-drop-enter-active,
.file-drop-leave-active {
transition: all 0.3s ease;
}
.file-drop-enter-from {
opacity: 0;
transform: translateY(-20px);
}
.file-drop-leave-to {
opacity: 0;
transform: translateX(20px);
}
.drawer-slide-enter-active { transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
.drawer-slide-enter-from { opacity: 0; transform: translateX(50px); }
.drawer-slide-enter-active {
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.drawer-slide-enter-from {
opacity: 0;
transform: translateX(50px);
}
@media (max-width: 768px) {
.scene { flex-direction: column; min-width: auto; }
.flow-arrow { transform: rotate(90deg); margin: 10px 0; width: 100%; align-items: center; }
.arrow-line { width: 2px; height: 20px; }
.scene {
flex-direction: column;
min-width: auto;
}
.flow-arrow {
transform: rotate(90deg);
margin: 10px 0;
width: 100%;
align-items: center;
}
.arrow-line {
width: 2px;
height: 20px;
}
}
</style>
@@ -15,15 +15,21 @@
<button @click="makeCommit" :disabled="!inited" class="action-btn">
提交
</button>
<button @click="createBranch" :disabled="!inited || hasBranch" class="action-btn">
<button
@click="createBranch"
:disabled="!inited || hasBranch"
class="action-btn"
>
🌿 创建分支
</button>
<button @click="mergeBranch" :disabled="!hasBranch || merging" class="action-btn">
<button
@click="mergeBranch"
:disabled="!hasBranch || merging"
class="action-btn"
>
🔀 合并分支
</button>
<button @click="reset" class="action-btn secondary">
🔄 重置
</button>
<button @click="reset" class="action-btn secondary">🔄 重置</button>
</div>
<!-- 提交历史可视化 -->
@@ -31,18 +37,62 @@
<div class="graph-container">
<svg viewBox="0 0 400 150" class="git-graph">
<!-- 主分支线 -->
<line x1="50" y1="60" x2="350" y2="60" stroke="#3b82f6" stroke-width="3" />
<line
x1="50"
y1="60"
x2="350"
y2="60"
stroke="#3b82f6"
stroke-width="3"
/>
<!-- 分支线 -->
<line v-if="hasBranch" x1="150" y1="60" x2="150" y2="100" stroke="#10b981" stroke-width="3" />
<line v-if="hasBranch" x1="150" y1="100" x2="300" y2="100" stroke="#10b981" stroke-width="3" />
<line
v-if="hasBranch"
x1="150"
y1="60"
x2="150"
y2="100"
stroke="#10b981"
stroke-width="3"
/>
<line
v-if="hasBranch"
x1="150"
y1="100"
x2="300"
y2="100"
stroke="#10b981"
stroke-width="3"
/>
<!-- 合并线 -->
<path v-if="merging" d="M 300 100 Q 320 80, 320 60" fill="none" stroke="#f59e0b" stroke-width="2" stroke-dasharray="5,5" />
<path
v-if="merging"
d="M 300 100 Q 320 80, 320 60"
fill="none"
stroke="#f59e0b"
stroke-width="2"
stroke-dasharray="5,5"
/>
<!-- 提交节点 -->
<circle v-for="(commit, i) in mainCommits" :key="'main-'+i" :cx="80 + i * 60" cy="60" r="10" fill="#3b82f6" />
<circle v-for="(commit, i) in branchCommits" :key="'branch-'+i" :cx="200 + i * 60" cy="100" r="10" fill="#10b981" />
<circle
v-for="(commit, i) in mainCommits"
:key="'main-' + i"
:cx="80 + i * 60"
cy="60"
r="10"
fill="#3b82f6"
/>
<circle
v-for="(commit, i) in branchCommits"
:key="'branch-' + i"
:cx="200 + i * 60"
cy="100"
r="10"
fill="#10b981"
/>
</svg>
</div>
</div>
@@ -65,7 +115,9 @@
<!-- 说明 -->
<div class="info-box">
<p><strong>💡 工作流程:</strong> 初始化 提交 创建分支 开发 合并</p>
<p>
<strong>💡 工作流程:</strong> 初始化 提交 创建分支 开发 合并
</p>
</div>
</div>
</template>