Files
agent-frontend-web/docs/superpowers/plans/2026-07-15-shuzhi-ai-chat-plan.md
T
2026-08-07 13:56:29 +08:00

11 KiB
Raw Blame History

数智 AI 聊天页面实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 创建独立的「数智 AI」聊天页面,实现流式对话功能

Architecture: 单文件 Vue 组件实现,组合式 API + ElementPlus 组件,左侧边栏 + 中间消息区 + 底部输入栏的三栏布局

Tech Stack: Vue3 Composition API, ElementPlus, SCSS


Task 1: 创建 frontend 目录结构

Files:

  • 创建: src/views/frontend/ (目录)

  • Step 1: 创建目录

命令: mkdir -p src/views/frontend


Task 2: 创建主页面组件 index.vue

Files:

  • 创建: src/views/frontend/index.vue

  • Step 1: 编写页面基础模板结构

<template>
  <div class="shuzhi-ai-container">
    <!-- 左侧边栏 -->
    <aside class="sidebar">
      <!-- LOGO  -->
      <div class="logo-bar">
        <span class="logo-text">数智 AI</span>
        <el-button link @click="handleComingSoon">
          <el-icon><Fold /></el-icon>
        </el-button>
      </div>
      <!-- Tab 切换 -->
      <div class="tab-switch">
        <el-button size="small" @click="handleComingSoon">项目</el-button>
        <el-button size="small" @click="handleComingSoon">通用</el-button>
      </div>
      <!-- 树形列表 -->
      <div class="tree-section">
        <div class="tree-node tree-node-level1">
          <span class="node-label">数智AI Demo</span>
          <el-button link size="small" @click="handleComingSoon">×</el-button>
          <el-button link size="small" @click="handleComingSoon">+</el-button>
        </div>
        <div class="tree-node tree-node-level2" @click="handleComingSoon">
          <span class="node-label">欢迎使用 数智AI</span>
          <span class="node-hint">57 分钟前</span>
        </div>
      </div>
    </aside>

    <!-- 中间主内容区 -->
    <main class="main-content">
      <!-- 面包屑和功能导航 -->
      <div class="content-header">
        <el-breadcrumb separator="/">
          <el-breadcrumb-item>general</el-breadcrumb-item>
          <el-breadcrumb-item>智能体</el-breadcrumb-item>
          <el-breadcrumb-item>欢迎使用 数智AI</el-breadcrumb-item>
        </el-breadcrumb>
        <div class="nav-buttons">
          <el-button v-for="btn in navButtons" :key="btn" @click="handleComingSoon">{{ btn }}</el-button>
        </div>
      </div>

      <!-- 消息区域 -->
      <el-scrollbar ref="scrollRef" class="messages-container">
        <div class="messages-wrapper">
          <template v-for="(msg, index) in messages" :key="index">
            <div class="message-divider" v-if="index > 0"></div>
            <!-- 用户消息 -->
            <div v-if="msg.role === 'user'" class="message-item user">
              <div class="message-bubble">
                <span class="message-tag">已处理 1m</span>
                <div class="message-content">{{ msg.content }}</div>
              </div>
            </div>
            <!-- AI 消息 -->
            <div v-else class="message-item assistant">
              <div class="message-bubble">
                <div class="message-content" v-html="formatMessage(msg.content)"></div>
              </div>
            </div>
          </template>
        </div>
      </el-scrollbar>

      <!-- 底部输入栏 -->
      <div class="input-bar">
        <div class="input-controls">
          <el-button size="small" @click="handleComingSoon">智能体</el-button>
          <el-select size="small" placeholder="选择" @click="handleComingSoon" style="width: 100px">
            <el-option label="选项1" value="1" />
          </el-select>
          <el-button size="small" @click="handleComingSoon">@</el-button>
          <el-button size="small" @click="handleComingSoon">权限</el-button>
        </div>
        <div class="input-wrapper">
          <el-input
            v-model="inputText"
            type="textarea"
            :rows="2"
            placeholder="告诉 数智AI 你想完成什么..."
            resize="none"
            @keydown.enter.exact.prevent="handleSend"
          />
          <el-button
            type="primary"
            class="send-btn"
            :disabled="!inputText.trim() || loading"
            @click="handleSend"
            :loading="loading"
          >
            发送
          </el-button>
        </div>
      </div>
    </main>
  </div>
</template>
  • Step 2: 编写 script setup 部分
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Fold } from '@element-plus/icons-vue'

const inputText = ref('')
const messages = ref([])
const loading = ref(false)
const scrollRef = ref()

const navButtons = ['智能体', '文件', '技能', '路由', '记忆', '常驻']

function handleComingSoon() {
  ElMessage.info('敬请期待')
}

function formatMessage(content) {
  if (!content) return ''
  // 高亮代码关键词
  return content
    .replace(/`([^`]+)`/g, '<code>$1</code>')
    .replace(/\n/g, '<br/>')
}

async function handleSend() {
  if (!inputText.value.trim() || loading.value) return

  const userContent = inputText.value.trim()
  inputText.value = ''

  // 添加用户消息
  messages.value.push({
    role: 'user',
    content: userContent
  })

  scrollToBottom()
  loading.value = true

  try {
    const response = await fetch('http://192.168.5.125:18000/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer 5c49dd2a721e0f408ea49298b6c13ddf17267eb0b64e3da7019ee581f597408e',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'qwen3.6-27b-nvfp4',
        messages: [
          {
            role: 'user',
            content: `/no_think\n ${userContent}`
          }
        ],
        max_tokens: 200,
        temperature: 0.7
      })
    })

    if (!response.ok) {
      throw new Error(`请求失败: ${response.status}`)
    }

    // 处理流式响应
    const reader = response.body.getReader()
    const decoder = new TextDecoder()
    let aiContent = ''

    // 添加 AI 消息占位
    messages.value.push({
      role: 'assistant',
      content: ''
    })

    while (true) {
      const { done, value } = await reader.read()
      if (done) break

      const chunk = decoder.decode(value)
      aiContent += chunk

      // 更新最后一条 AI 消息
      const lastMsg = messages.value[messages.value.length - 1]
      lastMsg.content = aiContent
      scrollToBottom()
    }
  } catch (error) {
    ElMessage.error('问答失败:' + error.message)
  } finally {
    loading.value = false
  }
}

function scrollToBottom() {
  setTimeout(() => {
    if (scrollRef.value) {
      scrollRef.value.setScrollTop(9999999)
    }
  }, 100)
}
</script>
  • Step 3: 编写样式部分
<style scoped lang="scss">
.shuzhi-ai-container {
  display: flex;
  height: 100vh;
  overflow: hidden;
}

.sidebar {
  width: 220px;
  background: #F8F9FA;
  display: flex;
  flex-direction: column;
  border-right: 1px solid #E5E7EB;
}

.logo-bar {
  height: 56px;
  background: #0F172A;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 12px;
  flex-shrink: 0;

  .logo-text {
    color: #fff;
    font-size: 16px;
    font-weight: 600;
  }
}

.tab-switch {
  padding: 12px;
  display: flex;
  gap: 8px;
  border-bottom: 1px solid #E5E7EB;
}

.tree-section {
  flex: 1;
  overflow-y: auto;
  padding: 12px;
}

.tree-node {
  display: flex;
  align-items: center;
  padding: 8px;
  border-radius: 8px;
  cursor: pointer;

  &:hover {
    background: #E5E7EB;
  }
}

.tree-node-level1 {
  justify-content: space-between;

  .node-label {
    font-weight: 600;
    color: #111;
  }
}

.tree-node-level2 {
  flex-direction: column;
  align-items: flex-start;
  margin-left: 16px;
  padding: 6px 8px;

  .node-label {
    color: #222;
  }

  .node-hint {
    font-size: 12px;
    color: #888;
    margin-top: 2px;
  }
}

.main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #fff;
  overflow: hidden;
}

.content-header {
  padding: 12px 24px;
  border-bottom: 1px solid #E5E7EB;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-shrink: 0;
}

.nav-buttons {
  display: flex;
  gap: 8px;
}

.messages-container {
  flex: 1;
  overflow: hidden;
  padding: 20px;
}

.messages-wrapper {
  max-width: 800px;
  margin: 0 auto;
}

.message-divider {
  height: 1px;
  background: #E5E7EB;
  margin: 20px 0;
}

.message-item {
  display: flex;

  &.user {
    justify-content: flex-end;

    .message-bubble {
      background: #F1F3F5;
      border-radius: 8px;
      padding: 12px 16px;
      max-width: 70%;
    }

    .message-tag {
      display: block;
      font-size: 12px;
      color: #888;
      margin-bottom: 4px;
    }

    .message-content {
      color: #222;
      line-height: 1.6;
    }
  }

  &.assistant {
    justify-content: flex-start;

    .message-bubble {
      padding: 0;
      max-width: 70%;
    }

    .message-content {
      color: #222;
      line-height: 1.8;

      :deep(code) {
        background: #EEEEEE;
        padding: 2px 4px;
        border-radius: 4px;
        font-family: monospace;
      }
    }
  }
}

.input-bar {
  padding: 16px 24px;
  border-top: 1px solid #E5E7EB;
  flex-shrink: 0;
}

.input-controls {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
}

.input-wrapper {
  display: flex;
  gap: 12px;
  align-items: flex-end;

  .el-textarea {
    flex: 1;
  }
}

.send-btn {
  height: 60px;
  width: 80px;
  background: #F1F3F5;
  border: none;
  color: #444;
  border-radius: 8px;

  &:not(:disabled):hover {
    background: #E5E7EB;
  }

  &:disabled {
    background: #F8F9FA;
    color: #bbb;
  }
}
</style>

Task 3: 添加路由配置

Files:

  • 修改: src/router/index.js (在 dynamicRoutes 数组末尾添加)

  • Step 1: 添加路由配置

dynamicRoutes 数组末尾添加:

{
  path: '/frontend',
  component: Layout,
  children: [
    {
      path: 'index',
      component: () => import('@/views/frontend/index'),
      name: 'Frontend',
      meta: { title: '数智 AI' }
    }
  ]
}

Task 4: 验证实现

  • Step 1: 检查文件是否创建成功

命令: ls -la src/views/frontend/

  • Step 2: 验证路由配置语法

检查 src/router/index.js 中是否有新增的 /frontend 路由


自检清单

  • Spec 覆盖:所有需求点都有对应实现
  • 占位符扫描:无 TBD、TODO 等占位符
  • 类型一致性:所有方法名、变量名一致

Plan complete and saved to docs/superpowers/plans/2026-07-15-shuzhi-ai-chat-plan.md

Two execution options:

1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration

2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints

Which approach?