From 4280da9fecb7810290675e8ba7f9698a058efe86 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 3 Mar 2026 10:17:39 +0800 Subject: [PATCH] refactor: reorg --- src/components/md-yarn-spinner.tsx | 185 +++++++++-------------------- src/components/stores/yarnStore.ts | 122 +++++++++++++++++++ src/styles.css | 49 -------- 3 files changed, 180 insertions(+), 176 deletions(-) create mode 100644 src/components/stores/yarnStore.ts diff --git a/src/components/md-yarn-spinner.tsx b/src/components/md-yarn-spinner.tsx index 6d58884..001e3b0 100644 --- a/src/components/md-yarn-spinner.tsx +++ b/src/components/md-yarn-spinner.tsx @@ -1,105 +1,38 @@ import { customElement, noShadowDOM } from 'solid-element'; -import {createSignal, createResource, For, Show, createEffect} from 'solid-js'; -import { parseYarn, compile, YarnRunner } from '../yarn-spinner'; -import type { RunnerOptions } from '../yarn-spinner/runtime/runner'; -import type { RuntimeResult, TextResult, OptionsResult } from '../yarn-spinner/runtime/results'; -import {loadElementSrc, resolvePath} from "./utils/path"; +import { For, Show, createEffect } from 'solid-js'; +import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results'; +import { createYarnStore } from './stores/yarnStore'; +import {RunnerOptions} from "../yarn-spinner/runtime/runner"; -// 缓存已加载的 yarn 文件内容 -const yarnCache = new Map(); - -// 加载 yarn 文件内容 -async function loadYarnFile(path: string): Promise { - if (yarnCache.has(path)) { - return yarnCache.get(path)!; - } - const response = await fetch(path); - const content = await response.text(); - yarnCache.set(path, content); - return content; -} - -// 加载多个 yarn 文件并拼接 -async function loadYarnFiles(paths: string[]): Promise { - const contents = await Promise.all(paths.map(path => loadYarnFile(path))); - return contents.join('\n'); -} - -customElement('md-yarn-spinner', { - startAt: "start", -}, (props, { element }) => { +customElement('md-yarn-spinner', {startAt: 'start'}, (props, { element }) => { noShadowDOM(); - const [dialogueHistory, setDialogueHistory] = createSignal([]); - const [currentOptions, setCurrentOptions] = createSignal(null); - const [isEnded, setIsEnded] = createSignal(false); - const [runnerInstance, setRunnerInstance] = createSignal(null); - - // 获取文件路径 - const {articlePath, rawSrc} = loadElementSrc(element); - const yarnPaths = (rawSrc || '').split(',') - .map((s: string) => resolvePath(articlePath, s.trim())) - .filter(Boolean); - - // 加载 yarn 内容 - const [yarnContent] = createResource(() => yarnPaths, loadYarnFiles); - - // 创建 runner - const createRunner = () => { - const content = yarnContent(); - if (!content) return null; - - try { - const ast = parseYarn(content); - const program = compile(ast); - - return new YarnRunner(program, props); - } catch (error) { - console.error('Failed to initialize YarnRunner:', error); - return null; + let historyContainer: HTMLDivElement | undefined; + + const { store, advance, restart } = createYarnStore(element as any, props); + + // 滚动到底部 + const scrollToBottom = () => { + if (historyContainer) { + setTimeout(() => { + historyContainer!.scrollTop = historyContainer!.scrollHeight; + }, 0); } }; - - function advance(index?: number){ - const runner = runnerInstance(); - if(!runner) return; - if(runner.currentResult?.type !== 'options' && runner.currentResult?.isDialogueEnd) return; - runner.advance(index); - processRunnerOutput(runner); - } - + + // 监听对话历史变化,自动滚动到底部 createEffect(() => { - if(!yarnContent()) return; - setRunnerInstance(createRunner()); - processRunnerOutput(runnerInstance()!); + store.dialogueHistory; + scrollToBottom(); }); - - // 处理 runner 输出 - const processRunnerOutput = (runner: YarnRunner) => { - const result = runner.currentResult; - if (!result) return; - - if(result.type === 'options'){ - setCurrentOptions(result); - }else{ - setCurrentOptions(null); - } - - setDialogueHistory([...runner.history]); - }; - - // 重新开始 - const restart = () => { - setDialogueHistory([]); - setCurrentOptions(null); - setIsEnded(false); - setRunnerInstance(createRunner()); - processRunnerOutput(runnerInstance()!); - }; - + // 渲染对话历史项 - const renderEntry = (result: RuntimeResult) => { - + const renderEntry = (result: RuntimeResult | OptionsResult['options'][0]) => { + if(!('type' in result)){ + return
+ {"->"} {result.text} +
+ } if (result.type === 'text') { const textResult = result as TextResult; return ( @@ -111,7 +44,7 @@ customElement('md-yarn-spinner', { ); } - + if (result.type === 'command') { return (
@@ -119,30 +52,49 @@ customElement('md-yarn-spinner', {
); } - + return null; }; - + return ( -
+
{/* 对话历史 */} -
- +
+
点击重新开始开始对话
- -
加载对话中...
-
- + {renderEntry}
+ {/* 浮动工具栏 */} +
+ + +
+ {/* 当前选项 */} - -
+
- + {(option, index) => (
- - - {/* 浮动工具栏 */} -
- - -
); }); diff --git a/src/components/stores/yarnStore.ts b/src/components/stores/yarnStore.ts new file mode 100644 index 0000000..3d03a7b --- /dev/null +++ b/src/components/stores/yarnStore.ts @@ -0,0 +1,122 @@ +import {createEffect, createResource } from "solid-js"; +import type {OptionsResult, RuntimeResult} from "../../yarn-spinner/runtime/results"; +import {compile, parseYarn, YarnRunner} from "../../yarn-spinner"; +import {loadElementSrc, resolvePath} from "../utils/path"; +import {createStore} from "solid-js/store"; +import {RunnerOptions} from "../../yarn-spinner/runtime/runner"; + +type YarnSpinnerStore = { + dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[], + currentOptions: OptionsResult | null, + isEnded: boolean, + runnerInstance: YarnRunner | null, +} + +export function createYarnStore(element: HTMLElement, props: RunnerOptions){ + const [store, setStore] = createStore({ + dialogueHistory: [], + currentOptions: null, + isEnded: false, + runnerInstance: null + }); + + // 获取文件路径 + const {articlePath, rawSrc} = loadElementSrc(element); + const yarnPaths = (rawSrc || '').split(',') + .map((s: string) => resolvePath(articlePath, s.trim())) + .filter(Boolean); + + // 加载 yarn 内容 + const [yarnContent] = createResource(() => yarnPaths, loadYarnFiles); + + // 创建 runner + const createRunner = () => { + const content = yarnContent(); + if (!content) return null; + + try { + const ast = parseYarn(content); + const program = compile(ast); + + return new YarnRunner(program, props); + } catch (error) { + console.error('Failed to initialize YarnRunner:', error); + return null; + } + }; + + function advance(index?: number){ + const runner = store.runnerInstance; + if(!runner) return; + if(index === undefined && runner.currentResult?.isDialogueEnd) return; + if(runner.currentResult?.type === 'options'){ + if(index === undefined)return; + const option = runner.currentResult.options[index]; + setStore('dialogueHistory', [...store.dialogueHistory, option]); + } + runner.advance(index); + processRunnerOutput(); + } + + createEffect(() => { + if(!yarnContent()) return; + setStore('runnerInstance', createRunner()); + requestAnimationFrame(function(){ + advance(); + }); + }); + + // 处理 runner 输出 + const processRunnerOutput = () => { + const runner = store.runnerInstance; + if(!runner)return; + + const result = runner.currentResult; + if (!result) return; + + if(result.type === 'options'){ + setStore('currentOptions', result); + }else{ + setStore('currentOptions', null); + } + + setStore('dialogueHistory', [...store.dialogueHistory, result]); + }; + + // 重新开始 + const restart = () => { + setStore({ + dialogueHistory: [], + currentOptions: null, + isEnded: false, + runnerInstance: createRunner(), + }); + processRunnerOutput(); + }; + + return { + store, + advance, + restart, + } +} + +// 缓存已加载的 yarn 文件内容 +const yarnCache = new Map(); + +// 加载 yarn 文件内容 +async function loadYarnFile(path: string): Promise { + if (yarnCache.has(path)) { + return yarnCache.get(path)!; + } + const response = await fetch(path); + const content = await response.text(); + yarnCache.set(path, content); + return content; +} + +// 加载多个 yarn 文件并拼接 +async function loadYarnFiles(paths: string[]): Promise { + const contents = await Promise.all(paths.map(path => loadYarnFile(path))); + return contents.join('\n'); +} diff --git a/src/styles.css b/src/styles.css index 292da36..f4c13ea 100644 --- a/src/styles.css +++ b/src/styles.css @@ -80,52 +80,3 @@ icon{ .col-3{ @apply lg:flex-3; } .col-4{ @apply lg:flex-4; } .col-5{ @apply lg:flex-5; } - -/* yarn-spinner */ - -.yarn-spinner { - @apply font-sans; -} - -.yarn-spinner .dialogue-history { - @apply space-y-3; -} - -.yarn-spinner .dialogue-entry { - @apply leading-relaxed; -} - -.yarn-spinner .text-entry { - @apply text-gray-800; -} - -.yarn-spinner .speaker { - @apply text-blue-600 font-semibold; -} - -.yarn-spinner .command-entry { - @apply text-sm text-gray-500 italic; -} - -.yarn-spinner .options-entry { - @apply mt-2; -} - -.yarn-spinner .selected-option { - @apply text-gray-600 font-medium; -} - -.yarn-spinner .option-button { - @apply w-full text-left px-4 py-2.5 bg-blue-50 hover:bg-blue-100 - rounded border border-blue-200 transition-colors cursor-pointer - text-gray-700 hover:text-gray-900; -} - -.yarn-spinner .toolbar { - @apply bg-gray-50 border-t border-gray-200; -} - -.yarn-spinner .restart-button { - @apply px-4 py-1.5 text-sm bg-gray-200 hover:bg-gray-300 - rounded transition-colors cursor-pointer text-gray-700; -} \ No newline at end of file