组件联动进阶:玩转 TinyRobot 组合开发提升项目灵活性

组件联动进阶:玩转 TinyRobot 组合开发,提升项目灵活性

AI 聊天应用不是单个组件的孤岛,而是多个组件协同工作的生态系统。用户输入消息(Sender)、查看消息列表(BubbleList)、选择提示(Prompts)、管理会话(useConversation)------这些环节需要无缝衔接,数据需要实时同步,状态需要一致联动。

TinyRobot 的组件设计遵循组合优于配置的原则,每个组件都是独立可用的,但通过事件、方法、组合式函数的衔接,可以构建出完整的聊天应用。本文将从 7 个联动场景出发,逐步构建一个完整的 AI 聊天应用。

Sender + BubbleList:输入与展示联动

这是最基础的联动模式:用户通过 Sender 输入消息,BubbleList 展示消息列表。

sendMessage 流程

核心流程:Sender submit → 创建 user 消息 → 创建 assistant 消息(loading)→ 请求 AI 响应 → 更新 assistant 消息内容。

vue 复制代码
<template>
  <tr-bubble-list
    :messages="messages"
    :role-configs="roleConfigs"
    :auto-scroll="true"
  />
  <tr-sender
    :loading="isLoading"
    @submit="handleSubmit"
    @cancel="handleCancel"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'

const messages = ref([])
const isLoading = ref(false)

const roleConfigs = {
  user: { placement: 'end', shape: 'corner' },
  assistant: { placement: 'start', shape: 'corner' }
}

const handleSubmit = async (text: string) => {
  // 1. 添加用户消息
  messages.value.push({
    role: 'user',
    content: text,
    id: `user-${Date.now()}`
  })

  // 2. 添加助手消息(loading 状态)
  const assistantId = `assistant-${Date.now()}`
  messages.value.push({
    role: 'assistant',
    content: '',
    loading: true,
    id: assistantId
  })

  isLoading.value = true

  // 3. 请求 AI 响应
  try {
    const response = await fetchAIResponse(text)
    // 4. 更新助手消息内容
    const msg = messages.value.find(m => m.id === assistantId)
    if (msg) {
      msg.content = response
      msg.loading = false
    }
  } catch (error) {
    const msg = messages.value.find(m => m.id === assistantId)
    if (msg) {
      msg.content = '请求失败,请重试'
      msg.loading = false
    }
  } finally {
    isLoading.value = false
  }
}

const handleCancel = () => {
  isLoading.value = false
  // 将最后一条 loading 的助手消息标记为已终止
  const lastMsg = messages.value[messages.value.length - 1]
  if (lastMsg?.loading) {
    lastMsg.loading = false
  }
}
</script>

loading 状态同步

Sender 的 loading 属性与 BubbleList 的消息 loading 状态需要同步:

  • Sender loading = true → 提交按钮变为停止按钮
  • BubbleList 中助手消息 loading = true → 显示加载动画

cancel 取消操作

点击 Sender 的停止按钮触发 cancel 事件,需要同时:

  1. 中止 AI 响应请求(如 abortRequest()
  2. 将 BubbleList 中 loading 状态的助手消息终止

Sender + Prompts:提示集与输入联动

Prompts 组件展示预设的提示列表,用户点击提示项后,文本填入 Sender。

item-click 事件与 setContent 方法

vue 复制代码
<template>
  <tr-prompts
    :items="promptItems"
    @item-click="handlePromptClick"
  />
  <tr-sender ref="senderRef" @submit="handleSubmit" />
</template>

<script setup lang="ts">
import { ref } from 'vue'

const senderRef = ref()

const promptItems = [
  { label: '帮我写一篇周报', description: '根据本周工作内容生成周报' },
  { label: '翻译以下内容', description: '将文本翻译为指定语言' },
  { label: '代码解释', description: '解释一段代码的逻辑和功能' },
  { label: '数据分析', description: '对给定数据进行分析和可视化建议' }
]

const handlePromptClick = (ev: MouseEvent, item: PromptProps) => {
  // 将提示文本填入 Sender
  senderRef.value?.setContent(item.label)
  senderRef.value?.focus()
}

const handleSubmit = (text: string) => {
  console.log('提交:', text)
}
</script>

关键衔接点:

  • Prompts 的 item-click 事件提供 (ev, item) 参数
  • Sender 的 setContent() 方法设置编辑器内容(v0.4 新增)
  • Sender 的 focus() 方法让编辑器获取焦点

这种"提示点击 → 内容填入 → 聚焦编辑器"的流程是 AI 聊天应用中最常见的交互模式。

Sender + Container:容器内嵌布局

Container 组件提供聊天界面的容器框架,Sender 通常放置在 Container 的 footer 插槽中。

全屏模式下的布局适配

vue 复制代码
<template>
  <tr-container
    v-model:show="showContainer"
    v-model:fullscreen="isFullscreen"
    title="AI 助手"
  >
    <tr-bubble-list :messages="messages" :auto-scroll="true" />

    <template #footer>
      <tr-sender
        v-model="inputText"
        :loading="isLoading"
        @submit="handleSubmit"
        @cancel="handleCancel"
      />
    </template>
  </tr-container>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const showContainer = ref(true)
const isFullscreen = ref(false)
const inputText = ref('')
const isLoading = ref(false)
const messages = ref([])

const handleSubmit = async (text: string) => {
  // 发送消息逻辑
}

const handleCancel = () => {
  isLoading.value = false
}
</script>

全屏模式下,Container 会加上 fullscreen 类名,此时可以使用 .fullscreen 选择器自定义 footer 插槽中 Sender 的样式:

css 复制代码
/* 全屏模式下 Sender 的样式调整 */
.fullscreen .tr-sender {
  --tr-sender-bg-color: #fff;
  --tr-sender-border-radius: 8px;
}

Container 的 CSS 变量也提供了全屏模式专属变量:

css 复制代码
:root {
  --tr-container-header-padding-fullscreen: 0 200px 24px;
  --tr-container-title-font-size-fullscreen: 20px;
}

Sender + Feedback:反馈与气泡联动

Feedback 组件提供对气泡消息的操作反馈(复制、点赞、刷新等),通常放置在 Bubble 的 content-footer 插槽中。

vue 复制代码
<template>
  <tr-bubble-list :messages="messages" :role-configs="roleConfigs">
    <template #content-footer="{ messages, role }">
      <!-- 仅对助手消息显示反馈 -->
      <tr-feedback
        v-if="role === 'assistant'"
        :actions="feedbackActions"
        @action="handleAction"
      />
    </template>
  </tr-bubble-list>
</template>

<script setup lang="ts">
const feedbackActions = [
  { name: 'copy', label: '复制' },
  { name: 'like', label: '点赞' },
  { name: 'dislike', label: '踩' },
  { name: 'refresh', label: '重新生成' }
]

const handleAction = (name: string) => {
  if (name === 'refresh') {
    // 重新触发 Sender 提交
    senderRef.value?.submit()
  }
  if (name === 'copy') {
    // 复制消息内容
    navigator.clipboard.writeText(currentMessage.content)
  }
}
</script>

Sender + useMessage:数据管理与输入联动

useMessage 是 TinyRobot 提供的消息数据管理组合式函数,它封装了发送消息、处理 AI 响应、管理请求状态等逻辑。与 Sender 联动后,开发者无需手动管理消息数组。

sendMessage 与 Sender.submit 对接

vue 复制代码
<template>
  <tr-bubble-list
    :messages="messageEngine.messages.value"
    :auto-scroll="true"
  />
  <tr-sender
    :loading="messageEngine.isProcessing.value"
    @submit="handleSenderSubmit"
    @cancel="handleSenderCancel"
  />
</template>

<script setup lang="ts">
import { useMessage } from '@opentiny/tiny-robot'

// 使用 useMessage 管理消息数据
const messageEngine = useMessage({
  responseProvider: async (requestBody, abortSignal) => {
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody),
      signal: abortSignal
    })
    // 处理 SSE 流式响应
    return sseStreamToGenerator(response)
  }
})

// Sender submit → useMessage.sendMessage
const handleSenderSubmit = (text: string) => {
  messageEngine.sendMessage(text)
}

// Sender cancel → useMessage.abortRequest
const handleSenderCancel = () => {
  messageEngine.abortRequest()
}
</script>

requestState 驱动 Sender 的 loading/disabled

useMessage 的 requestState 提供了更精细的状态控制:

typescript 复制代码
type RequestState = 'idle' | 'processing' | 'completed' | 'aborted' | 'error'
vue 复制代码
<template>
  <tr-sender
    :loading="messageEngine.isProcessing.value"
    :disabled="messageEngine.requestState.value === 'error'"
    @submit="handleSenderSubmit"
    @cancel="handleSenderCancel"
  />
</template>
  • isProcessing → Sender 的 loading(显示停止按钮)
  • requestState === 'error' → Sender 的 disabled(出错时禁用输入)

这种联动让 Sender 的状态完全由 useMessage 驱动,开发者无需手动管理 isLoading 状态。

Sender + useConversation:多会话联动

useConversation 在 useMessage 基础上增加了多会话管理能力。每个会话有独立的 useMessage 引擎,切换会话时需要同步更新 Sender 的状态。

activeConversation 与 Sender 联动

vue 复制代码
<template>
  <!-- 会话列表 -->
  <div class="conversation-list">
    <div
      v-for="conv in conversationEngine.conversations.value"
      :key="conv.id"
      :class="{ active: conv.id === conversationEngine.activeConversationId.value }"
      @click="switchConversation(conv.id)"
    >
      {{ conv.title || '新对话' }}
    </div>
    <button @click="createNewConversation">新建对话</button>
  </div>

  <!-- 消息展示 -->
  <tr-bubble-list
    :messages="currentMessages"
    :auto-scroll="true"
  />

  <!-- 输入框 -->
  <tr-sender
    :loading="currentEngine?.isProcessing.value"
    @submit="handleSenderSubmit"
    @cancel="handleSenderCancel"
  />
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useConversation } from '@opentiny/tiny-robot'

const conversationEngine = useConversation({
  useMessageOptions: {
    responseProvider: async (requestBody, abortSignal) => {
      // ...请求逻辑
    }
  }
})

// 当前会话的消息引擎
const currentEngine = computed(() =>
  conversationEngine.activeConversation.value?.engine
)

// 当前会话的消息列表
const currentMessages = computed(() =>
  currentEngine?.messages.value || []
)

// 切换会话
const switchConversation = (id: string) => {
  conversationEngine.switchConversation(id)
}

// 创建新会话
const createNewConversation = () => {
  conversationEngine.createConversation({ title: '新对话' })
}

// Sender submit → 当前会话的 sendMessage
const handleSenderSubmit = (text: string) => {
  conversationEngine.sendMessage(text)
}

// Sender cancel → 当前会话的 abortRequest
const handleSenderCancel = () => {
  conversationEngine.abortActiveRequest()
}
</script>

关键联动点:

  1. 切换会话switchConversation() → 更新 activeConversation → 更新 currentMessages → BubbleList 自动渲染新会话的消息
  2. 发送消息sendMessage() → 当前会话的 useMessage 引擎处理
  3. 取消请求abortActiveRequest() → 当前会话的请求被中止
  4. 并行处理:切换会话后,旧会话的请求可以在后台继续执行

Sender + ThemeProvider:主题联动

ThemeProvider 的主题切换会影响所有子组件的样式,包括 Sender。

vue 复制代码
<template>
  <theme-provider
    v-model:theme="currentTheme"
    v-model:color-mode="colorMode"
  >
    <tr-sender v-model="text" @submit="handleSubmit" />
  </theme-provider>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { ThemeProvider } from '@opentiny/tiny-robot'

const currentTheme = ref('')
const colorMode = ref('auto')
const text = ref('')

const handleSubmit = (content: string) => {
  console.log('提交:', content)
}
</script>

通过 CSS 属性选择器定制 Sender 在不同主题下的样式:

css 复制代码
/* 亮色模式 */
[data-tr-color-mode='light'] {
  --tr-sender-bg-color: #ffffff;
  --tr-sender-text-color: #333333;
  --tr-sender-placeholder-color: #999999;
}

/* 暗色模式 */
[data-tr-color-mode='dark'] {
  --tr-sender-bg-color: #1a1a2e;
  --tr-sender-text-color: #e0e0e0;
  --tr-sender-placeholder-color: #666666;
}

/* 自定义品牌主题 */
[data-tr-theme='brand'] {
  --tr-sender-bg-color: #f0f7ff;
  --tr-sender-border-radius: 16px;
  --tr-sender-button-size: 36px;
}

实战组合案例:完整 AI 聊天应用

将上述所有联动场景组合,构建一个完整的 AI 聊天应用:

vue 复制代码
<template>
  <theme-provider
    v-model:theme="theme"
    v-model:color-mode="colorMode"
    :storage="localStorage"
    storage-key="chat-app-theme"
  >
    <tr-container
      v-model:show="showChat"
      v-model:fullscreen="isFullscreen"
      title="AI 智能助手"
    >
      <!-- 会话切换 -->
      <div class="sidebar">
        <div
          v-for="conv in conversation.conversations.value"
          :key="conv.id"
          :class="{ active: conv.id === conversation.activeConversationId.value }"
          @click="conversation.switchConversation(conv.id)"
        >
          {{ conv.title || '新对话' }}
        </div>
        <button @click="createConversation">+ 新对话</button>
      </div>

      <!-- 消息展示区 -->
      <div class="chat-main">
        <!-- 提示集(无消息时显示) -->
        <tr-prompts
          v-if="currentMessages.length === 0"
          :items="promptItems"
          @item-click="handlePromptClick"
        />

        <!-- 消息列表 -->
        <tr-bubble-list
          v-else
          :messages="currentMessages"
          :role-configs="roleConfigs"
          :auto-scroll="true"
        >
          <template #content-footer="{ role }">
            <tr-feedback
              v-if="role === 'assistant' && !messageLoading"
              :actions="feedbackActions"
              @action="handleFeedbackAction"
            />
          </template>
        </tr-bubble-list>
      </div>

      <!-- 输入框 -->
      <template #footer>
        <tr-sender
          ref="senderRef"
          v-model="inputText"
          :loading="isProcessing"
          :default-actions="{
            submit: { tooltip: '发送(Enter)' },
            clear: { tooltip: '清空' }
          }"
          @submit="handleSubmit"
          @cancel="handleCancel"
        >
          <template #footer="{ disabled, loading, insert }">
            <tr-voice-button
              :disabled="disabled || loading"
              :speech-config="{ lang: 'zh-CN' }"
            />
            <tr-upload-button
              :disabled="disabled || loading"
              accept="image/*,.pdf"
              :max-size="20"
              @select="handleFileSelect"
            />
          </template>
        </tr-sender>
      </template>
    </tr-container>
  </theme-provider>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import {
  ThemeProvider,
  useConversation,
  useTheme
} from '@opentiny/tiny-robot'

// 主题配置
const theme = ref('')
const colorMode = ref('auto')
const { toggleColorMode, setTheme } = useTheme()

// 会话管理
const conversation = useConversation({
  useMessageOptions: {
    responseProvider: async (requestBody, abortSignal) => {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(requestBody),
        signal: abortSignal
      })
      return sseStreamToGenerator(response)
    }
  }
})

// 当前会话状态
const currentEngine = computed(() =>
  conversation.activeConversation.value?.engine
)
const currentMessages = computed(() => currentEngine?.messages.value || [])
const isProcessing = computed(() => currentEngine?.isProcessing.value || false)

// 组件引用
const senderRef = ref()
const inputText = ref('')
const showChat = ref(true)
const isFullscreen = ref(false)

// 提示项
const promptItems = [
  { label: '帮我写一篇周报', description: '根据本周工作内容生成周报' },
  { label: '翻译以下内容', description: '将文本翻译为指定语言' },
  { label: '代码解释', description: '解释一段代码的逻辑和功能' }
]

// 角色配置
const roleConfigs = {
  user: { placement: 'end', shape: 'corner' },
  assistant: { placement: 'start', shape: 'corner' }
}

// 反馈操作
const feedbackActions = [
  { name: 'copy', label: '复制' },
  { name: 'like', label: '点赞' },
  { name: 'refresh', label: '重新生成' }
]

// 事件处理
const handleSubmit = (text: string) => {
  conversation.sendMessage(text)
}

const handleCancel = () => {
  conversation.abortActiveRequest()
}

const handlePromptClick = (ev, item) => {
  senderRef.value?.setContent(item.label)
  senderRef.value?.focus()
}

const handleFileSelect = (files: File[]) => {
  console.log('文件:', files)
}

const handleFeedbackAction = (name: string) => {
  if (name === 'refresh') senderRef.value?.submit()
}

const createConversation = () => {
  conversation.createConversation({ title: '新对话' })
}
</script>

组合开发模式总结

TinyRobot 的组合开发遵循以下模式:

联动场景 衔接机制 核心方法/事件
Sender + BubbleList 事件 + 数据 submit → push message, cancel → abort
Sender + Prompts 事件 + 方法 item-click → setContent + focus
Sender + Container 插槽 footer 插槽放置 Sender
Sender + Feedback 插槽 + 事件 content-footer 插槽, action → submit
Sender + useMessage 组合式函数 sendMessage, isProcessing, abortRequest
Sender + useConversation 组合式函数 sendMessage, abortActiveRequest, switchConversation
Sender + ThemeProvider CSS 变量 data-tr-theme / data-tr-color-mode 属性选择器

核心原则:组件之间通过事件和方法衔接,而不是通过共享状态或直接引用。这种松耦合设计让每个组件都可以独立使用、独立测试,组合时只需要"接线"即可。


🔗 TinyRobot 官网tiny-robot.opentiny.design

🔗 GitHub 仓库github.com/opentiny/ti...

相关推荐
英勇无比的消炎药1 小时前
样式随心定制:TinyRobot 样式覆写与 CSS 变量实战解析
vue.js
疯狂的魔鬼1 小时前
多角色督办任务详情页:从权限矩阵到组件拆分的完整实现
前端·vue.js·架构
英勇无比的消炎药1 小时前
拆解内核:深入分析 TinyRobot 输入区组件设计与实现原理
vue.js
Cc_Debugger2 小时前
开发环境使用https配置
javascript·vue.js·https
触底反弹2 小时前
🎨 通义万相实战:用 Qwen 多模态 API 实现 AI 换装换姿势,10 行代码搞定!
vue.js·人工智能
零瓶水Herwt2 小时前
代替vue-currency-input使用原生货币符号
前端·vue.js
Cobyte3 小时前
20.Vue Vapor 的应用初始化
前端·javascript·vue.js
vx-Biye_Design3 小时前
springboot安阳地区研学旅游服务小程序-计算机毕业设计源码12785
java·vue.js·windows·spring boot·tomcat·maven·mybatis