feat(docs): add Netlify deployment guide and data encoding demos
- Add Netlify deployment section with form handling and functions examples - Replace old Git demos with new interactive components - Add comprehensive data encoding visualization demos - Update comparison table with Netlify information
This commit is contained in:
@@ -1,207 +0,0 @@
|
||||
<template>
|
||||
<div class="branch-demo">
|
||||
<div class="panel">
|
||||
<div class="controls">
|
||||
<button
|
||||
:disabled="inited || mergePending"
|
||||
class="btn"
|
||||
@click="init"
|
||||
>
|
||||
初始化
|
||||
</button>
|
||||
<button
|
||||
:disabled="!inited || mergePending"
|
||||
class="btn"
|
||||
@click="commit"
|
||||
>
|
||||
提交
|
||||
</button>
|
||||
<button
|
||||
:disabled="!inited || hasBranch"
|
||||
class="btn"
|
||||
@click="branch"
|
||||
>
|
||||
创建分支
|
||||
</button>
|
||||
<button
|
||||
:disabled="!hasBranch || mergePending"
|
||||
class="btn"
|
||||
@click="prepareMerge"
|
||||
>
|
||||
准备合并
|
||||
</button>
|
||||
<button
|
||||
:disabled="!mergePending"
|
||||
class="btn"
|
||||
@click="finishMerge"
|
||||
>
|
||||
完成合并
|
||||
</button>
|
||||
<button
|
||||
class="btn secondary"
|
||||
@click="reset"
|
||||
>
|
||||
重置
|
||||
</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"
|
||||
:key="i"
|
||||
:cx="60 + i * 50"
|
||||
cy="40"
|
||||
r="8"
|
||||
fill="#3b82f6"
|
||||
/>
|
||||
<circle
|
||||
v-for="(c, i) in feat"
|
||||
:key="i"
|
||||
:cx="180 + i * 50"
|
||||
cy="80"
|
||||
r="8"
|
||||
fill="#10b981"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<span>提交: {{ main.length }}</span>
|
||||
<span>分支: {{ hasBranch ? 2 : 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 分支策略:</strong> 并行开发,互不干扰,最后合并</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const inited = ref(false)
|
||||
const hasBranch = ref(false)
|
||||
const mergePending = 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 prepareMerge = () => {
|
||||
if (!hasBranch.value) return
|
||||
mergePending.value = true
|
||||
}
|
||||
const finishMerge = () => {
|
||||
if (!mergePending.value) return
|
||||
main.value.push(1)
|
||||
hasBranch.value = false
|
||||
feat.value = []
|
||||
mergePending.value = false
|
||||
}
|
||||
const reset = () => {
|
||||
inited.value = false
|
||||
hasBranch.value = false
|
||||
mergePending.value = false
|
||||
main.value = []
|
||||
feat.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.branch-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem 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: 6px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.graph svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.status {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
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>
|
||||
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<div class="gb-root">
|
||||
<!-- Terminal -->
|
||||
<div class="gb-terminal">
|
||||
<div class="term-bar">
|
||||
<span class="dot r" /><span class="dot y" /><span class="dot g" />
|
||||
<span class="term-title">~/project
|
||||
<span class="branch-tag">({{ branch }})</span>
|
||||
</span>
|
||||
</div>
|
||||
<div ref="termEl" class="term-body">
|
||||
<div v-for="(l, i) in lines" :key="i" class="t-line">
|
||||
<span v-if="l.kind === 'cmd'" class="t-ps">$ </span>
|
||||
<span :class="'t-' + l.kind">{{ l.text }}</span>
|
||||
</div>
|
||||
<div class="t-line">
|
||||
<span class="t-ps">$ </span>
|
||||
<span class="t-typing">{{ typing }}<span class="t-cur">▋</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="gb-btns">
|
||||
<button
|
||||
v-for="op in ops"
|
||||
:key="op.id"
|
||||
:disabled="running || !op.ok()"
|
||||
:class="['gb-btn', { 'gb-btn--on': active === op.id, 'gb-btn--dim': !op.ok() }]"
|
||||
@click="run(op)"
|
||||
>
|
||||
<code>{{ op.cmd }}</code>
|
||||
</button>
|
||||
<button class="gb-btn gb-btn--reset" :disabled="running" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<!-- SVG Graph -->
|
||||
<div class="gb-graph-wrap">
|
||||
<div class="gb-legend">
|
||||
<span class="leg-item"><span class="leg-dot main-c" />main 主分支</span>
|
||||
<span v-if="featLog.length" class="leg-item"><span class="leg-dot feat-c" />feature-login 功能分支</span>
|
||||
<span v-if="mergeNode" class="leg-item"><span class="leg-dot merge-c" />Merge 合并节点</span>
|
||||
<span class="leg-item head-leg"><span class="leg-head">HEAD</span> 你当前所在位置</span>
|
||||
</div>
|
||||
|
||||
<div class="svg-scroll">
|
||||
<svg :width="svgW" :height="svgH" class="gb-svg">
|
||||
<!-- ── 连接线 ── -->
|
||||
|
||||
<!-- main 主轨道横线 -->
|
||||
<line
|
||||
v-if="mainLog.length > 1"
|
||||
:x1="nodeX(0) + NODE_R"
|
||||
:y1="MAIN_Y"
|
||||
:x2="nodeX(mainLog.length - 1) - NODE_R"
|
||||
:y2="MAIN_Y"
|
||||
stroke="#5b9cf6" stroke-width="2.5"
|
||||
/>
|
||||
|
||||
<!-- 分叉弧线:从 main 最后一个原始节点向下弯到 feat 第一个节点 -->
|
||||
<path
|
||||
v-if="featLog.length"
|
||||
:d="forkPath"
|
||||
fill="none" stroke="#f9e2af" stroke-width="2.5" stroke-linecap="round"
|
||||
/>
|
||||
|
||||
<!-- feature 轨道横线 -->
|
||||
<line
|
||||
v-if="featLog.length > 1"
|
||||
:x1="featNodeX(0) + NODE_R"
|
||||
:y1="FEAT_Y"
|
||||
:x2="featNodeX(featLog.length - 1) - NODE_R"
|
||||
:y2="FEAT_Y"
|
||||
stroke="#f9e2af" stroke-width="2.5"
|
||||
/>
|
||||
|
||||
<!-- merge 收束弧线:从 feat 最后节点弯回 main merge 节点 -->
|
||||
<path
|
||||
v-if="mergeNode"
|
||||
:d="mergePath"
|
||||
fill="none" stroke="#a6e3a1" stroke-width="2.5" stroke-linecap="round"
|
||||
/>
|
||||
|
||||
<!-- ── 节点 ── -->
|
||||
|
||||
<!-- main 节点 -->
|
||||
<g v-for="(c, i) in mainLog" :key="'m'+i">
|
||||
<circle
|
||||
:cx="nodeX(i)"
|
||||
:cy="MAIN_Y"
|
||||
:r="c.merge ? NODE_R + 2 : NODE_R"
|
||||
:fill="c.merge ? '#a6e3a1' : '#5b9cf6'"
|
||||
stroke="#1a1a2e" stroke-width="2"
|
||||
/>
|
||||
<!-- HEAD 标签 -->
|
||||
<g v-if="branch === 'main' && i === mainLog.length - 1">
|
||||
<rect
|
||||
:x="nodeX(i) - 18"
|
||||
:y="MAIN_Y - NODE_R - 20"
|
||||
width="36" height="14"
|
||||
rx="3" fill="#5b9cf6" opacity="0.85"
|
||||
/>
|
||||
<text
|
||||
:x="nodeX(i)"
|
||||
:y="MAIN_Y - NODE_R - 10"
|
||||
text-anchor="middle" font-size="9"
|
||||
font-family="monospace" fill="white" font-weight="bold"
|
||||
>HEAD</text>
|
||||
</g>
|
||||
<!-- commit hash -->
|
||||
<text
|
||||
:x="nodeX(i)"
|
||||
:y="MAIN_Y + NODE_R + 14"
|
||||
text-anchor="middle" font-size="9"
|
||||
font-family="monospace" :fill="c.merge ? '#a6e3a1' : '#7f849c'"
|
||||
>{{ c.hash }}</text>
|
||||
<!-- commit msg -->
|
||||
<text
|
||||
:x="nodeX(i)"
|
||||
:y="MAIN_Y + NODE_R + 25"
|
||||
text-anchor="middle" font-size="9"
|
||||
fill="#64748b"
|
||||
>{{ c.shortMsg }}</text>
|
||||
</g>
|
||||
|
||||
<!-- feature 节点 -->
|
||||
<g v-for="(c, i) in featLog" :key="'f'+i">
|
||||
<circle
|
||||
:cx="featNodeX(i)"
|
||||
:cy="FEAT_Y"
|
||||
:r="NODE_R"
|
||||
fill="#f9e2af"
|
||||
stroke="#1a1a2e" stroke-width="2"
|
||||
/>
|
||||
<!-- HEAD 标签 -->
|
||||
<g v-if="branch === 'feature-login' && i === featLog.length - 1">
|
||||
<rect
|
||||
:x="featNodeX(i) - 18"
|
||||
:y="FEAT_Y + NODE_R + 4"
|
||||
width="36" height="14"
|
||||
rx="3" fill="#f9e2af" opacity="0.85"
|
||||
/>
|
||||
<text
|
||||
:x="featNodeX(i)"
|
||||
:y="FEAT_Y + NODE_R + 14"
|
||||
text-anchor="middle" font-size="9"
|
||||
font-family="monospace" fill="#1a1a2e" font-weight="bold"
|
||||
>HEAD</text>
|
||||
</g>
|
||||
<!-- hash & msg above -->
|
||||
<text
|
||||
:x="featNodeX(i)"
|
||||
:y="FEAT_Y - NODE_R - 14"
|
||||
text-anchor="middle" font-size="9"
|
||||
font-family="monospace" fill="#a89050"
|
||||
>{{ c.hash }}</text>
|
||||
<text
|
||||
:x="featNodeX(i)"
|
||||
:y="FEAT_Y - NODE_R - 3"
|
||||
text-anchor="middle" font-size="9"
|
||||
fill="#a89050"
|
||||
>{{ c.shortMsg }}</text>
|
||||
</g>
|
||||
|
||||
<!-- 分支名标签 -->
|
||||
<text
|
||||
:x="svgPad"
|
||||
:y="MAIN_Y - NODE_R - 26"
|
||||
font-size="10" font-family="monospace" fill="#5b9cf6" font-weight="bold"
|
||||
>main</text>
|
||||
<text
|
||||
v-if="featLog.length"
|
||||
:x="featNodeX(0)"
|
||||
:y="FEAT_Y + (branch==='feature-login' ? NODE_R + 26 : -NODE_R - 28)"
|
||||
font-size="10" font-family="monospace" fill="#f9e2af" font-weight="bold"
|
||||
text-anchor="middle"
|
||||
>feature-login</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hint" class="gb-hint">💡 {{ hint }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
|
||||
const NODE_R = 10
|
||||
const STEP = 100 // horizontal spacing between commits
|
||||
const svgPad = 50 // left padding
|
||||
const MAIN_Y = 70 // main track y
|
||||
const FEAT_Y = 170 // feature track y
|
||||
|
||||
const termEl = ref(null)
|
||||
const lines = ref([{ kind: 'dim', text: '# main 分支上已有 2 次提交,按步骤演示分支操作' }])
|
||||
const typing = ref('')
|
||||
const running = ref(false)
|
||||
const active = ref(null)
|
||||
const hint = ref('👆 依次点击上方命令按钮,观察下方分支图的变化')
|
||||
const branch = ref('main')
|
||||
|
||||
const mainLog = ref([
|
||||
{ hash: '9f3e1b2', shortMsg: 'init', merge: false },
|
||||
{ hash: 'c4d8a31', shortMsg: '首页', merge: false },
|
||||
])
|
||||
const featLog = ref([])
|
||||
const mergeNode = ref(false)
|
||||
let s = { created: false, c1: false, c2: false, merged: false }
|
||||
|
||||
// X position of the i-th main commit
|
||||
function nodeX(i) { return svgPad + i * STEP }
|
||||
|
||||
// fork point = last original main commit (before any merge)
|
||||
const forkIdx = computed(() => mainLog.value.filter(c => !c.merge).length - 1)
|
||||
|
||||
// X of feature commit i: starts one step after fork point
|
||||
function featNodeX(i) { return nodeX(forkIdx.value) + (i + 1) * STEP }
|
||||
|
||||
// SVG dimensions
|
||||
const svgW = computed(() => {
|
||||
const lastMain = nodeX(mainLog.value.length - 1)
|
||||
const lastFeat = featLog.value.length ? featNodeX(featLog.value.length - 1) : 0
|
||||
return Math.max(lastMain, lastFeat) + svgPad + 30
|
||||
})
|
||||
const svgH = computed(() => featLog.value.length ? 240 : 130)
|
||||
|
||||
// Arc from last original main node down to first feat node
|
||||
const forkPath = computed(() => {
|
||||
if (!featLog.value.length) return ''
|
||||
const x1 = nodeX(forkIdx.value)
|
||||
const y1 = MAIN_Y
|
||||
const x2 = featNodeX(0)
|
||||
const y2 = FEAT_Y
|
||||
// cubic bezier: go right then down
|
||||
return `M ${x1} ${y1} C ${x1 + 40} ${y1}, ${x2 - 20} ${y2}, ${x2} ${y2}`
|
||||
})
|
||||
|
||||
// Arc from last feat node back up to merge node on main
|
||||
const mergePath = computed(() => {
|
||||
if (!mergeNode.value || !featLog.value.length) return ''
|
||||
const x1 = featNodeX(featLog.value.length - 1)
|
||||
const y1 = FEAT_Y
|
||||
const mergeIdx = mainLog.value.length - 1
|
||||
const x2 = nodeX(mergeIdx)
|
||||
const y2 = MAIN_Y
|
||||
return `M ${x1} ${y1} C ${x1 + 30} ${y1}, ${x2 - 20} ${y2}, ${x2} ${y2}`
|
||||
})
|
||||
|
||||
const ops = [
|
||||
{
|
||||
id: 'create',
|
||||
cmd: 'git checkout -b feature-login',
|
||||
ok: () => !s.created,
|
||||
output: [
|
||||
{ kind: 'grn', text: "Switched to a new branch 'feature-login'" },
|
||||
],
|
||||
hint: '新分支创建了!它和 main 指向同一个提交,但是独立的"时间线"。现在你在 feature-login 上,main 的时间线不会动。',
|
||||
do: () => { s.created = true; branch.value = 'feature-login' },
|
||||
},
|
||||
{
|
||||
id: 'c1',
|
||||
cmd: 'git commit -m "feat: 登录表单"',
|
||||
ok: () => s.created && !s.c1,
|
||||
output: [
|
||||
{ kind: 'dim', text: '[feature-login e1a2b3c] feat: 登录表单' },
|
||||
{ kind: 'dim', text: ' 1 file changed, 38 insertions(+)' },
|
||||
],
|
||||
hint: '看图!feature-login 向右延伸了一个新节点,而 main 纹丝不动。这就是"平行宇宙"——两条线同时存在,互不影响。',
|
||||
do: () => { s.c1 = true; featLog.value.push({ hash: 'e1a2b3c', shortMsg: '登录表单' }) },
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
cmd: 'git commit -m "feat: 登录接口"',
|
||||
ok: () => s.c1 && !s.c2,
|
||||
output: [
|
||||
{ kind: 'dim', text: '[feature-login f4d5e6f] feat: 登录接口' },
|
||||
{ kind: 'dim', text: ' 1 file changed, 22 insertions(+)' },
|
||||
],
|
||||
hint: 'feature-login 又多了一个提交。此时它比 main 多了 2 个节点。功能开发完毕,准备合并回主线。',
|
||||
do: () => { s.c2 = true; featLog.value.push({ hash: 'f4d5e6f', shortMsg: '登录接口' }) },
|
||||
},
|
||||
{
|
||||
id: 'back',
|
||||
cmd: 'git checkout main',
|
||||
ok: () => s.c2 && branch.value !== 'main',
|
||||
output: [{ kind: 'grn', text: "Switched to branch 'main'" }],
|
||||
hint: '切回 main。HEAD 标签跳回到 main 最后的节点。feature-login 里写的代码,现在工作区完全看不到——两条线彻底隔离。',
|
||||
do: () => { branch.value = 'main' },
|
||||
},
|
||||
{
|
||||
id: 'merge',
|
||||
cmd: 'git merge feature-login',
|
||||
ok: () => s.c2 && branch.value === 'main' && !s.merged,
|
||||
output: [
|
||||
{ kind: 'dim', text: "Merge made by the 'ort' strategy." },
|
||||
{ kind: 'grn', text: ' login.js | 60 ++++++ 1 file changed' },
|
||||
],
|
||||
hint: '合并完成!看图:feature-login 的弧线收束回了 main,形成一个绿色合并节点。两条时间线重新汇合,登录功能进入主线。',
|
||||
do: () => {
|
||||
s.merged = true
|
||||
mergeNode.value = true
|
||||
mainLog.value.push({ hash: 'a9b8c7d', shortMsg: 'Merge', merge: true })
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
function scroll() { if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight }
|
||||
|
||||
async function run(op) {
|
||||
if (running.value) return
|
||||
running.value = true; active.value = op.id; hint.value = ''; typing.value = ''
|
||||
for (const ch of op.cmd) { typing.value += ch; await sleep(22) }
|
||||
await sleep(80)
|
||||
lines.value.push({ kind: 'cmd', text: op.cmd }); typing.value = ''
|
||||
await nextTick(); scroll(); await sleep(150)
|
||||
for (const l of op.output) { lines.value.push(l); await nextTick(); scroll(); await sleep(50) }
|
||||
op.do(); await sleep(100); hint.value = op.hint; running.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
lines.value = [{ kind: 'dim', text: '# main 分支上已有 2 次提交,按步骤演示分支操作' }]
|
||||
mainLog.value = [
|
||||
{ hash: '9f3e1b2', shortMsg: 'init', merge: false },
|
||||
{ hash: 'c4d8a31', shortMsg: '首页', merge: false },
|
||||
]
|
||||
featLog.value = []; branch.value = 'main'; mergeNode.value = false
|
||||
s = { created: false, c1: false, c2: false, merged: false }
|
||||
active.value = null
|
||||
hint.value = '👆 依次点击上方命令按钮,观察下方分支图的变化'
|
||||
typing.value = ''; running.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gb-root {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px; overflow: hidden;
|
||||
background: var(--vp-c-bg-soft); margin: 1rem 0; font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Terminal */
|
||||
.gb-terminal { background: #141420; }
|
||||
.term-bar {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 7px 12px; background: #1e1e2e;
|
||||
}
|
||||
.dot { width: 11px; height: 11px; border-radius: 50%; }
|
||||
.dot.r { background: #ff5f57; } .dot.y { background: #febc2e; } .dot.g { background: #28c840; }
|
||||
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
|
||||
.branch-tag { color: #cba6f7; font-weight: 600; }
|
||||
.term-body {
|
||||
min-height: 100px; max-height: 140px; overflow-y: auto;
|
||||
padding: 0.7rem 1rem;
|
||||
font-family: 'Menlo','Monaco',monospace; font-size: 0.76rem; line-height: 1.6; color: #cdd6f4;
|
||||
}
|
||||
.t-line { display: flex; }
|
||||
.t-ps { color: #a6e3a1; flex-shrink: 0; }
|
||||
.t-cmd { color: #cdd6f4; } .t-dim { color: #585b70; } .t-grn { color: #a6e3a1; }
|
||||
.t-typing { color: #cdd6f4; }
|
||||
.t-cur { animation: blink 1s step-end infinite; }
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
||||
|
||||
/* Buttons */
|
||||
.gb-btns {
|
||||
display: flex; flex-wrap: wrap; gap: 6px;
|
||||
padding: 8px 10px; background: #0d0d1a; border-top: 1px solid #2a2a3e;
|
||||
}
|
||||
.gb-btn {
|
||||
background: #1e1e2e; border: 1px solid #313244;
|
||||
border-radius: 5px; padding: 4px 9px; cursor: pointer; transition: border-color .2s;
|
||||
}
|
||||
.gb-btn code { font-size: 0.7rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
|
||||
.gb-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
|
||||
.gb-btn--on { border-color: var(--vp-c-brand) !important; }
|
||||
.gb-btn--on code { color: var(--vp-c-brand); }
|
||||
.gb-btn--dim { opacity: 0.3; cursor: not-allowed; }
|
||||
.gb-btn--reset { background: transparent; border-color: #313244; margin-left: auto; }
|
||||
.gb-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
|
||||
|
||||
/* Graph */
|
||||
.gb-graph-wrap {
|
||||
background: var(--vp-c-bg); border-top: 1px solid var(--vp-c-divider);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.gb-legend {
|
||||
display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px;
|
||||
font-size: 0.74rem; color: var(--vp-c-text-2);
|
||||
}
|
||||
.leg-item { display: flex; align-items: center; gap: 5px; }
|
||||
.leg-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.main-c { background: #5b9cf6; }
|
||||
.feat-c { background: #f9e2af; }
|
||||
.merge-c { background: #a6e3a1; }
|
||||
.leg-head {
|
||||
font-family: monospace; font-size: 0.68rem; font-weight: 700;
|
||||
background: #5b9cf655; color: #5b9cf6; padding: 1px 5px; border-radius: 3px;
|
||||
}
|
||||
.head-leg { gap: 4px; }
|
||||
|
||||
.svg-scroll { overflow-x: auto; }
|
||||
.gb-svg { display: block; overflow: visible; }
|
||||
|
||||
.gb-hint {
|
||||
padding: 8px 12px; background: var(--vp-c-bg-alt);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.8rem; color: var(--vp-c-text-2); line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="gcc-root">
|
||||
<p class="gcc-desc">把这张表存起来,遇到忘了的命令随时查:</p>
|
||||
<div class="gcc-chart-wrap">
|
||||
<div class="chart-header">
|
||||
<span class="y-axis-label">使用频率</span>
|
||||
<div class="chart-area">
|
||||
<svg class="chart-svg" :viewBox="`0 0 ${chartWidth} ${height}`" preserveAspectRatio="none" :width="chartWidth" :height="height">
|
||||
<!-- Grid lines (horizontal) -->
|
||||
<line v-for="y in gridY" :key="y" :x1="padding.left" :y1="y" :x2="chartWidth - padding.right" :y2="y" class="grid-line" />
|
||||
<!-- Y axis labels (1-5) -->
|
||||
<text v-for="label in yLabels" :key="label.val" :x="padding.left - 8" :y="label.y" class="y-label">{{ label.val }}</text>
|
||||
<!-- Bars -->
|
||||
<rect v-for="(row, i) in rows" :key="i" :x="barX(i)" :y="barY(row)" :width="barW" :height="barHeight(row)" class="bar-rect">
|
||||
<title>{{ row.cmd }} — {{ row.freqLabel || levelLabel(row.level) }}</title>
|
||||
</rect>
|
||||
<!-- X axis: 命令名 + 下方一行简短功能描述,旋转 -45° -->
|
||||
<g v-for="(row, i) in rows" :key="'label-'+i">
|
||||
<text
|
||||
:x="barX(i) + barW / 2"
|
||||
:y="labelY"
|
||||
class="x-label"
|
||||
text-anchor="end"
|
||||
:transform="`rotate(-45, ${barX(i) + barW / 2}, ${labelY})`"
|
||||
>
|
||||
{{ row.cmd }}
|
||||
</text>
|
||||
<text
|
||||
:x="barX(i) + barW / 2"
|
||||
:y="labelY + 26"
|
||||
class="x-desc"
|
||||
text-anchor="end"
|
||||
:transform="`rotate(-45, ${barX(i) + barW / 2}, ${labelY + 26})`"
|
||||
>
|
||||
{{ row.desc }}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="x-axis-label">命令 <span class="scroll-hint">(可左右滑动查看)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const rawRows = [
|
||||
{ cmd: 'git init', desc: '在当前目录初始化 Git 仓库', level: 0, freqLabel: '项目开始时一次' },
|
||||
{ cmd: 'git status', desc: '查看工作区和暂存区的状态', level: 5, freqLabel: '极高频' },
|
||||
{ cmd: 'git add <文件>', desc: '把指定文件放入暂存区', level: 5, freqLabel: '每次提交前' },
|
||||
{ cmd: 'git add .', desc: '把所有修改放入暂存区', level: 5, freqLabel: '' },
|
||||
{ cmd: 'git commit -m "..."', desc: '提交暂存区内容,附上说明', level: 5, freqLabel: '' },
|
||||
{ cmd: 'git push', desc: '推送到远程仓库', level: 5, freqLabel: '' },
|
||||
{ cmd: 'git pull', desc: '拉取远程最新内容', level: 5, freqLabel: '' },
|
||||
{ cmd: 'git log --oneline', desc: '查看简洁的提交历史', level: 4, freqLabel: '' },
|
||||
{ cmd: 'git checkout -b <分支名>', desc: '创建并切换到新分支', level: 4, freqLabel: '' },
|
||||
{ cmd: 'git checkout <分支名>', desc: '切换到已有分支', level: 4, freqLabel: '' },
|
||||
{ cmd: 'git clone <url>', desc: '克隆远程仓库到本地', level: 4, freqLabel: '' },
|
||||
{ cmd: 'git branch', desc: '查看所有本地分支', level: 3, freqLabel: '' },
|
||||
{ cmd: 'git merge <分支名>', desc: '将指定分支合并到当前分支', level: 3, freqLabel: '' },
|
||||
{ cmd: 'git stash', desc: '临时保存未提交的改动(切换任务时用)', level: 3, freqLabel: '' },
|
||||
{ cmd: 'git stash pop', desc: '恢复之前 stash 的改动', level: 3, freqLabel: '' },
|
||||
{ cmd: 'git reset HEAD~1', desc: '撤销最近一次提交(保留改动)', level: 3, freqLabel: '' },
|
||||
{ cmd: 'git diff', desc: '查看工作区和暂存区的具体差异', level: 3, freqLabel: '' },
|
||||
{ cmd: 'git branch -d <分支名>', desc: '删除已合并的分支', level: 2, freqLabel: '' },
|
||||
{ cmd: 'git remote add origin <url>', desc: '关联远程仓库(只做一次)', level: 0, freqLabel: '项目初始时' },
|
||||
]
|
||||
|
||||
const rows = computed(() => [...rawRows].sort((a, b) => b.level - a.level))
|
||||
|
||||
function levelLabel(level) {
|
||||
const map = { 5: '极高频', 4: '高频', 3: '中频', 2: '低频', 1: '很少', 0: '一次性' }
|
||||
return map[level] || ''
|
||||
}
|
||||
|
||||
const barW = 24
|
||||
const slotWidth = 88
|
||||
const chartWidth = computed(() => rawRows.length * slotWidth + 44 + 24)
|
||||
const height = 320
|
||||
const padding = { top: 12, right: 24, bottom: 150, left: 44 }
|
||||
const labelY = height - padding.bottom + 16
|
||||
|
||||
function barX(index) {
|
||||
return padding.left + index * slotWidth + (slotWidth - barW) / 2
|
||||
}
|
||||
function barHeight(row) {
|
||||
const plotHeight = height - padding.top - padding.bottom
|
||||
return Math.max(4, (row.level / 5) * plotHeight)
|
||||
}
|
||||
function barY(row) {
|
||||
const plotHeight = height - padding.top - padding.bottom
|
||||
return height - padding.bottom - barHeight(row)
|
||||
}
|
||||
|
||||
const gridY = computed(() => {
|
||||
const plotHeight = height - padding.top - padding.bottom
|
||||
const step = plotHeight / 5
|
||||
return Array.from({ length: 6 }, (_, i) => padding.top + i * step)
|
||||
})
|
||||
|
||||
const yLabels = computed(() => {
|
||||
const plotHeight = height - padding.top - padding.bottom
|
||||
const step = plotHeight / 5
|
||||
return Array.from({ length: 6 }, (_, i) => ({
|
||||
val: 5 - i,
|
||||
y: padding.top + i * step + 4,
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gcc-root {
|
||||
margin: 1rem 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.gcc-desc {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.gcc-chart-wrap {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
position: relative;
|
||||
}
|
||||
.y-axis-label {
|
||||
position: absolute;
|
||||
left: -26px;
|
||||
top: 50%;
|
||||
transform: rotate(-90deg) translateX(50%);
|
||||
transform-origin: left center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chart-area {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
min-height: 320px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.chart-svg {
|
||||
display: block;
|
||||
}
|
||||
.grid-line {
|
||||
stroke: var(--vp-c-divider);
|
||||
stroke-dasharray: 3 2;
|
||||
stroke-width: 1;
|
||||
}
|
||||
.y-label {
|
||||
font-size: 0.8rem;
|
||||
fill: var(--vp-c-text-3);
|
||||
text-anchor: end;
|
||||
}
|
||||
.bar-rect {
|
||||
fill: var(--vp-c-brand);
|
||||
rx: 2;
|
||||
transition: fill 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bar-rect:hover {
|
||||
fill: var(--vp-c-brand-2);
|
||||
}
|
||||
.x-label {
|
||||
font-size: 0.85rem;
|
||||
fill: var(--vp-c-text-2);
|
||||
}
|
||||
.x-desc {
|
||||
font-size: 0.72rem;
|
||||
fill: var(--vp-c-text-3);
|
||||
}
|
||||
.x-axis-label {
|
||||
margin-top: 0.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.scroll-hint {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
@@ -1,455 +0,0 @@
|
||||
<template>
|
||||
<div class="command-demo">
|
||||
<div class="panel">
|
||||
<div class="terminal">
|
||||
<div class="output">
|
||||
<div
|
||||
v-for="(line, i) in output"
|
||||
:key="i"
|
||||
:class="line.type"
|
||||
>
|
||||
<span
|
||||
v-if="line.type === 'command'"
|
||||
class="prompt"
|
||||
>$</span>
|
||||
<span v-html="line.text" />
|
||||
</div>
|
||||
<div
|
||||
v-if="output.length === 0"
|
||||
class="welcome"
|
||||
>
|
||||
输入命令开始学习 Git(建议先点“制造改动”,再跑 git status)
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">$</span>
|
||||
<input
|
||||
v-model="cmd"
|
||||
placeholder="(默认安全模式)请用下方按钮执行命令"
|
||||
class="cmd-input"
|
||||
:disabled="!freeMode"
|
||||
@keyup.enter="execute({ fromQuick: false })"
|
||||
>
|
||||
<button
|
||||
class="run-btn"
|
||||
:disabled="!freeMode"
|
||||
@click="execute({ fromQuick: false })"
|
||||
>
|
||||
运行
|
||||
</button>
|
||||
<button
|
||||
class="run-btn secondary"
|
||||
@click="clearOutput"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
<button
|
||||
class="run-btn secondary"
|
||||
@click="toggleFreeMode"
|
||||
>
|
||||
{{ freeMode ? '切回安全模式' : '开启自由模式' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-cmds">
|
||||
<button
|
||||
class="cmd-btn"
|
||||
@click="makeChanges"
|
||||
>
|
||||
制造改动
|
||||
</button>
|
||||
<button
|
||||
class="cmd-btn"
|
||||
@click="runCmd('git init')"
|
||||
>
|
||||
git init
|
||||
</button>
|
||||
<button
|
||||
class="cmd-btn"
|
||||
@click="runCmd('git status')"
|
||||
>
|
||||
git status
|
||||
</button>
|
||||
<button
|
||||
class="cmd-btn"
|
||||
@click="runCmd('git add .')"
|
||||
>
|
||||
git add .
|
||||
</button>
|
||||
<button
|
||||
class="cmd-btn"
|
||||
@click="runCmd(`git commit -m 'msg'`)"
|
||||
>
|
||||
git commit
|
||||
</button>
|
||||
<button
|
||||
class="cmd-btn"
|
||||
@click="runCmd('git log --oneline')"
|
||||
>
|
||||
git log
|
||||
</button>
|
||||
<button
|
||||
class="cmd-btn"
|
||||
@click="runCmd('git switch -c feat/demo')"
|
||||
>
|
||||
新分支
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<strong>💡 建议练习顺序:</strong> 制造改动 → status → add → status →
|
||||
commit → log
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const cmd = ref('')
|
||||
const output = ref([])
|
||||
const freeMode = ref(false)
|
||||
|
||||
// Minimal in-memory git state for learning purposes.
|
||||
const state = ref({
|
||||
inited: false,
|
||||
branch: 'main',
|
||||
commits: { main: [] },
|
||||
working: [], // modified files (not staged)
|
||||
staged: [] // staged files
|
||||
})
|
||||
|
||||
const pushLine = (type, text) => {
|
||||
output.value.push({ type, text: escapeHtml(text).replace(/\n/g, '<br />') })
|
||||
// keep the terminal from growing forever
|
||||
if (output.value.length > 60) output.value.splice(0, output.value.length - 60)
|
||||
}
|
||||
|
||||
const escapeHtml = (s) =>
|
||||
s
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('\"', '"')
|
||||
.replaceAll("'", ''')
|
||||
|
||||
const genHash = () => Math.random().toString(16).slice(2, 9)
|
||||
|
||||
const ensureRepo = () => {
|
||||
if (!state.value.inited) {
|
||||
pushLine(
|
||||
'error',
|
||||
'fatal: not a git repository (or any of the parent directories): .git'
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const statusText = () => {
|
||||
const s = state.value
|
||||
const lines = [`On branch ${s.branch}`]
|
||||
if (s.staged.length === 0 && s.working.length === 0) {
|
||||
lines.push('nothing to commit, working tree clean')
|
||||
return lines.join('\n')
|
||||
}
|
||||
if (s.staged.length) {
|
||||
lines.push('Changes to be committed:')
|
||||
s.staged.forEach((f) => lines.push(` modified: ${f}`))
|
||||
}
|
||||
if (s.working.length) {
|
||||
lines.push('Changes not staged for commit:')
|
||||
s.working.forEach((f) => lines.push(` modified: ${f}`))
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
const logText = () => {
|
||||
const s = state.value
|
||||
const list = s.commits[s.branch] || []
|
||||
if (!list.length)
|
||||
return 'fatal: your current branch does not have any commits yet'
|
||||
return list
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, 8)
|
||||
.map((c) => `${c.hash} ${c.msg}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
const branchText = () => {
|
||||
const s = state.value
|
||||
return Object.keys(s.commits)
|
||||
.sort()
|
||||
.map((b) => (b === s.branch ? `* ${b}` : ` ${b}`))
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
const makeChanges = () => {
|
||||
if (!state.value.inited) {
|
||||
pushLine('info', '提示:先 git init,再制造改动效果更真实。')
|
||||
return
|
||||
}
|
||||
const base = ['src/app.js', 'README.md', 'src/utils.js']
|
||||
state.value.working = base.slice(0, 1 + Math.floor(Math.random() * 3))
|
||||
// staged changes are independent
|
||||
pushLine(
|
||||
'success',
|
||||
`Edited ${state.value.working.length} file(s) (simulated).`
|
||||
)
|
||||
}
|
||||
|
||||
const execute = ({ fromQuick }) => {
|
||||
if (!freeMode.value && !fromQuick) {
|
||||
pushLine(
|
||||
'info',
|
||||
'当前是安全模式:请用下方按钮执行预设命令,避免“想当然”操作造成误解。'
|
||||
)
|
||||
cmd.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
const c = cmd.value.trim()
|
||||
if (!c) return
|
||||
|
||||
pushLine('command', c)
|
||||
|
||||
// Commands
|
||||
if (c === 'git init') {
|
||||
state.value.inited = true
|
||||
state.value.branch = 'main'
|
||||
state.value.commits = { main: [] }
|
||||
state.value.working = []
|
||||
state.value.staged = []
|
||||
pushLine('success', 'Initialized empty Git repository in ./.git/')
|
||||
} else if (c === 'git status') {
|
||||
if (!ensureRepo()) return
|
||||
pushLine('info', statusText())
|
||||
} else if (c === 'git add .' || c.startsWith('git add ')) {
|
||||
if (!ensureRepo()) return
|
||||
const s = state.value
|
||||
if (s.working.length === 0) {
|
||||
pushLine('info', 'Nothing specified, nothing added.')
|
||||
return
|
||||
}
|
||||
const toStage =
|
||||
c === 'git add .'
|
||||
? [...s.working]
|
||||
: [c.replace(/^git add\s+/, '').trim()].filter(Boolean)
|
||||
toStage.forEach((f) => {
|
||||
if (!s.staged.includes(f)) s.staged.push(f)
|
||||
s.working = s.working.filter((x) => x !== f)
|
||||
})
|
||||
pushLine('success', `Added ${toStage.length} path(s) to staging area.`)
|
||||
} else if (c.startsWith('git commit')) {
|
||||
if (!ensureRepo()) return
|
||||
const s = state.value
|
||||
if (s.staged.length === 0) {
|
||||
pushLine('error', 'nothing to commit (no changes added to commit)')
|
||||
return
|
||||
}
|
||||
const msgMatch = c.match(/-m\\s+\"([^\"]+)\"|-m\\s+'([^']+)'/)
|
||||
const msg = msgMatch?.[1] || msgMatch?.[2] || 'commit'
|
||||
const commit = { hash: genHash(), msg, files: [...s.staged] }
|
||||
if (!s.commits[s.branch]) s.commits[s.branch] = []
|
||||
s.commits[s.branch].push(commit)
|
||||
s.staged = []
|
||||
pushLine(
|
||||
'success',
|
||||
`[${s.branch} ${commit.hash}] ${msg}\\n ${commit.files.length} file(s) changed`
|
||||
)
|
||||
} else if (c === 'git log --oneline') {
|
||||
if (!ensureRepo()) return
|
||||
pushLine('info', logText())
|
||||
} else if (c === 'git branch') {
|
||||
if (!ensureRepo()) return
|
||||
pushLine('info', branchText())
|
||||
} else if (
|
||||
c.startsWith('git switch -c ') ||
|
||||
c.startsWith('git checkout -b ')
|
||||
) {
|
||||
if (!ensureRepo()) return
|
||||
const name = c.replace(/^git (switch -c|checkout -b)\s+/, '').trim()
|
||||
if (!name) {
|
||||
pushLine('error', 'fatal: you must specify a branch name')
|
||||
return
|
||||
}
|
||||
if (state.value.commits[name]) {
|
||||
pushLine('error', `fatal: A branch named '${name}' already exists.`)
|
||||
return
|
||||
}
|
||||
const base = state.value.commits[state.value.branch] || []
|
||||
state.value.commits[name] = [...base]
|
||||
state.value.branch = name
|
||||
pushLine('success', `Switched to a new branch '${name}'`)
|
||||
} else if (c.startsWith('git switch ') || c.startsWith('git checkout ')) {
|
||||
if (!ensureRepo()) return
|
||||
const name = c.replace(/^git (switch|checkout)\s+/, '').trim()
|
||||
if (!state.value.commits[name]) {
|
||||
pushLine(
|
||||
'error',
|
||||
`error: pathspec '${name}' did not match any file(s) known to git`
|
||||
)
|
||||
return
|
||||
}
|
||||
state.value.branch = name
|
||||
pushLine('success', `Switched to branch '${name}'`)
|
||||
} else if (c.startsWith('git restore')) {
|
||||
if (!ensureRepo()) return
|
||||
// Simplified restore for learning: clear working changes
|
||||
state.value.working = []
|
||||
pushLine('success', 'Restored working tree (simulated).')
|
||||
} else {
|
||||
pushLine(
|
||||
'error',
|
||||
'Unknown command (supported: init/status/add/commit/log/branch/switch/checkout/restore)'
|
||||
)
|
||||
}
|
||||
|
||||
cmd.value = ''
|
||||
}
|
||||
|
||||
const runCmd = (c) => {
|
||||
cmd.value = c
|
||||
execute({ fromQuick: true })
|
||||
}
|
||||
|
||||
const clearOutput = () => {
|
||||
output.value = []
|
||||
}
|
||||
|
||||
const toggleFreeMode = () => {
|
||||
freeMode.value = !freeMode.value
|
||||
cmd.value = ''
|
||||
pushLine(
|
||||
'info',
|
||||
freeMode.value
|
||||
? '已开启自由模式:现在可以手动输入命令(仍然只模拟,不会影响真实仓库)。'
|
||||
: '已切回安全模式:请使用下方按钮执行预设命令。'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.command-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.output {
|
||||
min-height: 150px;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.output .command {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.output .success {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.output .error {
|
||||
color: var(--vp-c-red-1, #ef4444);
|
||||
}
|
||||
.output .info {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.output .welcome {
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.input-line {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.cmd-input {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.875rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
|
||||
.cmd-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.45);
|
||||
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
|
||||
}
|
||||
|
||||
.run-btn {
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-brand);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.run-btn.secondary {
|
||||
background: var(--vp-c-bg);
|
||||
border-color: var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.quick-cmds {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cmd-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;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.cmd-btn:hover {
|
||||
background: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
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);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,529 @@
|
||||
<template>
|
||||
<div class="gc-root">
|
||||
<!-- Terminal -->
|
||||
<div class="gc-terminal">
|
||||
<div class="term-bar">
|
||||
<span class="dot r" /><span class="dot y" /><span class="dot g" />
|
||||
<span class="term-title">~/project (main)</span>
|
||||
</div>
|
||||
<div ref="termEl" class="term-body">
|
||||
<div v-for="(l, i) in lines" :key="i" class="t-line">
|
||||
<span v-if="l.kind === 'cmd'" class="t-ps">$ </span>
|
||||
<span :class="'t-' + l.kind">{{ l.text }}</span>
|
||||
</div>
|
||||
<div class="t-line">
|
||||
<span class="t-ps">$ </span>
|
||||
<span class="t-typing">{{ typing }}<span class="t-cur">▋</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="gc-btns">
|
||||
<button
|
||||
v-for="op in ops"
|
||||
:key="op.id"
|
||||
:disabled="running || !op.ok()"
|
||||
:class="['gc-btn', { 'gc-btn--on': active === op.id, 'gc-btn--dim': !op.ok() }]"
|
||||
@click="run(op)"
|
||||
>
|
||||
<code>{{ op.cmd }}</code>
|
||||
</button>
|
||||
<button class="gc-btn gc-btn--reset" :disabled="running" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<!-- 三区可视化 -->
|
||||
<div class="gc-three-areas">
|
||||
<div class="area-col area-work" :class="{ 'area-highlight': pulseArea === 'work' }">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📝</span>
|
||||
<span class="area-title">工作区</span>
|
||||
<span class="area-desc">Working Directory<br/>你正在改的文件</span>
|
||||
</div>
|
||||
<div class="area-body">
|
||||
<div class="area-label">Changes not staged for commit:</div>
|
||||
<template v-if="workFiles.length">
|
||||
<div v-for="f in workFiles" :key="f.name" class="file-row file-mod">
|
||||
<span class="file-badge">M</span>
|
||||
<code class="file-name">{{ f.name }}</code>
|
||||
<span class="file-state">未暂存</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="area-empty">(无未暂存修改)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="area-arrow" :class="{ 'arrow-lit': addDone }">
|
||||
<code class="arrow-cmd">git add</code>
|
||||
<span class="arrow-symbol">→</span>
|
||||
</div>
|
||||
|
||||
<div class="area-col area-stage" :class="{ 'area-highlight': pulseArea === 'stage' }">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📦</span>
|
||||
<span class="area-title">暂存区</span>
|
||||
<span class="area-desc">Staging Area<br/>准备这次提交的文件</span>
|
||||
</div>
|
||||
<div class="area-body">
|
||||
<div class="area-label">Changes to be committed:</div>
|
||||
<template v-if="stagedFiles.length">
|
||||
<div v-for="f in stagedFiles" :key="f.name" class="file-row file-staged">
|
||||
<span class="file-badge">A</span>
|
||||
<code class="file-name">{{ f.name }}</code>
|
||||
<span class="file-state">已暂存</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="area-empty">(空)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="area-arrow" :class="{ 'arrow-lit': commitDone }">
|
||||
<code class="arrow-cmd">git commit</code>
|
||||
<span class="arrow-symbol">→</span>
|
||||
</div>
|
||||
|
||||
<div class="area-col area-repo" :class="{ 'area-highlight': pulseArea === 'repo' }">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">🗄️</span>
|
||||
<span class="area-title">仓库</span>
|
||||
<span class="area-desc">Repository (.git)<br/>永久保存的版本</span>
|
||||
</div>
|
||||
<div class="area-body">
|
||||
<div class="area-label">已提交记录 (git log):</div>
|
||||
<template v-if="commits.length">
|
||||
<div v-for="(c, i) in commits" :key="c.hash" class="commit-row">
|
||||
<span class="commit-badge">✓</span>
|
||||
<code class="commit-hash">{{ c.hash }}</code>
|
||||
<span class="commit-msg">{{ c.msg }}</span>
|
||||
<span v-if="i === 0" class="commit-head">HEAD</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="area-empty">(无提交)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hint -->
|
||||
<div v-if="hint" class="gc-hint">💡 {{ hint }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
|
||||
const termEl = ref(null)
|
||||
const lines = ref([{ kind: 'dim', text: '# 你刚改了 3 个文件,现在演示 add → commit 流程' }])
|
||||
const typing = ref('')
|
||||
const running = ref(false)
|
||||
const active = ref(null)
|
||||
const hint = ref('点击下方命令按钮,按顺序执行。观察上方三区里文件如何随命令移动。')
|
||||
const pulseArea = ref(null)
|
||||
|
||||
const files = ref([
|
||||
{ name: 'login.js', staged: false, committed: false },
|
||||
{ name: 'style.css', staged: false, committed: false },
|
||||
{ name: 'debug.log', staged: false, committed: false },
|
||||
])
|
||||
const commits = ref([{ hash: '9f3e1b2', msg: 'init: 项目初始化' }])
|
||||
|
||||
// 工作区:未暂存且未提交的修改(git status 里红色的)
|
||||
const workFiles = computed(() =>
|
||||
files.value.filter(f => !f.staged && !f.committed)
|
||||
)
|
||||
// 暂存区:已暂存但还没提交的(git status 里绿色的)
|
||||
const stagedFiles = computed(() =>
|
||||
files.value.filter(f => f.staged && !f.committed)
|
||||
)
|
||||
|
||||
let addDone = false, commitDone = false
|
||||
|
||||
const ops = [
|
||||
{
|
||||
id: 'status',
|
||||
cmd: 'git status',
|
||||
ok: () => true,
|
||||
output: [
|
||||
{ kind: 'dim', text: 'On branch main' },
|
||||
{ kind: 'dim', text: '' },
|
||||
{ kind: 'dim', text: 'Changes not staged for commit:' },
|
||||
{ kind: 'red', text: ' modified: login.js' },
|
||||
{ kind: 'red', text: ' modified: style.css' },
|
||||
{ kind: 'red', text: ' modified: debug.log' },
|
||||
{ kind: 'dim', text: '' },
|
||||
{ kind: 'dim', text: 'no changes added to commit (use "git add")' },
|
||||
],
|
||||
hint: '红色 = 改了但还没暂存。三区里可以看到:3 个文件都在「工作区」,暂存区是空的。先用 git status 看清楚状态,再决定下一步。',
|
||||
do: () => { pulseArea.value = 'work' },
|
||||
},
|
||||
{
|
||||
id: 'add',
|
||||
cmd: 'git add login.js style.css',
|
||||
ok: () => !addDone,
|
||||
output: [
|
||||
{ kind: 'dim', text: '# git add 只加你指定的文件,debug.log 跳过' },
|
||||
{ kind: 'dim', text: '' },
|
||||
{ kind: 'dim', text: 'On branch main' },
|
||||
{ kind: 'dim', text: '' },
|
||||
{ kind: 'dim', text: 'Changes to be committed:' },
|
||||
{ kind: 'grn', text: ' modified: login.js' },
|
||||
{ kind: 'grn', text: ' modified: style.css' },
|
||||
{ kind: 'dim', text: '' },
|
||||
{ kind: 'red', text: 'Untracked files:' },
|
||||
{ kind: 'red', text: ' debug.log ← 没 add,不会提交' },
|
||||
],
|
||||
hint: '绿色 = 进入暂存区。观察:login.js 和 style.css 从工作区「搬进」了暂存区;debug.log 仍留在工作区(未暂存),不会参与这次提交。',
|
||||
do: () => {
|
||||
addDone = true
|
||||
files.value[0].staged = true
|
||||
files.value[1].staged = true
|
||||
pulseArea.value = 'stage'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'commit',
|
||||
cmd: 'git commit -m "feat: 添加登录功能"',
|
||||
ok: () => addDone && !commitDone,
|
||||
output: [
|
||||
{ kind: 'dim', text: '[main a1b2c3d] feat: 添加登录功能' },
|
||||
{ kind: 'dim', text: ' 2 files changed, 47 insertions(+)' },
|
||||
{ kind: 'dim', text: ' create mode 100644 login.js' },
|
||||
{ kind: 'dim', text: ' create mode 100644 style.css' },
|
||||
],
|
||||
hint: 'commit 成功!暂存区里的内容被「封存」进仓库,形成新的一条提交记录。暂存区变空;debug.log 仍在工作区,不受影响。',
|
||||
do: () => {
|
||||
commitDone = true
|
||||
files.value[0].staged = false
|
||||
files.value[0].committed = true
|
||||
files.value[1].staged = false
|
||||
files.value[1].committed = true
|
||||
commits.value.unshift({ hash: 'a1b2c3d', msg: 'feat: 添加登录功能' })
|
||||
pulseArea.value = 'repo'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'log',
|
||||
cmd: 'git log --oneline',
|
||||
ok: () => commitDone,
|
||||
output: [
|
||||
{ kind: 'yel', text: 'a1b2c3d (HEAD -> main) feat: 添加登录功能' },
|
||||
{ kind: 'yel', text: '9f3e1b2 init: 项目初始化' },
|
||||
],
|
||||
hint: '每行一个 commit,最新的在最上面。仓库区里可以看到完整的历史时间轴;工作区里只剩 debug.log(未提交的临时文件)。',
|
||||
do: () => { pulseArea.value = 'repo' },
|
||||
},
|
||||
{
|
||||
id: 'status2',
|
||||
cmd: 'git status',
|
||||
ok: () => commitDone,
|
||||
output: [
|
||||
{ kind: 'dim', text: 'On branch main' },
|
||||
{ kind: 'dim', text: '' },
|
||||
{ kind: 'dim', text: 'Changes not staged for commit:' },
|
||||
{ kind: 'red', text: ' modified: debug.log' },
|
||||
{ kind: 'dim', text: '' },
|
||||
{ kind: 'dim', text: 'no changes added to commit (use "git add")' },
|
||||
],
|
||||
hint: '提交后:login.js 和 style.css 已进仓库,工作区里只剩 debug.log 的修改。红色 = 改了但还没暂存,下次提交前可再 git add。',
|
||||
do: () => { pulseArea.value = 'work' },
|
||||
},
|
||||
]
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
async function run(op) {
|
||||
if (running.value) return
|
||||
running.value = true
|
||||
active.value = op.id
|
||||
hint.value = ''
|
||||
typing.value = ''
|
||||
pulseArea.value = null
|
||||
|
||||
for (const ch of op.cmd) {
|
||||
typing.value += ch
|
||||
await sleep(22)
|
||||
}
|
||||
await sleep(80)
|
||||
lines.value.push({ kind: 'cmd', text: op.cmd })
|
||||
typing.value = ''
|
||||
await nextTick()
|
||||
scroll()
|
||||
await sleep(150)
|
||||
|
||||
for (const l of op.output) {
|
||||
lines.value.push(l)
|
||||
await nextTick()
|
||||
scroll()
|
||||
await sleep(50)
|
||||
}
|
||||
|
||||
op.do()
|
||||
await sleep(120)
|
||||
hint.value = op.hint
|
||||
running.value = false
|
||||
setTimeout(() => { pulseArea.value = null }, 1500)
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight
|
||||
}
|
||||
|
||||
function reset() {
|
||||
lines.value = [{ kind: 'dim', text: '# 你刚改了 3 个文件,现在演示 add → commit 流程' }]
|
||||
files.value.forEach(f => { f.staged = false; f.committed = false })
|
||||
commits.value = [{ hash: '9f3e1b2', msg: 'init: 项目初始化' }]
|
||||
addDone = false
|
||||
commitDone = false
|
||||
active.value = null
|
||||
pulseArea.value = null
|
||||
hint.value = '点击下方命令按钮,按顺序执行。观察上方三区里文件如何随命令移动。'
|
||||
typing.value = ''
|
||||
running.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gc-root {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 1rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Terminal */
|
||||
.gc-terminal { background: #141420; }
|
||||
.term-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 7px 12px;
|
||||
background: #1e1e2e;
|
||||
}
|
||||
.dot { width: 11px; height: 11px; border-radius: 50%; }
|
||||
.dot.r { background: #ff5f57; }
|
||||
.dot.y { background: #febc2e; }
|
||||
.dot.g { background: #28c840; }
|
||||
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
|
||||
|
||||
.term-body {
|
||||
min-height: 140px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 0.8rem 1rem;
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.65;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
.t-line { display: flex; }
|
||||
.t-ps { color: #a6e3a1; flex-shrink: 0; }
|
||||
.t-cmd { color: #cdd6f4; }
|
||||
.t-dim { color: #585b70; }
|
||||
.t-red { color: #f38ba8; }
|
||||
.t-grn { color: #a6e3a1; }
|
||||
.t-yel { color: #89b4fa; }
|
||||
.t-typing { color: #cdd6f4; }
|
||||
.t-cur { animation: blink 1s step-end infinite; }
|
||||
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
||||
|
||||
/* Buttons */
|
||||
.gc-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
background: #0d0d1a;
|
||||
border-top: 1px solid #2a2a3e;
|
||||
}
|
||||
.gc-btn {
|
||||
background: #1e1e2e;
|
||||
border: 1px solid #313244;
|
||||
border-radius: 5px;
|
||||
padding: 4px 9px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.gc-btn code { font-size: 0.7rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
|
||||
.gc-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
|
||||
.gc-btn--on { border-color: var(--vp-c-brand) !important; }
|
||||
.gc-btn--on code { color: var(--vp-c-brand); }
|
||||
.gc-btn--dim { opacity: 0.3; cursor: not-allowed; }
|
||||
.gc-btn--reset {
|
||||
background: transparent;
|
||||
border-color: #313244;
|
||||
margin-left: auto;
|
||||
}
|
||||
.gc-btn--reset code { display: none; }
|
||||
.gc-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
|
||||
|
||||
/* 三区布局 */
|
||||
.gc-three-areas {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr auto 1fr;
|
||||
gap: 0;
|
||||
align-items: stretch;
|
||||
padding: 12px 14px;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
min-height: 180px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.gc-three-areas {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
}
|
||||
.area-arrow { transform: rotate(90deg); justify-self: center; }
|
||||
}
|
||||
|
||||
.area-col {
|
||||
border: 1.5px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: border-color 0.25s, box-shadow 0.25s;
|
||||
}
|
||||
.area-col.area-highlight {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 14%, transparent);
|
||||
}
|
||||
.area-work { border-left: 4px solid #f38ba8; }
|
||||
.area-stage { border-left: 4px solid #a6e3a1; }
|
||||
.area-repo { border-left: 4px solid #5b9cf6; }
|
||||
|
||||
.area-header {
|
||||
padding: 6px 10px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.area-icon { font-size: 1rem; margin-right: 4px; }
|
||||
.area-title {
|
||||
font-weight: 700;
|
||||
font-size: 0.88rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.area-desc {
|
||||
display: block;
|
||||
font-size: 0.68rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 2px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.area-body {
|
||||
padding: 8px 10px;
|
||||
flex: 1;
|
||||
min-height: 72px;
|
||||
}
|
||||
.area-label {
|
||||
font-size: 0.68rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 6px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.area-empty {
|
||||
font-size: 0.74rem;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.file-row,
|
||||
.commit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.76rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.file-row:last-child,
|
||||
.commit-row:last-child { margin-bottom: 0; }
|
||||
.file-mod {
|
||||
background: #f38ba818;
|
||||
border-left: 3px solid #f38ba8;
|
||||
}
|
||||
.file-staged {
|
||||
background: #a6e3a118;
|
||||
border-left: 3px solid #a6e3a1;
|
||||
}
|
||||
.file-badge {
|
||||
font-weight: 700;
|
||||
font-size: 0.72rem;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.file-mod .file-badge { color: #f38ba8; }
|
||||
.file-staged .file-badge { color: #a6e3a1; }
|
||||
.file-name { font-family: monospace; color: var(--vp-c-text-1); }
|
||||
.file-state {
|
||||
margin-left: auto;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.commit-row {
|
||||
background: #5b9cf618;
|
||||
border-left: 3px solid #5b9cf6;
|
||||
}
|
||||
.commit-badge { color: #5b9cf6; font-weight: 700; flex-shrink: 0; }
|
||||
.commit-hash {
|
||||
font-family: monospace;
|
||||
font-size: 0.7rem;
|
||||
color: #5b9cf6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.commit-msg {
|
||||
font-size: 0.72rem;
|
||||
color: var(--vp-c-text-2);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.commit-head {
|
||||
font-size: 0.64rem;
|
||||
font-family: monospace;
|
||||
font-weight: 700;
|
||||
background: #5b9cf6;
|
||||
color: #fff;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.area-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.area-arrow.arrow-lit { opacity: 1; }
|
||||
.arrow-cmd {
|
||||
font-size: 0.66rem;
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-brand);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.arrow-symbol {
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-brand);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.gc-hint {
|
||||
padding: 10px 12px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<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><<<<<<< 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>>>>>>>>> feature
|
||||
</div>
|
||||
<div class="line normal">
|
||||
<span class="ln">8</span> console.log('Bye');
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="resolve('current')"
|
||||
>
|
||||
保留当前
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="resolve('incoming')"
|
||||
>
|
||||
保留传入
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="resolve('manual')"
|
||||
>
|
||||
手动合并
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 解决冲突:</strong> 选择保留哪个版本,或手动编辑合并</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
const resolved = ref(false)
|
||||
const resolve = (choice) => {
|
||||
resolved.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.conflict-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.editor {
|
||||
background: #1f2937;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
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: 0.75rem;
|
||||
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>
|
||||
@@ -1,264 +0,0 @@
|
||||
<template>
|
||||
<div class="remote-demo">
|
||||
<div class="panel">
|
||||
<div class="repos">
|
||||
<div class="repo">
|
||||
<div class="header">
|
||||
💻 本地
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="badge">main</span>
|
||||
<span class="hint"> Ahead {{ ahead }} / Behind {{ behind }} </span>
|
||||
</div>
|
||||
<div class="commits">
|
||||
<div
|
||||
v-for="c in local"
|
||||
:key="c"
|
||||
class="commit-dot"
|
||||
>
|
||||
<span class="dot local" />
|
||||
<span class="hash">{{ c.substring(0, 6) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="local.length === 0"
|
||||
class="empty"
|
||||
>
|
||||
无
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sync">
|
||||
⇄
|
||||
</div>
|
||||
|
||||
<div class="repo">
|
||||
<div class="header">
|
||||
☁️ 远程
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="badge">origin/main</span>
|
||||
<span class="hint">模拟队友提交在这里发生</span>
|
||||
</div>
|
||||
<div class="commits">
|
||||
<div
|
||||
v-for="c in remote"
|
||||
:key="c"
|
||||
class="commit-dot"
|
||||
>
|
||||
<span class="dot remote" />
|
||||
<span class="hash">{{ c.substring(0, 6) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="remote.length === 0"
|
||||
class="empty"
|
||||
>
|
||||
无
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button
|
||||
class="btn"
|
||||
@click="localCommit"
|
||||
>
|
||||
本地提交
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="remoteCommit"
|
||||
>
|
||||
远程新增提交
|
||||
</button>
|
||||
<button
|
||||
:disabled="local.length <= remote.length"
|
||||
class="btn"
|
||||
@click="push"
|
||||
>
|
||||
git push
|
||||
</button>
|
||||
<button
|
||||
:disabled="behind === 0"
|
||||
class="btn"
|
||||
@click="pull"
|
||||
>
|
||||
git pull
|
||||
</button>
|
||||
<button
|
||||
class="btn secondary"
|
||||
@click="reset"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<strong>💡 远程协作:</strong> 你本地落后(Behind)就
|
||||
pull,你本地领先(Ahead)就 push。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
const local = ref([])
|
||||
const remote = ref([])
|
||||
|
||||
const localCommit = () => {
|
||||
local.value.push(Math.random().toString(16).substr(2, 7))
|
||||
}
|
||||
|
||||
const remoteCommit = () => {
|
||||
remote.value.push(Math.random().toString(16).substr(2, 7))
|
||||
}
|
||||
|
||||
const push = () => {
|
||||
remote.value = [...local.value]
|
||||
}
|
||||
|
||||
const pull = () => {
|
||||
local.value = [...remote.value]
|
||||
}
|
||||
|
||||
const ahead = computed(() =>
|
||||
Math.max(0, local.value.length - remote.value.length)
|
||||
)
|
||||
const behind = computed(() =>
|
||||
Math.max(0, remote.value.length - local.value.length)
|
||||
)
|
||||
|
||||
const reset = () => {
|
||||
local.value = []
|
||||
remote.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.remote-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem 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: 6px;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
.header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.badge {
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
.hint {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.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: var(--vp-c-brand);
|
||||
}
|
||||
.dot.remote {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.5);
|
||||
}
|
||||
.hash {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
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: 0.75rem;
|
||||
}
|
||||
.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: 0.75rem;
|
||||
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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,561 +0,0 @@
|
||||
<template>
|
||||
<div class="git-scenarios-demo">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div class="h">
|
||||
常见场景:直接照抄的 Git 命令
|
||||
</div>
|
||||
<div class="sub">
|
||||
选一个场景,按步骤执行;每一步都解释“为什么这么做”。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
class="btn"
|
||||
:disabled="activeStepIndex === 0"
|
||||
@click="prevStep"
|
||||
>
|
||||
上一步
|
||||
</button>
|
||||
<button
|
||||
class="btn primary"
|
||||
:disabled="activeStepIndex >= activeScenario.steps.length - 1"
|
||||
@click="nextStep"
|
||||
>
|
||||
下一步
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click="resetSteps"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="s in scenarios"
|
||||
:key="s.id"
|
||||
class="tab"
|
||||
:class="{ active: activeScenarioId === s.id }"
|
||||
@click="selectScenario(s.id)"
|
||||
>
|
||||
{{ s.title }}
|
||||
<span class="tag">{{ s.level }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="scenario-meta">
|
||||
<div class="scenario-desc">
|
||||
{{ activeScenario.desc }}
|
||||
</div>
|
||||
<div
|
||||
v-if="activeScenario.note"
|
||||
class="scenario-note"
|
||||
>
|
||||
{{ activeScenario.note }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-card">
|
||||
<div class="step-top">
|
||||
<div class="step-title">
|
||||
Step {{ activeStepIndex + 1 }} / {{ activeScenario.steps.length }}
|
||||
<span class="step-name">{{ activeStep.title }}</span>
|
||||
</div>
|
||||
<button
|
||||
class="copy-btn"
|
||||
@click="copy(activeStep.cmd)"
|
||||
>
|
||||
{{ copied ? '已复制' : '复制命令' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cmd">
|
||||
<code>{{ activeStep.cmd }}</code>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeStep.output"
|
||||
class="output"
|
||||
>
|
||||
<div class="label">
|
||||
你通常会看到:
|
||||
</div>
|
||||
<pre><code>{{ activeStep.output }}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="why">
|
||||
<div class="label">
|
||||
为什么:
|
||||
</div>
|
||||
<div class="text">
|
||||
{{ activeStep.why }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeStep.warn"
|
||||
class="warn"
|
||||
>
|
||||
<div class="label">
|
||||
注意:
|
||||
</div>
|
||||
<div class="text">
|
||||
{{ activeStep.warn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tips">
|
||||
<div class="tips-title">
|
||||
最容易踩坑的 3 件事
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>先看状态再动手:</strong>每次操作前先跑一次
|
||||
<code>git status</code>。
|
||||
</li>
|
||||
<li>
|
||||
<strong>只提交“你想提交的东西”:</strong>用
|
||||
<code>git add path</code> 精准暂存,别习惯性
|
||||
<code>git add .</code>。
|
||||
</li>
|
||||
<li>
|
||||
<strong>撤销要分层:</strong>没进暂存 / 进了暂存 / 已经
|
||||
commit,命令完全不同。
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
id: 'daily',
|
||||
title: '日常提交',
|
||||
level: '必会',
|
||||
desc: '在本地改代码并提交;这是你 90% 的 Git 使用场景。',
|
||||
steps: [
|
||||
{
|
||||
title: '看当前状态',
|
||||
cmd: 'git status',
|
||||
output:
|
||||
'On branch main\nChanges not staged for commit:\n modified: src/app.js',
|
||||
why: '先确认“你在哪个分支 + 改了哪些文件”,避免在错误分支提交。'
|
||||
},
|
||||
{
|
||||
title: '暂存你要提交的文件',
|
||||
cmd: 'git add src/app.js',
|
||||
output:
|
||||
'On branch main\nChanges to be committed:\n modified: src/app.js',
|
||||
why: '把“这次提交要包含的改动”放进暂存区,确保提交内容可控。'
|
||||
},
|
||||
{
|
||||
title: '提交并写清楚信息',
|
||||
cmd: 'git commit -m \"fix: handle empty input\"',
|
||||
output:
|
||||
'[main 1a2b3c4] fix: handle empty input\n 1 file changed, 3 insertions(+)',
|
||||
why: 'commit message 要能让未来的你/同事一眼看懂“改了什么 + 为什么”。'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'new-project',
|
||||
title: '新项目推远程',
|
||||
level: '常用',
|
||||
desc: '把本地新项目推到 GitHub/GitLab(remote 一般叫 origin)。',
|
||||
note: '前提:你已经在远端创建了空仓库(不要勾选 README/License,以免产生冲突)。',
|
||||
steps: [
|
||||
{
|
||||
title: '初始化仓库',
|
||||
cmd: 'git init',
|
||||
output: 'Initialized empty Git repository in .../.git/',
|
||||
why: '让当前目录变成一个 Git 仓库。'
|
||||
},
|
||||
{
|
||||
title: '第一次提交',
|
||||
cmd: 'git add . && git commit -m \"chore: initial commit\"',
|
||||
output: '[main ...] chore: initial commit',
|
||||
why: '没有提交就无法 push;先把“初始状态”存档。'
|
||||
},
|
||||
{
|
||||
title: '绑定远程地址',
|
||||
cmd: 'git remote add origin <REMOTE_URL>',
|
||||
output: '',
|
||||
why: '告诉 Git 你的云端仓库在哪里(origin 只是一个名字)。'
|
||||
},
|
||||
{
|
||||
title: '推送并建立追踪关系',
|
||||
cmd: 'git push -u origin main',
|
||||
output: 'Branch \"main\" set up to track \"origin/main\".',
|
||||
why: '加 -u 后,以后可以直接用 git push / git pull。'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'branch-pr',
|
||||
title: '开分支做功能',
|
||||
level: '必会',
|
||||
desc: '在 feature 分支开发,推送后提 PR;这是团队协作的基本功。',
|
||||
steps: [
|
||||
{
|
||||
title: '更新主分支',
|
||||
cmd: 'git switch main && git pull',
|
||||
output: '',
|
||||
why: '在开新分支前先把 main 更新到最新,减少未来合并冲突。'
|
||||
},
|
||||
{
|
||||
title: '创建并切到 feature 分支',
|
||||
cmd: 'git switch -c feat/login-form',
|
||||
output: "Switched to a new branch 'feat/login-form'",
|
||||
why: '把改动隔离在分支里,主分支保持可随时发布。'
|
||||
},
|
||||
{
|
||||
title: '提交并推送分支',
|
||||
cmd: 'git push -u origin feat/login-form',
|
||||
output: '',
|
||||
why: '推到远端后,才能在 GitHub/GitLab 上发起 PR/MR。'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'undo',
|
||||
title: '撤销/回滚',
|
||||
level: '救命',
|
||||
desc: '写错了别慌:先判断“改动在哪一层”。',
|
||||
steps: [
|
||||
{
|
||||
title: '未 add:丢掉工作区改动',
|
||||
cmd: 'git restore <file>',
|
||||
output: '',
|
||||
why: '只撤销工作区的修改,不影响暂存区和提交历史。',
|
||||
warn: '会丢弃未提交的改动;不确定时先备份或用 stash。'
|
||||
},
|
||||
{
|
||||
title: '已 add:撤回暂存',
|
||||
cmd: 'git restore --staged <file>',
|
||||
output: '',
|
||||
why: '把文件从暂存区撤回到工作区,便于重新选择提交内容。'
|
||||
},
|
||||
{
|
||||
title: '已 commit:推荐用 revert',
|
||||
cmd: 'git revert <commit>',
|
||||
output: '',
|
||||
why: 'revert 会生成一个“反向提交”,对协作更安全(不会改写历史)。',
|
||||
warn: '不要在共享分支随意 reset --hard(会让别人同步困难)。'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'conflict',
|
||||
title: '解决冲突',
|
||||
level: '常见',
|
||||
desc: '多人改同一段代码时,Git 需要你手动选择。',
|
||||
steps: [
|
||||
{
|
||||
title: '合并/拉取触发冲突',
|
||||
cmd: 'git merge <branch>',
|
||||
output: 'CONFLICT (content): Merge conflict in src/app.js',
|
||||
why: 'Git 无法自动决定保留哪一边的改动。'
|
||||
},
|
||||
{
|
||||
title: '打开冲突文件并解决标记',
|
||||
cmd: 'git status',
|
||||
output:
|
||||
'Unmerged paths:\n both modified: src/app.js\n\nfix conflicts and run \"git commit\"',
|
||||
why: '用 status 定位冲突文件,然后打开文件删掉 <<<<<<</=======/>>>>>>> 标记。'
|
||||
},
|
||||
{
|
||||
title: '标记冲突已解决并提交',
|
||||
cmd: 'git add src/app.js && git commit',
|
||||
output: '',
|
||||
why: 'add 表示“我已解决冲突”;commit 记录一次合并结果。'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const activeScenarioId = ref(scenarios[0].id)
|
||||
const activeStepIndex = ref(0)
|
||||
const copied = ref(false)
|
||||
|
||||
const activeScenario = computed(
|
||||
() => scenarios.find((s) => s.id === activeScenarioId.value) || scenarios[0]
|
||||
)
|
||||
|
||||
const activeStep = computed(
|
||||
() => activeScenario.value.steps[activeStepIndex.value]
|
||||
)
|
||||
|
||||
const resetSteps = () => {
|
||||
activeStepIndex.value = 0
|
||||
}
|
||||
|
||||
const selectScenario = (id) => {
|
||||
activeScenarioId.value = id
|
||||
resetSteps()
|
||||
}
|
||||
|
||||
const nextStep = () => {
|
||||
activeStepIndex.value = Math.min(
|
||||
activeScenario.value.steps.length - 1,
|
||||
activeStepIndex.value + 1
|
||||
)
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
activeStepIndex.value = Math.max(0, activeStepIndex.value - 1)
|
||||
}
|
||||
|
||||
const copy = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 800)
|
||||
} catch {
|
||||
copied.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.git-scenarios-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.25rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.title .h {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.title .sub {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.45rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 0.75rem 0 1rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.35);
|
||||
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-rgb), 0.12);
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.scenario-meta {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.scenario-desc {
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.scenario-note {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.step-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-name {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.cmd {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.cmd code {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.output {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.output pre {
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.why,
|
||||
.warn {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.why .text {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.warn {
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(var(--vp-c-brand-rgb), 0.18);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.06);
|
||||
}
|
||||
|
||||
.warn .text {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.tips-title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tips ul {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<div class="stash-demo">
|
||||
<div class="panel">
|
||||
<div class="areas">
|
||||
<div class="area">
|
||||
<div class="header">
|
||||
💻 工作区 ({{ work.length }})
|
||||
</div>
|
||||
<div
|
||||
v-for="f in work"
|
||||
:key="f"
|
||||
class="file"
|
||||
>
|
||||
📄 {{ f }}
|
||||
</div>
|
||||
<div
|
||||
v-if="work.length === 0"
|
||||
class="empty"
|
||||
>
|
||||
空
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span class="msg">{{ s }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="stash.length === 0"
|
||||
class="empty"
|
||||
>
|
||||
空
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button
|
||||
:disabled="work.length > 0"
|
||||
class="btn"
|
||||
@click="doWork"
|
||||
>
|
||||
修改
|
||||
</button>
|
||||
<button
|
||||
:disabled="work.length === 0 || stash.length >= 3"
|
||||
class="btn"
|
||||
@click="save"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
:disabled="stash.length === 0"
|
||||
class="btn"
|
||||
@click="pop"
|
||||
>
|
||||
恢复
|
||||
</button>
|
||||
<button
|
||||
class="btn secondary"
|
||||
@click="reset"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 Stash 用途:</strong> 临时保存工作现场,切换任务</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stash-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.areas {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.area {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
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: 0.75rem;
|
||||
}
|
||||
.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: 0.75rem;
|
||||
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>
|
||||
@@ -1,161 +0,0 @@
|
||||
<template>
|
||||
<div class="storage-demo">
|
||||
<div class="panel">
|
||||
<div class="comparison">
|
||||
<div class="mode-selector">
|
||||
<button
|
||||
:class="{ active: mode === 'full' }"
|
||||
class="mode-btn"
|
||||
@click="mode = 'full'"
|
||||
>
|
||||
完整备份
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: mode === 'git' }"
|
||||
class="mode-btn"
|
||||
@click="mode = 'git'"
|
||||
>
|
||||
Git 增量
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="visualization">
|
||||
<div class="bar-container">
|
||||
<div
|
||||
class="bar full"
|
||||
:style="{ height: fullSize + '%' }"
|
||||
>
|
||||
<span class="label">完整备份: {{ fullSize }}MB</span>
|
||||
</div>
|
||||
<div
|
||||
class="bar git"
|
||||
:style="{ height: gitSize + '%' }"
|
||||
>
|
||||
<span class="label">Git 存储: {{ gitSize }}MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ savedPercent }}%</span>
|
||||
<span class="label">节省空间</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ versionCount }}</span>
|
||||
<span class="label">版本数</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 Git 增量存储:</strong> 只保存变更部分,大幅节省空间</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const mode = ref('git')
|
||||
const versionCount = ref(5)
|
||||
const fullSize = ref(500)
|
||||
const gitSize = ref(50)
|
||||
|
||||
const savedPercent = computed(() =>
|
||||
Math.round((1 - gitSize.value / fullSize.value) * 100)
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.storage-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.mode-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: height 0.5s ease;
|
||||
}
|
||||
|
||||
.bar.full {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
}
|
||||
.bar.git {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-item .value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.stat-item .label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
padding: 0.75rem;
|
||||
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);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div class="gs-root">
|
||||
<!-- Terminal -->
|
||||
<div class="gs-terminal">
|
||||
<div class="term-bar">
|
||||
<span class="dot r" /><span class="dot y" /><span class="dot g" />
|
||||
<span class="term-title">~/project (main)</span>
|
||||
</div>
|
||||
<div ref="termEl" class="term-body">
|
||||
<div v-for="(l, i) in lines" :key="i" class="t-line">
|
||||
<span v-if="l.kind === 'cmd'" class="t-ps">$ </span>
|
||||
<span :class="'t-' + l.kind">{{ l.text }}</span>
|
||||
</div>
|
||||
<div class="t-line">
|
||||
<span class="t-ps">$ </span>
|
||||
<span class="t-typing">{{ typing }}<span class="t-cur">▋</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="gs-btns">
|
||||
<button
|
||||
v-for="op in ops"
|
||||
:key="op.id"
|
||||
:disabled="running || !op.ok()"
|
||||
:class="['gs-btn', { 'gs-btn--on': active === op.id, 'gs-btn--dim': !op.ok() }]"
|
||||
@click="run(op)"
|
||||
>
|
||||
<code>{{ op.cmd }}</code>
|
||||
</button>
|
||||
<button class="gs-btn gs-btn--reset" :disabled="running" @click="reset">重置</button>
|
||||
</div>
|
||||
|
||||
<!-- Dual-repo visual -->
|
||||
<div class="gs-repos">
|
||||
<div class="repo-card" :class="{ 'repo-pulse': pulse === 'local' }">
|
||||
<div class="repo-header">
|
||||
<span class="repo-icon">💻</span>
|
||||
<span class="repo-name">本地仓库</span>
|
||||
<span class="repo-path">~/project</span>
|
||||
</div>
|
||||
<div class="commit-col">
|
||||
<div v-if="!localLog.length" class="no-commits">(空)</div>
|
||||
<div
|
||||
v-for="(c, i) in localLog"
|
||||
:key="i"
|
||||
class="cmt-row"
|
||||
:class="{ 'cmt-new': c.isNew }"
|
||||
>
|
||||
<span class="cmt-dot local-dot" />
|
||||
<code class="cmt-hash">{{ c.hash }}</code>
|
||||
<span class="cmt-msg">{{ c.msg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-footer">
|
||||
<span v-if="localAhead > 0" class="badge-ahead">↑ {{ localAhead }} 个未推送</span>
|
||||
<span v-else-if="localLog.length" class="badge-sync">✓ 已同步</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow column -->
|
||||
<div class="arrow-col">
|
||||
<div class="arrow-row" :class="{ 'arrow-lit': pulse === 'push' }">
|
||||
<span class="arrow-label">push →</span>
|
||||
</div>
|
||||
<div class="arrow-row arrow-pull" :class="{ 'arrow-lit': pulse === 'pull' }">
|
||||
<span class="arrow-label">← pull</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repo-card repo-remote" :class="{ 'repo-pulse-remote': pulse === 'remote' }">
|
||||
<div class="repo-header">
|
||||
<span class="repo-icon">☁️</span>
|
||||
<span class="repo-name">远程仓库</span>
|
||||
<span class="repo-path">github.com/you/project</span>
|
||||
</div>
|
||||
<div class="commit-col">
|
||||
<div v-if="!remoteLog.length" class="no-commits">(空)</div>
|
||||
<div
|
||||
v-for="(c, i) in remoteLog"
|
||||
:key="i"
|
||||
class="cmt-row"
|
||||
:class="{ 'cmt-new': c.isNew }"
|
||||
>
|
||||
<span class="cmt-dot remote-dot" />
|
||||
<code class="cmt-hash">{{ c.hash }}</code>
|
||||
<span class="cmt-msg">{{ c.msg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="repo-footer">
|
||||
<span v-if="remoteLog.length" class="badge-online">🌐 在线</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hint" class="gs-hint">💡 {{ hint }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
|
||||
const termEl = ref(null)
|
||||
const lines = ref([{ kind: 'dim', text: '# 本地 2 次提交,还没关联远程仓库' }])
|
||||
const typing = ref('')
|
||||
const running = ref(false)
|
||||
const active = ref(null)
|
||||
const hint = ref('点击下方命令按钮,按顺序执行')
|
||||
const pulse = ref('')
|
||||
|
||||
const localLog = ref([
|
||||
{ hash: '9f3e1b2', msg: 'init: 初始化项目', isNew: false },
|
||||
{ hash: 'c4d8a31', msg: 'feat: 首页布局', isNew: false },
|
||||
])
|
||||
const remoteLog = ref([])
|
||||
const localAhead = ref(2)
|
||||
let s = { linked: false, pushed: false, committed: false, pushed2: false }
|
||||
|
||||
const ops = [
|
||||
{
|
||||
id: 'remote',
|
||||
cmd: 'git remote add origin https://github.com/you/project.git',
|
||||
ok: () => !s.linked,
|
||||
output: [
|
||||
{ kind: 'dim', text: '# 建立本地与远程的关联(只做一次)' },
|
||||
{ kind: 'grn', text: 'origin https://github.com/you/project.git (fetch)' },
|
||||
{ kind: 'grn', text: 'origin https://github.com/you/project.git (push)' },
|
||||
],
|
||||
hint: '"origin" 是远程仓库的别名,相当于给 GitHub 地址起个简短的联系人名字。',
|
||||
do: () => { s.linked = true },
|
||||
p: '',
|
||||
},
|
||||
{
|
||||
id: 'push1',
|
||||
cmd: 'git push -u origin main',
|
||||
ok: () => s.linked && !s.pushed,
|
||||
output: [
|
||||
{ kind: 'dim', text: 'Enumerating objects: 5, done.' },
|
||||
{ kind: 'grn', text: 'To https://github.com/you/project.git' },
|
||||
{ kind: 'grn', text: ' * [new branch] main -> main' },
|
||||
],
|
||||
hint: '第一次 push 加 -u,以后直接 git push 就行。本地提交现在上传到 GitHub 了。',
|
||||
do: () => {
|
||||
s.pushed = true; localAhead.value = 0
|
||||
remoteLog.value = localLog.value.map(c => ({ ...c, isNew: true }))
|
||||
setTimeout(() => remoteLog.value.forEach(c => c.isNew = false), 900)
|
||||
},
|
||||
p: 'push',
|
||||
},
|
||||
{
|
||||
id: 'commit',
|
||||
cmd: 'git commit -m "fix: 修复登录 Bug"',
|
||||
ok: () => s.pushed && !s.committed,
|
||||
output: [
|
||||
{ kind: 'dim', text: '[main b5e6f7a] fix: 修复登录 Bug' },
|
||||
{ kind: 'yel', text: "Your branch is 1 commit ahead of 'origin/main'." },
|
||||
],
|
||||
hint: '本地新增一个 commit,但还没 push。远程还是旧的,本地比它"快了一步"。',
|
||||
do: () => {
|
||||
s.committed = true; localAhead.value = 1
|
||||
localLog.value.unshift({ hash: 'b5e6f7a', msg: 'fix: 修复登录 Bug', isNew: true })
|
||||
setTimeout(() => localLog.value.forEach(c => c.isNew = false), 900)
|
||||
},
|
||||
p: 'local',
|
||||
},
|
||||
{
|
||||
id: 'push2',
|
||||
cmd: 'git push',
|
||||
ok: () => s.committed && !s.pushed2,
|
||||
output: [
|
||||
{ kind: 'grn', text: 'To https://github.com/you/project.git' },
|
||||
{ kind: 'grn', text: ' c4d8a31..b5e6f7a main -> main' },
|
||||
],
|
||||
hint: '第二次 push 不需要 -u,直接推。远程和本地又同步了。',
|
||||
do: () => {
|
||||
s.pushed2 = true; localAhead.value = 0
|
||||
remoteLog.value = localLog.value.map(c => ({ ...c, isNew: true }))
|
||||
setTimeout(() => remoteLog.value.forEach(c => c.isNew = false), 900)
|
||||
},
|
||||
p: 'push',
|
||||
},
|
||||
{
|
||||
id: 'pull',
|
||||
cmd: 'git pull',
|
||||
ok: () => s.pushed,
|
||||
output: [
|
||||
{ kind: 'grn', text: 'From https://github.com/you/project.git' },
|
||||
{ kind: 'grn', text: ' b5e6f7a..d8c9e0f main -> origin/main' },
|
||||
{ kind: 'dim', text: 'Fast-forward: readme.md | 5 +++++ 1 file changed' },
|
||||
],
|
||||
hint: 'pull = fetch + merge。队友推上去的提交,现在也同步到你本地了。',
|
||||
do: () => {
|
||||
const c = { hash: 'd8c9e0f', msg: '队友: 更新 README', isNew: true }
|
||||
remoteLog.value.unshift({ ...c })
|
||||
localLog.value.unshift({ ...c })
|
||||
setTimeout(() => {
|
||||
remoteLog.value.forEach(x => x.isNew = false)
|
||||
localLog.value.forEach(x => x.isNew = false)
|
||||
}, 900)
|
||||
},
|
||||
p: 'pull',
|
||||
},
|
||||
]
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
function scroll() { if (termEl.value) termEl.value.scrollTop = termEl.value.scrollHeight }
|
||||
|
||||
async function run(op) {
|
||||
if (running.value) return
|
||||
running.value = true; active.value = op.id; hint.value = ''; typing.value = ''; pulse.value = ''
|
||||
for (const ch of op.cmd) { typing.value += ch; await sleep(20) }
|
||||
await sleep(80)
|
||||
lines.value.push({ kind: 'cmd', text: op.cmd }); typing.value = ''
|
||||
await nextTick(); scroll(); await sleep(150)
|
||||
for (const l of op.output) { lines.value.push(l); await nextTick(); scroll(); await sleep(50) }
|
||||
op.do()
|
||||
pulse.value = op.p
|
||||
await sleep(100); hint.value = op.hint
|
||||
setTimeout(() => { if (pulse.value === op.p) pulse.value = '' }, 1200)
|
||||
running.value = false
|
||||
}
|
||||
|
||||
function reset() {
|
||||
lines.value = [{ kind: 'dim', text: '# 本地 2 次提交,还没关联远程仓库' }]
|
||||
localLog.value = [
|
||||
{ hash: '9f3e1b2', msg: 'init: 初始化项目', isNew: false },
|
||||
{ hash: 'c4d8a31', msg: 'feat: 首页布局', isNew: false },
|
||||
]
|
||||
remoteLog.value = []; localAhead.value = 2
|
||||
s = { linked: false, pushed: false, committed: false, pushed2: false }
|
||||
active.value = null; hint.value = '点击下方命令按钮,按顺序执行'
|
||||
typing.value = ''; running.value = false; pulse.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gs-root {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px; overflow: hidden;
|
||||
background: var(--vp-c-bg-soft); margin: 1rem 0; font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Terminal */
|
||||
.gs-terminal { background: #141420; }
|
||||
.term-bar {
|
||||
display: flex; align-items: center; gap: 5px;
|
||||
padding: 7px 12px; background: #1e1e2e;
|
||||
}
|
||||
.dot { width: 11px; height: 11px; border-radius: 50%; }
|
||||
.dot.r { background: #ff5f57; } .dot.y { background: #febc2e; } .dot.g { background: #28c840; }
|
||||
.term-title { margin-left: 8px; font-size: 0.72rem; color: #666; font-family: monospace; }
|
||||
.term-body {
|
||||
min-height: 120px; max-height: 180px; overflow-y: auto;
|
||||
padding: 0.7rem 1rem;
|
||||
font-family: 'Menlo','Monaco',monospace; font-size: 0.76rem; line-height: 1.6; color: #cdd6f4;
|
||||
}
|
||||
.t-line { display: flex; }
|
||||
.t-ps { color: #a6e3a1; flex-shrink: 0; }
|
||||
.t-cmd { color: #cdd6f4; } .t-dim { color: #585b70; } .t-grn { color: #a6e3a1; } .t-yel { color: #89b4fa; }
|
||||
.t-typing { color: #cdd6f4; }
|
||||
.t-cur { animation: blink 1s step-end infinite; }
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
|
||||
|
||||
/* Buttons */
|
||||
.gs-btns {
|
||||
display: flex; flex-wrap: wrap; gap: 6px;
|
||||
padding: 8px 10px; background: #0d0d1a; border-top: 1px solid #2a2a3e;
|
||||
}
|
||||
.gs-btn {
|
||||
background: #1e1e2e; border: 1px solid #313244;
|
||||
border-radius: 5px; padding: 4px 9px; cursor: pointer; transition: border-color .2s;
|
||||
}
|
||||
.gs-btn code { font-size: 0.7rem; color: #7f849c; font-family: monospace; white-space: nowrap; }
|
||||
.gs-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); }
|
||||
.gs-btn--on { border-color: var(--vp-c-brand) !important; }
|
||||
.gs-btn--on code { color: var(--vp-c-brand); }
|
||||
.gs-btn--dim { opacity: 0.3; cursor: not-allowed; }
|
||||
.gs-btn--reset { background: transparent; border-color: #313244; margin-left: auto; }
|
||||
.gs-btn--reset::after { content: '重置'; font-size: 0.7rem; color: #585b70; }
|
||||
|
||||
/* Repos */
|
||||
.gs-repos {
|
||||
display: grid; grid-template-columns: 1fr auto 1fr;
|
||||
gap: 8px; padding: 10px 12px;
|
||||
background: var(--vp-c-bg); border-top: 1px solid var(--vp-c-divider);
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.gs-repos { grid-template-columns: 1fr; }
|
||||
.arrow-col { flex-direction: row; justify-content: center; gap: 16px; }
|
||||
}
|
||||
|
||||
.repo-card {
|
||||
border: 1.5px solid var(--vp-c-divider); border-radius: 8px;
|
||||
padding: 8px 10px; background: var(--vp-c-bg-soft);
|
||||
transition: border-color .3s, box-shadow .3s;
|
||||
}
|
||||
.repo-remote { border-color: #60a5fa44; background: color-mix(in srgb, #60a5fa 4%, var(--vp-c-bg-soft)); }
|
||||
.repo-pulse { border-color: var(--vp-c-brand) !important; box-shadow: 0 0 0 3px color-mix(in srgb, var(--vp-c-brand) 12%, transparent); }
|
||||
.repo-pulse-remote { border-color: #60a5fa !important; box-shadow: 0 0 0 3px #60a5fa22; }
|
||||
|
||||
.repo-header {
|
||||
display: flex; align-items: center; gap: 5px; margin-bottom: 6px; flex-wrap: wrap;
|
||||
}
|
||||
.repo-icon { font-size: 1rem; }
|
||||
.repo-name { font-weight: 700; font-size: 0.8rem; }
|
||||
.repo-path { font-family: monospace; font-size: 0.62rem; color: var(--vp-c-text-3); margin-left: auto; }
|
||||
|
||||
.commit-col { min-height: 48px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.no-commits { color: var(--vp-c-text-3); font-size: 0.72rem; }
|
||||
.cmt-row {
|
||||
display: flex; align-items: center; gap: 5px; font-size: 0.72rem;
|
||||
padding: 2px 4px; border-radius: 3px; transition: background .3s;
|
||||
}
|
||||
.cmt-new { background: color-mix(in srgb, var(--vp-c-brand) 10%, transparent); }
|
||||
.cmt-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||
.local-dot { background: var(--vp-c-brand); }
|
||||
.remote-dot { background: #60a5fa; }
|
||||
.cmt-hash { color: var(--vp-c-brand); font-size: 0.68rem; }
|
||||
.cmt-msg { color: var(--vp-c-text-2); }
|
||||
|
||||
.repo-footer { margin-top: 5px; font-size: 0.7rem; min-height: 16px; }
|
||||
.badge-ahead { color: var(--vp-c-brand); font-weight: 600; }
|
||||
.badge-sync { color: #a6e3a1; }
|
||||
.badge-online { color: #60a5fa; }
|
||||
|
||||
/* Arrows */
|
||||
.arrow-col {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
gap: 12px; padding-top: 32px;
|
||||
}
|
||||
.arrow-row {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
opacity: 0.25; transition: opacity .3s;
|
||||
}
|
||||
.arrow-row.arrow-lit { opacity: 1; }
|
||||
.arrow-label {
|
||||
font-size: 0.66rem; font-family: monospace;
|
||||
color: var(--vp-c-brand); white-space: nowrap;
|
||||
}
|
||||
.arrow-pull .arrow-label { color: #60a5fa; }
|
||||
|
||||
.gs-hint {
|
||||
padding: 8px 12px; background: var(--vp-c-bg-alt);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.8rem; color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,743 +0,0 @@
|
||||
<template>
|
||||
<div class="three-areas-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">📂</span>
|
||||
<span class="title">Git 三区概念</span>
|
||||
<span class="subtitle">工作区 → 暂存区 → 仓库</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<div class="scene">
|
||||
<!-- 1. Working Directory (Desk) -->
|
||||
<div class="zone working">
|
||||
<div class="zone-header">
|
||||
<span class="zone-icon">💻</span>
|
||||
<div class="zone-info">
|
||||
<span class="zone-title">工作区 (Desk)</span>
|
||||
<span class="zone-desc">你的书桌,随便乱放</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desk-surface">
|
||||
<transition-group name="file-pop">
|
||||
<div
|
||||
v-for="file in workingFiles"
|
||||
:key="file.id"
|
||||
class="file-card"
|
||||
@click="addToStaging(file)"
|
||||
>
|
||||
<div class="file-icon">
|
||||
{{ file.icon }}
|
||||
</div>
|
||||
<div class="file-name">
|
||||
{{ file.name }}
|
||||
</div>
|
||||
<div class="action-hint">
|
||||
Add +
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div
|
||||
v-if="workingFiles.length === 0"
|
||||
class="empty-state"
|
||||
>
|
||||
桌上很干净 ✨
|
||||
<button
|
||||
class="create-btn"
|
||||
@click="createNewFile"
|
||||
>
|
||||
新建文件 📝
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="flow-arrow">
|
||||
<div class="arrow-line" />
|
||||
<div class="arrow-label">
|
||||
git add
|
||||
</div>
|
||||
<div class="arrow-head">
|
||||
▶
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Staging Area (Box) -->
|
||||
<div class="zone staging">
|
||||
<div class="zone-header">
|
||||
<span class="zone-icon">📦</span>
|
||||
<div class="zone-info">
|
||||
<span class="zone-title">暂存区 (Box)</span>
|
||||
<span class="zone-desc">快递盒,准备打包</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-container">
|
||||
<div class="box-body">
|
||||
<transition-group name="file-drop">
|
||||
<div
|
||||
v-for="file in stagedFiles"
|
||||
:key="file.id"
|
||||
class="file-card mini"
|
||||
@click="unstageFile(file)"
|
||||
>
|
||||
<div class="file-icon">
|
||||
{{ file.icon }}
|
||||
</div>
|
||||
<div class="file-name">
|
||||
{{ file.name }}
|
||||
</div>
|
||||
<div class="action-hint">
|
||||
Remove -
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div
|
||||
v-if="stagedFiles.length === 0"
|
||||
class="empty-state box-empty"
|
||||
>
|
||||
盒子是空的 🕸️
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-flap left" />
|
||||
<div class="box-flap right" />
|
||||
</div>
|
||||
<div class="staging-actions">
|
||||
<button
|
||||
class="commit-btn"
|
||||
:disabled="stagedFiles.length === 0"
|
||||
@click="commitFiles"
|
||||
>
|
||||
封箱寄出 (git commit) 🚚
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="flow-arrow">
|
||||
<div class="arrow-line" />
|
||||
<div class="arrow-label">
|
||||
git commit
|
||||
</div>
|
||||
<div class="arrow-head">
|
||||
▶
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Repository (Cabinet) -->
|
||||
<div class="zone repo">
|
||||
<div class="zone-header">
|
||||
<span class="zone-icon">🗄️</span>
|
||||
<div class="zone-info">
|
||||
<span class="zone-title">仓库 (Cabinet)</span>
|
||||
<span class="zone-desc">档案柜,永久保存</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cabinet-body">
|
||||
<transition-group name="drawer-slide">
|
||||
<div
|
||||
v-for="commit in commits.slice().reverse()"
|
||||
:key="commit.hash"
|
||||
class="drawer-item"
|
||||
>
|
||||
<div class="drawer-handle" />
|
||||
<div class="commit-info">
|
||||
<span class="commit-hash">#{{ commit.hash }}</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div
|
||||
v-if="commits.length === 0"
|
||||
class="empty-state"
|
||||
>
|
||||
柜子是空的 💨
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="block">
|
||||
<div class="block-title">
|
||||
当前等价命令
|
||||
</div>
|
||||
<pre class="mono"><code>{{ historyText }}</code></pre>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div class="block-title">
|
||||
git status(模拟)
|
||||
</div>
|
||||
<pre class="mono"><code>{{ statusText }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>Git 的三区就像餐厅——工作区是餐桌(随便放),暂存区是备菜盘(准备上菜),仓库是菜单(永久记录)。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const fileIdCounter = ref(1)
|
||||
|
||||
const createId = () => `file-${fileIdCounter.value++}`
|
||||
|
||||
const workingFiles = ref([
|
||||
{ id: createId(), name: 'essay.txt', icon: '📝' },
|
||||
{ id: createId(), name: 'photo.jpg', icon: '🖼️' },
|
||||
{ id: createId(), name: 'style.css', icon: '🎨' }
|
||||
])
|
||||
|
||||
const stagedFiles = ref([])
|
||||
const commits = ref([])
|
||||
const history = ref(['$ git status'])
|
||||
|
||||
const pushHistory = (line) => {
|
||||
history.value.push(line)
|
||||
if (history.value.length > 6)
|
||||
history.value.splice(0, history.value.length - 6)
|
||||
}
|
||||
|
||||
const historyText = computed(() => history.value.join('\n'))
|
||||
|
||||
const statusText = computed(() => {
|
||||
const lines = ['On branch main']
|
||||
if (stagedFiles.value.length === 0 && workingFiles.value.length === 0) {
|
||||
lines.push('nothing to commit, working tree clean')
|
||||
return lines.join('\n')
|
||||
}
|
||||
if (stagedFiles.value.length) {
|
||||
lines.push('Changes to be committed:')
|
||||
stagedFiles.value.forEach((f) => lines.push(` new file: ${f.name}`))
|
||||
}
|
||||
if (workingFiles.value.length) {
|
||||
lines.push('Untracked files:')
|
||||
workingFiles.value.forEach((f) => lines.push(` ${f.name}`))
|
||||
}
|
||||
return lines.join('\n')
|
||||
})
|
||||
|
||||
const createNewFile = () => {
|
||||
const types = [
|
||||
{ name: 'script.js', icon: '📜' },
|
||||
{ name: 'data.json', icon: '📊' },
|
||||
{ name: 'readme.md', icon: '📘' }
|
||||
]
|
||||
const randomType = types[Math.floor(Math.random() * types.length)]
|
||||
workingFiles.value.push({
|
||||
id: createId(),
|
||||
name: randomType.name,
|
||||
icon: randomType.icon
|
||||
})
|
||||
pushHistory(`$ touch ${randomType.name}`)
|
||||
}
|
||||
|
||||
const addToStaging = (file) => {
|
||||
const index = workingFiles.value.findIndex((f) => f.id === file.id)
|
||||
if (index !== -1) {
|
||||
workingFiles.value.splice(index, 1)
|
||||
stagedFiles.value.push(file)
|
||||
pushHistory(`$ git add ${file.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
const unstageFile = (file) => {
|
||||
const index = stagedFiles.value.findIndex((f) => f.id === file.id)
|
||||
if (index !== -1) {
|
||||
stagedFiles.value.splice(index, 1)
|
||||
workingFiles.value.push(file)
|
||||
pushHistory(`$ git restore --staged ${file.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
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 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)
|
||||
})
|
||||
|
||||
pushHistory(`$ git commit -m "${randomMsg}"`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.three-areas-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
|
||||
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.scene {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
/* Common Zone Styles */
|
||||
.zone {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.zone-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
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);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 1. Working Desk */
|
||||
.zone.working {
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
}
|
||||
.desk-surface {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
background-size: 10px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.file-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
width: 80px;
|
||||
height: 90px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.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;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.9);
|
||||
color: var(--vp-c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.file-card:hover .action-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 2. Staging Box */
|
||||
.zone.staging {
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
}
|
||||
.box-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.box-body {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
|
||||
}
|
||||
|
||||
.file-card.mini {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
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: var(--vp-c-brand);
|
||||
}
|
||||
.file-card.mini .action-hint {
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.9);
|
||||
}
|
||||
|
||||
.box-flap {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
width: 45%;
|
||||
height: 20px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
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);
|
||||
}
|
||||
|
||||
.staging-actions {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.commit-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.commit-btn:disabled {
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-2);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.commit-btn:hover:not(:disabled) {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* 3. Repo Cabinet */
|
||||
.zone.repo {
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
}
|
||||
.cabinet-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.drawer-item {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
}
|
||||
.drawer-handle {
|
||||
width: 30px;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.commit-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.commit-hash {
|
||||
font-size: 0.6rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
.commit-msg {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.commit-files {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
.tiny-file {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
/* Arrows */
|
||||
.flow-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mono {
|
||||
margin: 0;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
color: var(--vp-c-text-1);
|
||||
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-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);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,486 +0,0 @@
|
||||
<!--
|
||||
GitWorkflowDemo.vue
|
||||
Git 基础工作流演示 - 寄快递版
|
||||
|
||||
展示 Git 的基本工作流程:修改 → 暂存 → 提交
|
||||
高度控制:紧凑布局,确保在 600px 内
|
||||
-->
|
||||
<template>
|
||||
<div class="git-workflow-demo">
|
||||
<div class="demo-header">
|
||||
<span class="icon">📦</span>
|
||||
<span class="title">Git 工作流</span>
|
||||
<span class="subtitle">修改 → 暂存 → 提交,三步走</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-content">
|
||||
<!-- 文件状态区域 -->
|
||||
<div class="file-area">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📝</span>
|
||||
<span class="area-name">工作区</span>
|
||||
<span class="area-desc">你正在改的文件</span>
|
||||
</div>
|
||||
<div class="file-list">
|
||||
<div
|
||||
v-for="file in files"
|
||||
:key="file.name"
|
||||
class="file-item"
|
||||
:class="{
|
||||
'modified': file.status === 'modified',
|
||||
'staged': file.status === 'staged',
|
||||
'committed': file.status === 'committed'
|
||||
}"
|
||||
>
|
||||
<span class="file-icon">{{ getIcon(file.status) }}</span>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<span class="file-status">{{ getStatusText(file.status) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 箭头 -->
|
||||
<div
|
||||
v-if="!allCommitted"
|
||||
class="arrow-group"
|
||||
>
|
||||
<div
|
||||
class="arrow"
|
||||
:class="{ active: hasStaged }"
|
||||
>
|
||||
↓
|
||||
</div>
|
||||
<div
|
||||
v-if="hasStaged"
|
||||
class="arrow-label"
|
||||
>
|
||||
git add
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 暂存区 -->
|
||||
<div class="stage-area">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">📋</span>
|
||||
<span class="area-name">暂存区</span>
|
||||
<span class="area-desc">准备打包的文件</span>
|
||||
</div>
|
||||
<div class="file-list">
|
||||
<div
|
||||
v-for="file in stagedFiles"
|
||||
:key="file.name"
|
||||
class="file-item staged"
|
||||
>
|
||||
<span class="file-icon">📌</span>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<span class="file-status">待提交</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="stagedFiles.length === 0"
|
||||
class="empty-tip"
|
||||
>
|
||||
暂无文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 箭头 -->
|
||||
<div
|
||||
v-if="hasStaged"
|
||||
class="arrow-group"
|
||||
>
|
||||
<div class="arrow active">
|
||||
↓
|
||||
</div>
|
||||
<div class="arrow-label">
|
||||
git commit
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 仓库区 -->
|
||||
<div class="repo-area">
|
||||
<div class="area-header">
|
||||
<span class="area-icon">🏪</span>
|
||||
<span class="area-name">仓库</span>
|
||||
<span class="area-desc">已保存的版本</span>
|
||||
</div>
|
||||
<div class="commit-list">
|
||||
<div
|
||||
v-for="(commit, i) in commits"
|
||||
:key="i"
|
||||
class="commit-item"
|
||||
>
|
||||
<span class="commit-icon">✅</span>
|
||||
<span class="commit-msg">{{ commit.msg }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="commits.length === 0"
|
||||
class="empty-tip"
|
||||
>
|
||||
暂无提交
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-panel">
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="allModified"
|
||||
@click="modifyFile"
|
||||
>
|
||||
✏️ 修改文件
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="!hasModified || allStaged"
|
||||
@click="stageFiles"
|
||||
>
|
||||
📌 暂存修改
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="!hasStaged"
|
||||
@click="commitFiles"
|
||||
>
|
||||
✅ 提交版本
|
||||
</button>
|
||||
<button
|
||||
class="action-btn secondary"
|
||||
@click="reset"
|
||||
>
|
||||
🔄 重置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<span class="icon">💡</span>
|
||||
<strong>核心思想:</strong>工作区修改 → 暂存区挑选 → 仓库永久保存
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const files = ref([
|
||||
{ name: 'index.html', status: 'unmodified' },
|
||||
{ name: 'app.js', status: 'unmodified' },
|
||||
{ name: 'style.css', status: 'unmodified' }
|
||||
])
|
||||
|
||||
const commits = ref([])
|
||||
|
||||
const hasModified = computed(() =>
|
||||
files.value.some(f => f.status === 'modified')
|
||||
)
|
||||
|
||||
const hasStaged = computed(() =>
|
||||
files.value.some(f => f.status === 'staged')
|
||||
)
|
||||
|
||||
const allCommitted = computed(() =>
|
||||
files.value.every(f => f.status === 'committed')
|
||||
)
|
||||
|
||||
const allModified = computed(() =>
|
||||
files.value.every(f => f.status === 'modified')
|
||||
)
|
||||
|
||||
const allStaged = computed(() =>
|
||||
files.value.every(f => f.status === 'staged' || f.status === 'committed')
|
||||
)
|
||||
|
||||
const stagedFiles = computed(() =>
|
||||
files.value.filter(f => f.status === 'staged')
|
||||
)
|
||||
|
||||
const getIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'modified': return '📝'
|
||||
case 'staged': return '📌'
|
||||
case 'committed': return '✅'
|
||||
default: return '📄'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'modified': return '已修改'
|
||||
case 'staged': return '已暂存'
|
||||
case 'committed': return '已提交'
|
||||
default: return '未修改'
|
||||
}
|
||||
}
|
||||
|
||||
const modifyFile = () => {
|
||||
const unmodified = files.value.filter(f => f.status === 'unmodified' || f.status === 'committed')
|
||||
if (unmodified.length > 0) {
|
||||
const file = unmodified[0]
|
||||
file.status = 'modified'
|
||||
}
|
||||
}
|
||||
|
||||
const stageFiles = () => {
|
||||
files.value.forEach(f => {
|
||||
if (f.status === 'modified') {
|
||||
f.status = 'staged'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const commitFiles = () => {
|
||||
const staged = files.value.filter(f => f.status === 'staged')
|
||||
if (staged.length > 0) {
|
||||
files.value.forEach(f => {
|
||||
if (f.status === 'staged') {
|
||||
f.status = 'committed'
|
||||
}
|
||||
})
|
||||
commits.value.push({
|
||||
msg: `提交了 ${staged.length} 个文件`,
|
||||
files: staged.map(f => f.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
files.value.forEach(f => {
|
||||
f.status = 'unmodified'
|
||||
})
|
||||
commits.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.git-workflow-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
max-height: 550px;
|
||||
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.demo-header .icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.demo-header .title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-header .subtitle {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.area-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.area-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.area-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.area-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.file-area,
|
||||
.stage-area,
|
||||
.repo-area {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.file-list,
|
||||
.commit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.file-item,
|
||||
.commit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.file-item.modified {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-left: 3px solid var(--vp-c-warning);
|
||||
}
|
||||
|
||||
.file-item.staged {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.file-item.committed {
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-left: 3px solid var(--vp-c-success);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.file-status {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.commit-item {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.commit-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.commit-msg {
|
||||
flex: 1;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.arrow-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--vp-c-text-3);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.arrow.active {
|
||||
color: var(--vp-c-brand);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.arrow-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.action-panel {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.action-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover:not(:disabled) {
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-box strong {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user