feat: add interactive demos for AI history, Auth design, and Git intro
This commit is contained in:
@@ -2,12 +2,25 @@
|
||||
<div class="branch-demo">
|
||||
<div class="panel">
|
||||
<div class="controls">
|
||||
<button @click="init" :disabled="inited" class="btn">初始化</button>
|
||||
<button @click="commit" :disabled="!inited" class="btn">提交</button>
|
||||
<button @click="init" :disabled="inited || mergePending" class="btn">
|
||||
初始化
|
||||
</button>
|
||||
<button @click="commit" :disabled="!inited || mergePending" class="btn">
|
||||
提交
|
||||
</button>
|
||||
<button @click="branch" :disabled="!inited || hasBranch" class="btn">
|
||||
创建分支
|
||||
</button>
|
||||
<button @click="merge" :disabled="!hasBranch" class="btn">合并</button>
|
||||
<button
|
||||
@click="prepareMerge"
|
||||
:disabled="!hasBranch || mergePending"
|
||||
class="btn"
|
||||
>
|
||||
准备合并
|
||||
</button>
|
||||
<button @click="finishMerge" :disabled="!mergePending" class="btn">
|
||||
完成合并
|
||||
</button>
|
||||
<button @click="reset" class="btn secondary">重置</button>
|
||||
</div>
|
||||
|
||||
@@ -72,6 +85,7 @@
|
||||
import { ref } from 'vue'
|
||||
const inited = ref(false)
|
||||
const hasBranch = ref(false)
|
||||
const mergePending = ref(false)
|
||||
const main = ref([])
|
||||
const feat = ref([])
|
||||
|
||||
@@ -88,16 +102,21 @@ const branch = () => {
|
||||
feat.value = [1]
|
||||
}
|
||||
}
|
||||
const merge = () => {
|
||||
if (hasBranch.value) {
|
||||
main.value.push(1)
|
||||
hasBranch.value = false
|
||||
feat.value = []
|
||||
}
|
||||
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 = []
|
||||
}
|
||||
|
||||
@@ -8,33 +8,56 @@
|
||||
<span v-html="line.text"></span>
|
||||
</div>
|
||||
<div v-if="output.length === 0" class="welcome">
|
||||
输入命令开始学习 Git
|
||||
输入命令开始学习 Git(建议先点“制造改动”,再跑 git status)
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-line">
|
||||
<span class="prompt">$</span>
|
||||
<input
|
||||
v-model="cmd"
|
||||
@keyup.enter="execute"
|
||||
placeholder="git status"
|
||||
@keyup.enter="execute({ fromQuick: false })"
|
||||
placeholder="(默认安全模式)请用下方按钮执行命令"
|
||||
class="cmd-input"
|
||||
:disabled="!freeMode"
|
||||
/>
|
||||
<button @click="execute" class="run-btn">运行</button>
|
||||
<button
|
||||
@click="execute({ fromQuick: false })"
|
||||
class="run-btn"
|
||||
:disabled="!freeMode"
|
||||
>
|
||||
运行
|
||||
</button>
|
||||
<button @click="clearOutput" class="run-btn secondary">清空</button>
|
||||
<button @click="toggleFreeMode" class="run-btn secondary">
|
||||
{{ freeMode ? '切回安全模式' : '开启自由模式' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-cmds">
|
||||
<button @click="runCmd('git init')" class="cmd-btn">初始化</button>
|
||||
<button @click="runCmd('git status')" class="cmd-btn">状态</button>
|
||||
<button @click="runCmd('git add .')" class="cmd-btn">添加</button>
|
||||
<button @click="runCmd('git commit -m \'msg\'')" class="cmd-btn">
|
||||
提交
|
||||
<button @click="makeChanges" class="cmd-btn">制造改动</button>
|
||||
<button @click="runCmd('git init')" class="cmd-btn">git init</button>
|
||||
<button @click="runCmd('git status')" class="cmd-btn">
|
||||
git status
|
||||
</button>
|
||||
<button @click="runCmd('git add .')" class="cmd-btn">git add .</button>
|
||||
<button @click="runCmd(`git commit -m 'msg'`)" class="cmd-btn">
|
||||
git commit
|
||||
</button>
|
||||
<button @click="runCmd('git log --oneline')" class="cmd-btn">
|
||||
git log
|
||||
</button>
|
||||
<button @click="runCmd('git switch -c feat/demo')" class="cmd-btn">
|
||||
新分支
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 常用命令:</strong> init → status → add → commit</p>
|
||||
<p>
|
||||
<strong>💡 建议练习顺序:</strong> 制造改动 → status → add → status →
|
||||
commit → log
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,29 +67,202 @@ 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 execute = () => {
|
||||
const c = cmd.value.trim()
|
||||
if (!c) return
|
||||
|
||||
output.value.push({ type: 'command', text: c })
|
||||
pushLine('command', c)
|
||||
|
||||
// Commands
|
||||
if (c === 'git init') {
|
||||
output.value.push({
|
||||
type: 'success',
|
||||
text: 'Initialized empty Git repository'
|
||||
})
|
||||
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') {
|
||||
output.value.push({
|
||||
type: 'info',
|
||||
text: 'On branch main\nnothing to commit'
|
||||
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)
|
||||
})
|
||||
} else if (c === 'git add .') {
|
||||
output.value.push({ type: 'success', text: 'Files added to staging area' })
|
||||
pushLine('success', `Added ${toStage.length} path(s) to staging area.`)
|
||||
} else if (c.startsWith('git commit')) {
|
||||
output.value.push({ type: 'success', text: '1 file committed' })
|
||||
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 {
|
||||
output.value.push({ type: 'error', text: 'Unknown command' })
|
||||
pushLine(
|
||||
'error',
|
||||
'Unknown command (supported: init/status/add/commit/log/branch/switch/checkout/restore)'
|
||||
)
|
||||
}
|
||||
|
||||
cmd.value = ''
|
||||
@@ -74,7 +270,22 @@ const execute = () => {
|
||||
|
||||
const runCmd = (c) => {
|
||||
cmd.value = c
|
||||
execute()
|
||||
execute({ fromQuick: true })
|
||||
}
|
||||
|
||||
const clearOutput = () => {
|
||||
output.value = []
|
||||
}
|
||||
|
||||
const toggleFreeMode = () => {
|
||||
freeMode.value = !freeMode.value
|
||||
cmd.value = ''
|
||||
pushLine(
|
||||
'info',
|
||||
freeMode.value
|
||||
? '已开启自由模式:现在可以手动输入命令(仍然只模拟,不会影响真实仓库)。'
|
||||
: '已切回安全模式:请使用下方按钮执行预设命令。'
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -88,33 +299,34 @@ const runCmd = (c) => {
|
||||
}
|
||||
|
||||
.terminal {
|
||||
background: #1f2937;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-family: monospace;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.output {
|
||||
min-height: 150px;
|
||||
margin-bottom: 1rem;
|
||||
color: #d1d5db;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.output .command {
|
||||
color: #10b981;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.output .success {
|
||||
color: #10b981;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.output .error {
|
||||
color: #ef4444;
|
||||
color: var(--vp-c-red-1, #ef4444);
|
||||
}
|
||||
.output .info {
|
||||
color: #60a5fa;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.output .welcome {
|
||||
color: #9ca3af;
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -125,32 +337,42 @@ const runCmd = (c) => {
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: #10b981;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.cmd-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #d1d5db;
|
||||
font-family: monospace;
|
||||
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.25rem 0.75rem;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border: none;
|
||||
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;
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<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>
|
||||
@@ -17,6 +21,10 @@
|
||||
|
||||
<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>
|
||||
@@ -29,22 +37,26 @@
|
||||
|
||||
<div class="controls">
|
||||
<button @click="localCommit" class="btn">本地提交</button>
|
||||
<button @click="remoteCommit" class="btn">远程新增提交</button>
|
||||
<button
|
||||
@click="push"
|
||||
:disabled="local.length <= remote.length"
|
||||
class="btn"
|
||||
>
|
||||
推送 Push
|
||||
git push
|
||||
</button>
|
||||
<button @click="pull" :disabled="!hasRemote" class="btn">
|
||||
拉取 Pull
|
||||
<button @click="pull" :disabled="behind === 0" class="btn">
|
||||
git pull
|
||||
</button>
|
||||
<button @click="reset" class="btn secondary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>💡 远程协作:</strong> Push 上传,Pull 下载,保持同步</p>
|
||||
<p>
|
||||
<strong>💡 远程协作:</strong> 你本地落后(Behind)就
|
||||
pull,你本地领先(Ahead)就 push。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -53,23 +65,33 @@
|
||||
import { ref, computed } from 'vue'
|
||||
const local = ref([])
|
||||
const remote = ref([])
|
||||
const hasRemote = ref(false)
|
||||
|
||||
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.push(...local.value.slice(remote.value.length))
|
||||
hasRemote.value = false
|
||||
remote.value = [...local.value]
|
||||
}
|
||||
|
||||
const pull = () => {
|
||||
if (hasRemote.value) local.value.push(Math.random().toString(16).substr(2, 7))
|
||||
hasRemote.value = false
|
||||
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 = []
|
||||
hasRemote.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,6 +120,26 @@ const reset = () => {
|
||||
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;
|
||||
}
|
||||
@@ -113,13 +155,13 @@ const reset = () => {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.dot.local {
|
||||
background: #3b82f6;
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
.dot.remote {
|
||||
background: #10b981;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.5);
|
||||
}
|
||||
.hash {
|
||||
font-family: monospace;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 0.875rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
<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" @click="prevStep" :disabled="activeStepIndex === 0">
|
||||
上一步
|
||||
</button>
|
||||
<button
|
||||
class="btn primary"
|
||||
@click="nextStep"
|
||||
:disabled="activeStepIndex >= activeScenario.steps.length - 1"
|
||||
>
|
||||
下一步
|
||||
</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 class="scenario-note" v-if="activeScenario.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: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 1.25rem;
|
||||
margin: 1rem 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: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.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: 1rem;
|
||||
border-radius: 8px;
|
||||
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>
|
||||
@@ -121,11 +121,22 @@
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const fileIdCounter = ref(1)
|
||||
|
||||
@@ -139,6 +150,32 @@ const workingFiles = ref([
|
||||
|
||||
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 = [
|
||||
@@ -152,6 +189,7 @@ const createNewFile = () => {
|
||||
name: randomType.name,
|
||||
icon: randomType.icon
|
||||
})
|
||||
pushHistory(`$ touch ${randomType.name}`)
|
||||
}
|
||||
|
||||
const addToStaging = (file) => {
|
||||
@@ -159,6 +197,7 @@ const addToStaging = (file) => {
|
||||
if (index !== -1) {
|
||||
workingFiles.value.splice(index, 1)
|
||||
stagedFiles.value.push(file)
|
||||
pushHistory(`$ git add ${file.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +206,7 @@ const unstageFile = (file) => {
|
||||
if (index !== -1) {
|
||||
stagedFiles.value.splice(index, 1)
|
||||
workingFiles.value.push(file)
|
||||
pushHistory(`$ git restore --staged ${file.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,14 +230,16 @@ const commitFiles = () => {
|
||||
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: 12px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
@@ -215,12 +257,11 @@ const commitFiles = () => {
|
||||
.zone {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 2px solid transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
@@ -262,8 +303,7 @@ const commitFiles = () => {
|
||||
|
||||
/* 1. Working Desk */
|
||||
.zone.working {
|
||||
border-color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
}
|
||||
.desk-surface {
|
||||
flex: 1;
|
||||
@@ -272,14 +312,15 @@ const commitFiles = () => {
|
||||
align-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
|
||||
background: var(--vp-c-bg-soft);
|
||||
background-size: 10px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.file-card {
|
||||
background: white;
|
||||
border: 1px solid #d1d5db;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
width: 80px;
|
||||
@@ -289,15 +330,12 @@ const commitFiles = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
transform: translateY(-4px) rotate(2deg);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.1);
|
||||
border-color: #f59e0b;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
@@ -316,8 +354,8 @@ const commitFiles = () => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(245, 158, 11, 0.9);
|
||||
color: white;
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.9);
|
||||
color: var(--vp-c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -332,8 +370,8 @@ const commitFiles = () => {
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
background: var(--vp-c-brand);
|
||||
color: var(--vp-c-bg);
|
||||
border: none;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
@@ -344,8 +382,7 @@ const commitFiles = () => {
|
||||
|
||||
/* 2. Staging Box */
|
||||
.zone.staging {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
}
|
||||
.box-container {
|
||||
flex: 1;
|
||||
@@ -360,8 +397,8 @@ const commitFiles = () => {
|
||||
.box-body {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
background: #dbeafe;
|
||||
border: 2px solid #3b82f6;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
position: relative;
|
||||
@@ -389,10 +426,10 @@ const commitFiles = () => {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.file-card.mini:hover {
|
||||
border-color: #ef4444;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.file-card.mini .action-hint {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
background: rgba(var(--vp-c-brand-rgb), 0.9);
|
||||
}
|
||||
|
||||
.box-flap {
|
||||
@@ -400,8 +437,8 @@ const commitFiles = () => {
|
||||
top: -20px;
|
||||
width: 45%;
|
||||
height: 20px;
|
||||
background: #93c5fd;
|
||||
border: 2px solid #3b82f6;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-bottom: none;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
@@ -423,30 +460,27 @@ const commitFiles = () => {
|
||||
text-align: center;
|
||||
}
|
||||
.commit-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
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;
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
.commit-btn:disabled {
|
||||
background: #93c5fd;
|
||||
background: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-2);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
.commit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 8px rgba(59, 130, 246, 0.4);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* 3. Repo Cabinet */
|
||||
.zone.repo {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
border-color: rgba(var(--vp-c-brand-rgb), 0.25);
|
||||
}
|
||||
.cabinet-body {
|
||||
flex: 1;
|
||||
@@ -457,22 +491,21 @@ const commitFiles = () => {
|
||||
}
|
||||
|
||||
.drawer-item {
|
||||
background: white;
|
||||
border: 1px solid #10b981;
|
||||
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;
|
||||
box-shadow: 0 2px 0 #059669; /* 3D effect */
|
||||
position: relative;
|
||||
}
|
||||
.drawer-handle {
|
||||
width: 30px;
|
||||
height: 6px;
|
||||
background: #d1fae5;
|
||||
border: 1px solid #10b981;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.commit-info {
|
||||
@@ -482,8 +515,8 @@ const commitFiles = () => {
|
||||
}
|
||||
.commit-hash {
|
||||
font-size: 0.6rem;
|
||||
color: #10b981;
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
.commit-msg {
|
||||
font-size: 0.8rem;
|
||||
@@ -521,6 +554,38 @@ const commitFiles = () => {
|
||||
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: 8px;
|
||||
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 {
|
||||
@@ -571,5 +636,9 @@ const commitFiles = () => {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,25 +9,36 @@
|
||||
<div class="git-workflow-demo">
|
||||
<!-- 控制面板 -->
|
||||
<div class="control-panel">
|
||||
<button @click="initRepo" :disabled="inited" class="action-btn">
|
||||
<button
|
||||
@click="initRepo"
|
||||
:disabled="inited || mergePending"
|
||||
class="action-btn"
|
||||
>
|
||||
🎯 初始化仓库
|
||||
</button>
|
||||
<button @click="makeCommit" :disabled="!inited" class="action-btn">
|
||||
<button
|
||||
@click="makeCommit"
|
||||
:disabled="!inited || mergePending"
|
||||
class="action-btn"
|
||||
>
|
||||
✅ 提交
|
||||
</button>
|
||||
<button
|
||||
@click="createBranch"
|
||||
:disabled="!inited || hasBranch"
|
||||
:disabled="!inited || hasBranch || mergePending"
|
||||
class="action-btn"
|
||||
>
|
||||
🌿 创建分支
|
||||
</button>
|
||||
<button
|
||||
@click="mergeBranch"
|
||||
:disabled="!hasBranch || merging"
|
||||
@click="prepareMerge"
|
||||
:disabled="!hasBranch || mergePending"
|
||||
class="action-btn"
|
||||
>
|
||||
🔀 合并分支
|
||||
🔀 准备合并
|
||||
</button>
|
||||
<button @click="finishMerge" :disabled="!mergePending" class="action-btn">
|
||||
✅ 完成合并
|
||||
</button>
|
||||
<button @click="reset" class="action-btn secondary">🔄 重置</button>
|
||||
</div>
|
||||
@@ -68,10 +79,10 @@
|
||||
|
||||
<!-- 合并线 -->
|
||||
<path
|
||||
v-if="merging"
|
||||
v-if="mergePending"
|
||||
d="M 300 100 Q 320 80, 320 60"
|
||||
fill="none"
|
||||
stroke="#f59e0b"
|
||||
stroke="var(--vp-c-brand)"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="5,5"
|
||||
/>
|
||||
@@ -127,12 +138,12 @@ import { ref, computed } from 'vue'
|
||||
|
||||
const inited = ref(false)
|
||||
const hasBranch = ref(false)
|
||||
const merging = ref(false)
|
||||
const mergePending = ref(false)
|
||||
const mainCommits = ref([])
|
||||
const branchCommits = ref([])
|
||||
|
||||
const status = computed(() => {
|
||||
if (merging) return '合并中...'
|
||||
if (mergePending.value) return '准备合并:检查改动/解决冲突后再完成合并'
|
||||
if (hasBranch) return '分支已创建'
|
||||
if (inited) return '已初始化'
|
||||
return '未初始化'
|
||||
@@ -156,22 +167,23 @@ const createBranch = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const mergeBranch = () => {
|
||||
if (hasBranch.value) {
|
||||
merging.value = true
|
||||
setTimeout(() => {
|
||||
mainCommits.value.push({ hash: Math.random().toString(16).substr(2, 6) })
|
||||
hasBranch.value = false
|
||||
branchCommits.value = []
|
||||
merging.value = false
|
||||
}, 1000)
|
||||
}
|
||||
const prepareMerge = () => {
|
||||
if (!hasBranch.value) return
|
||||
mergePending.value = true
|
||||
}
|
||||
|
||||
const finishMerge = () => {
|
||||
if (!mergePending.value) return
|
||||
mainCommits.value.push({ hash: Math.random().toString(16).substr(2, 6) })
|
||||
hasBranch.value = false
|
||||
branchCommits.value = []
|
||||
mergePending.value = false
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
inited.value = false
|
||||
hasBranch.value = false
|
||||
merging.value = false
|
||||
mergePending.value = false
|
||||
mainCommits.value = []
|
||||
branchCommits.value = []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user