feat: add interactive demos for AI history, Auth design, and Git intro

This commit is contained in:
sanbuphy
2026-01-19 11:25:10 +08:00
parent bb28f010e3
commit 7d86ba9504
55 changed files with 12984 additions and 5776 deletions
@@ -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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('\"', '&quot;')
.replaceAll("'", '&#039;')
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;