docs: 补充 AI 打字机输出、小程序灵感卡片、脚本主页布局等设计文档和计划

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 18:39:26 +08:00
parent 886f04046b
commit 6e5a379bef
12 changed files with 722 additions and 0 deletions
@@ -0,0 +1,125 @@
import { computed, ref } from 'vue'
export const useTypewriterStream = ({ interval = 24, step = 1 } = {}) => {
const visibleText = ref('')
const targetText = ref('')
const received = ref(false)
const backendDone = ref(false)
const failed = ref(false)
let timer = null
let waiters = []
const isWaiting = computed(() => !received.value && !backendDone.value && !failed.value)
const isStreaming = computed(() => received.value && (visibleText.value.length < targetText.value.length || !backendDone.value))
const isDraining = computed(() => backendDone.value && visibleText.value.length < targetText.value.length)
const isDone = computed(() => backendDone.value && visibleText.value.length >= targetText.value.length && !failed.value)
const stopTimer = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
const resolveWaiters = () => {
if (!waiters.length) return
const currentWaiters = waiters
waiters = []
currentWaiters.forEach(resolve => resolve(visibleText.value))
}
const isFullyRendered = () => {
return backendDone.value && visibleText.value.length >= targetText.value.length
}
const tick = () => {
if (visibleText.value.length < targetText.value.length) {
const nextLength = Math.min(targetText.value.length, visibleText.value.length + step)
visibleText.value = targetText.value.slice(0, nextLength)
return
}
if (backendDone.value || failed.value) {
stopTimer()
if (isFullyRendered() || failed.value) {
resolveWaiters()
}
}
}
const ensureTimer = () => {
if (!timer) {
timer = setInterval(tick, interval)
}
}
const reset = () => {
stopTimer()
visibleText.value = ''
targetText.value = ''
received.value = false
backendDone.value = false
failed.value = false
resolveWaiters()
}
const push = (nextText = '') => {
const next = String(nextText || '')
if (next.length < visibleText.value.length) {
reset()
}
received.value = true
targetText.value = next
ensureTimer()
}
const finish = (finalText) => {
if (typeof finalText === 'string') {
targetText.value = finalText
}
backendDone.value = true
if (targetText.value.length > visibleText.value.length) {
ensureTimer()
} else {
stopTimer()
resolveWaiters()
}
}
const fail = (message) => {
failed.value = true
backendDone.value = true
if (!visibleText.value && message) {
visibleText.value = message
targetText.value = message
}
stopTimer()
resolveWaiters()
}
const waitForDone = () => {
if (isFullyRendered() || failed.value) {
return Promise.resolve(visibleText.value)
}
return new Promise(resolve => {
waiters.push(resolve)
ensureTimer()
})
}
return {
visibleText,
targetText,
isWaiting,
isStreaming,
isDraining,
isDone,
push,
finish,
waitForDone,
fail,
reset,
dispose: stopTimer
}
}
export default useTypewriterStream