结构化数据
通过 v-model:data 获取所有已解析区块的结构化信息。
<StreamContains v-model:data="blocks" :model-value="text" mode="accurate">
<Think />
</StreamContains>表单联动示例
配合内置表单组件(InputBlock、SelectBlock、ButtonBlock),可以实时捕获用户交互数据并反映在 blocks 中。
blocks (0)
[]
form state
{
"values": {},
"valid": true,
"errors": {},
"submitted": false
}<template>
<div class="s-form-demo">
<Input v-model="text" :rows="5" :simulate-text="exampleText" />
<hr>
<div class="s-form-demo-render">
<StreamContains v-model:data="blocks" :model-value="text" mode="accurate">
<InputBlock />
<SelectBlock />
<ButtonBlock />
</StreamContains>
</div>
<details class="s-form-demo-blocks" open>
<summary>blocks ({{ blocks.length }})</summary>
<pre>{{ JSON.stringify(blocks, null, 2) }}</pre>
</details>
<details class="s-form-demo-blocks" open>
<summary>form state</summary>
<pre>{{ JSON.stringify({
values: form.values,
valid: form.valid,
errors: form.errors,
submitted: form.submitted,
lastAction: form.lastAction
}, null, 2) }}</pre>
</details>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { StreamContains, InputBlock, SelectBlock, ButtonBlock, useStreamFormData } from 'stream-ui'
import Input from '../../.comm/Input.vue'
const exampleText = [
'<input-block id="name-input" name="name" label="姓名" placeholder="请输入姓名" required />',
'<select-block id="department-select" name="department" label="部门" options="技术部,市场部,财务部,人事部" required />',
'<button-block id="submit-application" action="submit" label="提交申请" />'
].join('\n')
const text = ref(exampleText)
const blocks = ref([])
const form = useStreamFormData(blocks)
</script>
<style scoped>
.s-form-demo-render {
padding: 8px 0;
}
.s-form-demo-blocks {
margin-top: 12px;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 8px 12px;
font-size: 13px;
}
.s-form-demo-blocks summary {
cursor: pointer;
font-weight: 600;
color: #475569;
}
.s-form-demo-blocks pre {
margin: 8px 0 0;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
</style>StreamBlockData
interface StreamBlockData {
id: string // 区块唯一 ID
category: 'component' | 'fallback' | 'text' // 区块分类
tagName: string // 标签名(小写)
attrs?: Record<string, string | boolean> // 标签属性
content: string // 内部内容
isClosed: boolean // 是否已闭合
payload?: unknown // 子组件回传数据
}三种 category:
component:命中注册组件的标签fallback:未注册标签,由 DefaultTag 渲染text:标签外的普通文本
去抖更新
areBlocksEqual 对比前后区块数组,若完全相同则跳过 emit('update:data')。更新在 queueMicrotask 中批量执行。
reportData
子组件调用 props.reportData(value) 后,对应区块的 payload 字段会更新,触发 v-model:data 更新。
<script setup>
const props = defineProps(['reportData'])
const onChange = (e) => {
props.reportData({ value: e.target.value })
}
</script>表单数据归一化
对于内置的 InputBlock、SelectBlock、ButtonBlock,可以使用 useStreamFormData(blocks) 将原始区块数组整理成表单视图。非 Vue 场景也可以直接调用 collectStreamFormData(blocks)。
import { ref } from 'vue'
import { useStreamFormData } from 'stream-ui'
const blocks = ref([])
const form = useStreamFormData(blocks)
console.log(form.value.values.email)form 同时提供提交和校验状态,适合直接接入业务提交逻辑:
if (form.value.submitted && form.value.valid) {
submit(form.value.values)
}
if (!form.value.valid) {
console.log(form.value.errors)
}建议让 LLM 为交互字段生成稳定的 id 和业务字段名 name:
<input-block id="email-input" name="email" label="邮箱" placeholder="请输入邮箱" required />
<select-block id="department-select" name="department" label="部门" options="技术部,市场部,财务部" required />
<button-block id="submit-button" action="submit" label="提交" />id 用于跨流式更新保留区块 payload,name 用于生成 form.values 的字段键。没有 name 时,collectStreamFormData 会回退使用区块 id。当 action="submit" 的按钮被点击后,form.submitted 会变为 true。
初始载荷持久化
initBlockPayloadMap 从外部 props.data 中读取已有 payload,跨渲染周期保留。
标签上的字符串 id 属性会优先作为 StreamBlockData.id。这能让交互组件在流式内容前面插入新文本或新标签后,依旧按稳定 ID 找回已有 payload。
<input-block id="email" label="邮箱" placeholder="请输入邮箱" />如果没有提供字符串 id,Stream UI 会继续使用自动生成的区块 ID。文本区块也始终使用自动 ID。
自闭合标签
<br />、<input name="email" /> 等自闭合标签:
isClosed立即为true- 不包含内容
- 不影响后续文本的解析