1
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
# 数智 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: 编写页面基础模板结构**
|
||||
|
||||
```vue
|
||||
<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 部分**
|
||||
|
||||
```javascript
|
||||
<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: 编写样式部分**
|
||||
|
||||
```scss
|
||||
<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` 数组末尾添加:
|
||||
|
||||
```javascript
|
||||
{
|
||||
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?**
|
||||
Reference in New Issue
Block a user