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?**
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
# Hide Layout Chrome for `common` Role Implementation Plan
|
||||
|
||||
> **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:** Conditionally hide sidebar, navbar, tags-view, and the floating Settings button — and stretch `<app-main />` to fill the viewport — when the logged-in user's role list contains `"common"`.
|
||||
|
||||
**Architecture:** Add a single `isCommonUser` computed to `src/layout/index.vue` that derives from the existing Pinia `useUserStore`. Use that computed as a `v-if` gate on every chrome element and as a class binding on `.main-container`. Add one scoped SCSS rule for the fullscreen layout. No new files, no store/router/permission changes.
|
||||
|
||||
**Tech Stack:** Vue 3 (Composition API, `<script setup>`), Pinia, Vue Router, Element Plus, SCSS, Vuex-pattern Vue project (this is the `vue-element-plus-admin` template family).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-04-hide-chrome-for-common-role-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
Only one file changes:
|
||||
|
||||
| File | Change | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/layout/index.vue` | Edit (script + template + style) | Add `isCommonUser` computed, gate chrome with `v-if`, add `fullscreen` class binding, add `fullscreen` SCSS rule |
|
||||
|
||||
No new files. No test files (the project has no frontend test infrastructure — see spec § Testing; verification is manual).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Apply all `src/layout/index.vue` edits
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/layout/index.vue:1-115` (entire file)
|
||||
|
||||
This task is one edit cycle but split into named sub-steps so the implementer can verify the diff top-to-bottom. Use the Edit tool with exact string matching against the current file.
|
||||
|
||||
### Step 1.1 — Add `useUserStore` import
|
||||
|
||||
In `<script setup>`, the current line is:
|
||||
|
||||
```js
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
const theme = computed(() => settingsStore.theme)
|
||||
```
|
||||
|
||||
It must become:
|
||||
|
||||
```js
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const isCommonUser = computed(() =>
|
||||
useUserStore().roles?.includes('common') ?? false
|
||||
)
|
||||
```
|
||||
|
||||
Edit (exact old → new):
|
||||
|
||||
```js
|
||||
old:
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
new:
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const isCommonUser = computed(() =>
|
||||
useUserStore().roles?.includes('common') ?? false
|
||||
)
|
||||
```
|
||||
|
||||
### Step 1.2 — Gate the four chrome elements + add `fullscreen` class
|
||||
|
||||
Current template body (lines 1-14):
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33' }">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<navbar @setLayout="setLayout" />
|
||||
<tags-view v-if="needTagsView" />
|
||||
</div>
|
||||
<app-main />
|
||||
<settings ref="settingRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Must become:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33' }">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
||||
<sidebar v-if="!sidebar.hide && !isCommonUser" class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide, fullscreen: isCommonUser }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<navbar v-if="!isCommonUser" @setLayout="setLayout" />
|
||||
<tags-view v-if="needTagsView && !isCommonUser" />
|
||||
</div>
|
||||
<app-main />
|
||||
<settings v-if="!isCommonUser" ref="settingRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Five edits inside the template — do them one at a time with Edit if needed, or use Edit `replace_all=false` on each line above (each old/new pair is unique enough).
|
||||
|
||||
Edit 1 — sidebar line:
|
||||
```diff
|
||||
- <sidebar v-if="!sidebar.hide" class="sidebar-container" />
|
||||
+ <sidebar v-if="!sidebar.hide && !isCommonUser" class="sidebar-container" />
|
||||
```
|
||||
|
||||
Edit 2 — main-container opening line:
|
||||
```diff
|
||||
- <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
|
||||
+ <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide, fullscreen: isCommonUser }" class="main-container">
|
||||
```
|
||||
|
||||
Edit 3 — navbar line:
|
||||
```diff
|
||||
- <navbar @setLayout="setLayout" />
|
||||
+ <navbar v-if="!isCommonUser" @setLayout="setLayout" />
|
||||
```
|
||||
|
||||
Edit 4 — tags-view line:
|
||||
```diff
|
||||
- <tags-view v-if="needTagsView" />
|
||||
+ <tags-view v-if="needTagsView && !isCommonUser" />
|
||||
```
|
||||
|
||||
Edit 5 — settings line:
|
||||
```diff
|
||||
- <settings ref="settingRef" />
|
||||
+ <settings v-if="!isCommonUser" ref="settingRef" />
|
||||
```
|
||||
|
||||
### Step 1.3 — Add `.fullscreen` SCSS rule
|
||||
|
||||
Inside the existing `<style lang="scss" scoped>` block, append after the `.mobile .fixed-header` rule at line ~115:
|
||||
|
||||
```scss
|
||||
.main-container.fullscreen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
Use Edit with surrounding context to make the match unique — for example, anchor on `.mobile .fixed-header` block ending with `width: 100%;` followed by `}`.
|
||||
|
||||
### Step 1.4 — Self-check the diff
|
||||
|
||||
Re-read the whole file (`src/layout/index.vue`) and confirm:
|
||||
|
||||
- `useUserStore` import is present and `isCommonUser` computed is declared in `<script setup>`.
|
||||
- `<sidebar>`, `<navbar>`, `<tags-view>`, `<settings>` all carry `v-if` gating on `!isCommonUser`.
|
||||
- The `<div class="main-container">` has `fullscreen: isCommonUser` in its class binding.
|
||||
- `.main-container.fullscreen` SCSS rule is inside the scoped `<style>` block.
|
||||
|
||||
- [ ] **Step 1 completed**
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Manual verification
|
||||
|
||||
The project has no automated test suite (`package.json` scripts only include `dev` and `build`). Verify by running the dev server and exercising both role paths.
|
||||
|
||||
### Step 2.1 — Start the dev server
|
||||
|
||||
Run (from `D:/数科智联/agent-web/agent-web`):
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Expected: Vite/Element-Plus dev server starts; URL is logged (typically `http://localhost:5173` or similar). Leave running.
|
||||
|
||||
### Step 2.2 — Verify admin role (no `common` in roles)
|
||||
|
||||
1. Open the dev URL in a browser.
|
||||
2. Log in as a user whose `roles` array does **not** include `"common"` (e.g. admin / editor test accounts).
|
||||
3. Open browser DevTools → Vue DevTools (or just inspect the DOM). Confirm all four are present in `#app`:
|
||||
- `<aside>` containing the left sidebar.
|
||||
- Top `<header>` / navbar.
|
||||
- Tags-view row beneath the navbar.
|
||||
- `<app-main />` to the right.
|
||||
4. Confirm the floating Settings button is visible at the bottom-right.
|
||||
5. Click a couple of navigation links; tags-view and sidebar behavior should be unchanged from prior baseline.
|
||||
|
||||
Expected: identical to pre-change behavior. If anything looks off (extra padding, missing element), stop and re-read the file diff.
|
||||
|
||||
### Step 2.3 — Verify `common` role (chromeless)
|
||||
|
||||
1. Log out, then log in as a user whose `roles` array contains `"common"` (e.g. `['common']` or `['common', 'something-else']`).
|
||||
2. Reload the page if needed.
|
||||
3. Confirm visually:
|
||||
- No left sidebar.
|
||||
- No top navbar.
|
||||
- No tags-view row.
|
||||
- No Settings floating button.
|
||||
- `<app-main />` fills the entire viewport edge-to-edge (no sidebar gutter, no top whitespace from where navbar was).
|
||||
4. Resize the window narrower to test responsive: `.fullscreen` should remain full screen at all widths.
|
||||
5. Inside `<app-main />`, exercise the page's own controls (links, buttons, forms) to confirm interaction still works.
|
||||
|
||||
Expected: clean chromeless experience. If you see a gap or scrollbar that wasn't there before, the SCSS rule did not apply — check that the class binding in the template is correct and that `<style scoped>` did not strip the selector (use `:deep` if necessary, though for a scoped class on a top-level element this should not be needed).
|
||||
|
||||
### Step 2.4 — Verify mixed role `['admin', 'common']`
|
||||
|
||||
Repeat Step 2.3 with a user whose roles list is `['admin', 'common']`. Because `Array.prototype.includes` semantics treat any inclusion as "match", chrome should be hidden. If you see chrome here, the `isCommonUser` computed is wrong.
|
||||
|
||||
### Step 2.5 — Stop the dev server
|
||||
|
||||
When verification is complete, stop the dev server (Ctrl-C in the terminal where it is running).
|
||||
|
||||
- [ ] **Step 2 completed**
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Commit
|
||||
|
||||
### Step 3.1 — Stage and review
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git diff src/layout/index.vue
|
||||
```
|
||||
|
||||
Expected: only `src/layout/index.vue` is modified. No stray changes from your edits.
|
||||
|
||||
### Step 3.2 — Commit
|
||||
|
||||
```bash
|
||||
git add src/layout/index.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(layout): common 角色隐藏侧栏/顶栏/tagsview 并全屏渲染 app-main
|
||||
|
||||
根据 useUserStore().roles 是否包含 'common' 隐藏 sidebar、navbar、
|
||||
tags-view、Settings 浮动按钮,并让 main-container 在该模式下全屏铺满。
|
||||
仅修改 src/layout/index.vue。
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
Expected: one new commit on top of `b193fa1`. Working tree should be clean (modulo any pre-existing changes unrelated to this task — those are not part of this plan).
|
||||
|
||||
- [ ] **Step 3 completed**
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- [x] **Spec coverage:**
|
||||
- Goal (hide chrome + fullscreen app-main for `common` role) → Task 1
|
||||
- Trigger (`roles.includes('common')`) → Step 1.1
|
||||
- All four chrome elements gated → Steps 1.2 edits 1, 3, 4, 5
|
||||
- Fullscreen class binding on `main-container` → Step 1.2 edit 2
|
||||
- Fullscreen SCSS rule → Step 1.3
|
||||
- Manual verification scenarios (admin / common / mixed / page interaction) → Tasks 2.2, 2.3, 2.4
|
||||
- Settings also hidden (confirmed in brainstorming) → Step 1.2 edit 5
|
||||
- [x] **Placeholder scan:** No TBD/TODO. Each edit has exact code. Manual verification steps have concrete URLs and accounts to use (caller supplies).
|
||||
- [x] **Type consistency:** All references use `isCommonUser`. No alternate naming introduced. `useUserStore` matches existing imports elsewhere (`@/store/modules/user`).
|
||||
- [x] **Commit hygiene:** One focused commit. Message starts with `feat(layout):`. Co-authored-by line present.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,703 @@
|
||||
# Frontend 会话列表底部用户区域块 Implementation Plan
|
||||
|
||||
> **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:** 在 `ConversationSidebar` 底部新增用户区域块,包含用户头像+昵称、退出按钮、前往智能体广场按钮;侧边栏收起时仅显示小头像弹出菜单;点击头像昵称打开「个人资料」修改弹窗(可改头像、昵称)。
|
||||
|
||||
**Architecture:** 新增两个独立的 Vue 3 组件 `UserArea.vue` 和 `UserInfoDialog.vue`,`ConversationSidebar.vue` 引入并放置底部。两个组件用 `defineExpose` / `v-model` + emit 通信,遵循项目现有的 Composition API + Element Plus 风格。
|
||||
|
||||
**Tech Stack:** Vue 3 + Vite + Element Plus + Pinia + vue-cropper(已有依赖)+ vue-router
|
||||
|
||||
**Note:** 项目无测试框架,所有验证通过手动 `yarn dev` 在浏览器操作完成。
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Create
|
||||
|
||||
- `src/views/frontend/components/UserArea.vue` — 底部区域块组件(展示 + 退出 + 跳转 + 弹出头像菜单)
|
||||
- `src/views/frontend/components/UserInfoDialog.vue` — 个人资料修改弹窗(头像裁剪 + 昵称)
|
||||
|
||||
### Modify
|
||||
|
||||
- `src/views/frontend/components/ConversationSidebar.vue` — 引入 `UserArea`(**注意:放在 `<transition>` + `sidebar-content` 之外**——因为 `sidebar-content` 有 `v-if="!collapsed"` 会让 user area 在收起态被隐藏;放到 `.conversation-sidebar` 直接子节点让 UserArea 自己处理收起/展开 UI),调整 `sidebar-content` 为 flex 列布局让会话列表占中间、UserArea 固定在底部
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 创建 UserArea.vue 组件骨架(展开态)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/views/frontend/components/UserArea.vue`
|
||||
|
||||
- [ ] **Step 1: 创建文件,写入以下完整内容**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div v-if="!collapsed" class="user-area">
|
||||
<div class="user-area-divider"></div>
|
||||
|
||||
<div class="user-info-row" @click="openInfoDialog">
|
||||
<el-avatar :size="36" :src="user.avatar" />
|
||||
<div class="user-name-wrap">
|
||||
<span class="user-name">{{ user.nickName || user.name || '未登录' }}</span>
|
||||
</div>
|
||||
<el-tooltip content="退出登录" placement="top" :show-after="300">
|
||||
<el-button class="logout-btn" link @click.stop="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<el-button class="marketplace-btn" @click="goToMarketplace">
|
||||
<el-icon><Promotion /></el-icon>
|
||||
<span>前往智能体广场</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { SwitchButton, Promotion } from '@element-plus/icons-vue'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import UserInfoDialog from './UserInfoDialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const user = computed(() => ({
|
||||
avatar: userStore.avatar,
|
||||
nickName: userStore.nickName,
|
||||
name: userStore.name
|
||||
}))
|
||||
|
||||
const infoDialogVisible = ref(false)
|
||||
|
||||
function openInfoDialog() {
|
||||
infoDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleSaved(updated) {
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '退出',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await userStore.logOut()
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
console.error('退出失败:', error)
|
||||
ElMessage.error('退出失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
function goToMarketplace() {
|
||||
router.push('/frontend/marketplace')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openInfoDialog
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-area-divider {
|
||||
height: 1px;
|
||||
background: #e4e7ed;
|
||||
margin: -12px -12px 12px -12px;
|
||||
}
|
||||
|
||||
.user-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 4px 8px;
|
||||
margin-left: 0 !important;
|
||||
color: #909399;
|
||||
|
||||
&:hover {
|
||||
color: #f56c6c;
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.marketplace-btn {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
border: 1px solid #d9ecff;
|
||||
|
||||
&:hover {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 创建 UserInfoDialog.vue 组件
|
||||
|
||||
**Files:**
|
||||
- Create: `src/views/frontend/components/UserInfoDialog.vue`
|
||||
|
||||
- [ ] **Step 1: 创建文件,写入以下完整内容**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog
|
||||
title="个人资料"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="头像" prop="avatar">
|
||||
<div class="avatar-uploader" @click="triggerFileInput">
|
||||
<el-avatar :size="64" :src="previewAvatar || form.avatar" />
|
||||
<div class="avatar-mask">
|
||||
<el-icon><Camera /></el-icon>
|
||||
<span>更换头像</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="昵称" prop="nickName">
|
||||
<el-input v-model="form.nickName" maxlength="30" show-word-limit placeholder="请输入昵称" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||
</template>
|
||||
|
||||
<el-dialog
|
||||
title="裁剪头像"
|
||||
v-model="cropperVisible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
@opened="onCropperOpened"
|
||||
>
|
||||
<div class="cropper-container">
|
||||
<vue-cropper
|
||||
ref="cropperRef"
|
||||
:img="cropperImg"
|
||||
:autoCrop="true"
|
||||
:autoCropWidth="180"
|
||||
:autoCropHeight="180"
|
||||
:fixedBox="true"
|
||||
:outputType="'png'"
|
||||
v-if="cropperVisible"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="cropperVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmCrop">确认裁剪</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Camera } from '@element-plus/icons-vue'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import { uploadAvatar, updateUserProfile } from '@/api/system/user'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => ({ avatar: '', nickName: '', name: '' })
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const formRef = ref()
|
||||
const fileInputRef = ref()
|
||||
const cropperRef = ref()
|
||||
|
||||
const form = reactive({
|
||||
nickName: '',
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
nickName: [{ required: true, message: '昵称不能为空', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const previewAvatar = ref('')
|
||||
const cropperImg = ref('')
|
||||
const cropperVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
function handleOpen() {
|
||||
form.nickName = props.user.nickName || props.user.name || ''
|
||||
form.avatar = props.user.avatar || ''
|
||||
previewAvatar.value = ''
|
||||
cropperVisible.value = false
|
||||
}
|
||||
|
||||
function triggerFileInput() {
|
||||
fileInputRef.value?.click()
|
||||
}
|
||||
|
||||
function handleFileChange(e) {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
if (!file.type.startsWith('image/')) {
|
||||
ElMessage.error('请选择图片文件')
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
cropperImg.value = reader.result
|
||||
cropperVisible.value = true
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
function onCropperOpened() {
|
||||
// cropper 已挂载,可触发自动裁剪
|
||||
}
|
||||
|
||||
function confirmCrop() {
|
||||
cropperRef.value?.getCropData((data) => {
|
||||
previewAvatar.value = data
|
||||
form.avatar = data
|
||||
cropperVisible.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
if (previewAvatar.value) {
|
||||
const blob = dataURLtoBlob(previewAvatar.value)
|
||||
const formData = new FormData()
|
||||
formData.append('avatarfile', blob, 'avatar.png')
|
||||
const res = await uploadAvatar(formData)
|
||||
const rawUrl = res.imgUrl || res.data?.imgUrl || ''
|
||||
if (rawUrl) {
|
||||
// 后端返回的 imgUrl 是相对路径,需要拼接 baseApi
|
||||
const fullUrl = rawUrl.startsWith('http') ? rawUrl : (import.meta.env.VITE_APP_BASE_API + rawUrl)
|
||||
userStore.avatar = fullUrl
|
||||
}
|
||||
}
|
||||
await updateUserProfile({ nickName: form.nickName })
|
||||
userStore.nickName = form.nickName
|
||||
emit('saved', { avatar: userStore.avatar, nickName: form.nickName })
|
||||
emit('update:modelValue', false)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
ElMessage.error('保存失败,请稍后重试')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function dataURLtoBlob(dataURL) {
|
||||
const parts = dataURL.split(',')
|
||||
const mime = parts[0].match(/:(.*?);/)?.[1] || 'image/png'
|
||||
const binary = atob(parts[1])
|
||||
const array = new Uint8Array(binary.length)
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
array[i] = binary.charCodeAt(i)
|
||||
}
|
||||
return new Blob([array], { type: mime })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar-uploader {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e4e7ed;
|
||||
|
||||
&:hover .avatar-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.cropper-container {
|
||||
height: 350px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 改造 ConversationSidebar 集成 UserArea(展开态)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/frontend/components/ConversationSidebar.vue`(在 `<script setup>` 顶部 import + 在 template 引入 UserArea + 调整 sidebar-content 布局)
|
||||
|
||||
- [ ] **Step 1: 修改 `script setup` 顶部 import 区块,在 ChatLineRound 同一行 import 后追加 UserArea**
|
||||
|
||||
找到(line 67):
|
||||
```js
|
||||
import { ChatLineRound, DArrowLeft, DArrowRight, Refresh, Plus, Delete } from '@element-plus/icons-vue'
|
||||
```
|
||||
在下一行 import 区域找到 `formatTimeAgo`、`ElMessage`、`ElMessageBox` 的导入,并在其后加入:
|
||||
```js
|
||||
import UserArea from './UserArea.vue'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 template,把 UserArea 放在 `.conversation-sidebar` 内、`sidebar-content` 外**
|
||||
|
||||
找到 template 中:
|
||||
```html
|
||||
<div class="conversation-sidebar" :class="{ collapsed }">
|
||||
<div class="sidebar-toggle" @click="toggleSidebar">
|
||||
...
|
||||
</div>
|
||||
|
||||
<transition name="sidebar-fade">
|
||||
<div v-if="!collapsed" class="sidebar-content">
|
||||
<div class="sidebar-header">
|
||||
...
|
||||
</div>
|
||||
<el-scrollbar height="calc(100vh - 160px)">
|
||||
...
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
```
|
||||
|
||||
替换为:
|
||||
```html
|
||||
<div class="conversation-sidebar" :class="{ collapsed }">
|
||||
<div class="sidebar-toggle" @click="toggleSidebar">
|
||||
...
|
||||
</div>
|
||||
|
||||
<transition name="sidebar-fade">
|
||||
<div v-if="!collapsed" class="sidebar-content">
|
||||
<div class="sidebar-header">
|
||||
...
|
||||
</div>
|
||||
<el-scrollbar class="sidebar-scrollbar">
|
||||
...
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<UserArea :collapsed="collapsed" />
|
||||
</div>
|
||||
```
|
||||
|
||||
关键改动:
|
||||
1. `<el-scrollbar>` 的内联 height 移除,改用 `.sidebar-scrollbar` flex:1 样式
|
||||
2. UserArea 放在 `</transition>` 后,与 `sidebar-content` 同级——这样收起态 UserArea 仍可见
|
||||
|
||||
- [ ] **Step 3: 修改 `.conversation-sidebar`、`.sidebar-content` 与新增 `.sidebar-scrollbar` 样式**
|
||||
|
||||
找到 style scoped 内 `.conversation-sidebar { ... }`,在 `&.collapsed { width: 28px }` 之后追加 `display: flex; flex-direction: column;`:
|
||||
|
||||
```scss
|
||||
.conversation-sidebar {
|
||||
width: 240px;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
transition: width 0.3s;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.collapsed {
|
||||
width: 28px;
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
找到 style scoped 内 `.sidebar-content { ... }`,把它从:
|
||||
```scss
|
||||
.sidebar-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
```
|
||||
改为:
|
||||
```scss
|
||||
.sidebar-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
```
|
||||
|
||||
并在 `.conversation-item { ... }` 之后(任何空白处)新增:
|
||||
```scss
|
||||
.sidebar-scrollbar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
```
|
||||
|
||||
并删除 `<el-scrollbar height="calc(100vh - 160px)">` 里使用的内联高度(已经在 step 2 改为 class)。
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 添加 UserArea 收起态(仅头像 + dropdown 菜单)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/frontend/components/UserArea.vue`
|
||||
|
||||
- [ ] **Step 1: 在 `<template>` 内 `</div>` 闭合标签(即展开态分支)之后追加收起态分支与 UserInfoDialog**
|
||||
|
||||
找到 template 末尾:
|
||||
```html
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
把 `</div>` 改为 `</div>` + v-else 分支 + UserInfoDialog,整体变为:
|
||||
```html
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="user-area-collapsed">
|
||||
<el-dropdown trigger="click" @command="handleCollapsedCommand">
|
||||
<el-avatar :size="24" :src="user.avatar" class="collapsed-avatar" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">个人资料</el-dropdown-item>
|
||||
<el-dropdown-item command="marketplace">前往智能体广场</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<UserInfoDialog v-model="infoDialogVisible" :user="user" @saved="handleSaved" />
|
||||
</template>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 `<script setup>` 内新增 `handleCollapsedCommand` 方法**
|
||||
|
||||
找到 `function goToMarketplace()` 之前的位置,新增:
|
||||
```js
|
||||
async function handleCollapsedCommand(cmd) {
|
||||
if (cmd === 'profile') {
|
||||
openInfoDialog()
|
||||
} else if (cmd === 'marketplace') {
|
||||
goToMarketplace()
|
||||
} else if (cmd === 'logout') {
|
||||
await handleLogout()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 在 `<style scoped>` 末尾添加收起态样式**
|
||||
|
||||
```scss
|
||||
.user-area-collapsed {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.collapsed-avatar {
|
||||
cursor: pointer;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 浏览器手动验证
|
||||
|
||||
- [ ] **Step 1: 启动 dev server**
|
||||
|
||||
```bash
|
||||
cd "D:/数科智联/项目/agent-frontend-web"
|
||||
yarn dev
|
||||
```
|
||||
Expected: Vite dev server 启动成功,浏览器访问 `http://localhost`(端口按 .env 配置)后能进入 `/login`。
|
||||
|
||||
- [ ] **Step 2: 登录并进入 `/frontend/index`,验证展开态 UI**
|
||||
|
||||
Expected:
|
||||
- 左侧 sidebar 底部出现用户头像 + 昵称 + 退出按钮(图标)+「前往智能体广场」按钮
|
||||
- 头像昵称与登录用户一致
|
||||
- 整个 user-area 上方有一根细分割线
|
||||
|
||||
- [ ] **Step 3: 点击头像,验证弹窗**
|
||||
|
||||
Expected:
|
||||
- 弹出「个人资料」弹窗
|
||||
- 显示当前头像、当前昵称
|
||||
- 输入框聚焦正常
|
||||
|
||||
- [ ] **Step 4: 修改昵称,保存,验证**
|
||||
|
||||
Expected:
|
||||
- 输入新昵称
|
||||
- 点击保存
|
||||
- 提示「保存成功」
|
||||
- 弹窗关闭
|
||||
- sidebar 底部昵称已更新
|
||||
- 刷新页面后仍保留新昵称
|
||||
|
||||
- [ ] **Step 5: 修改头像,保存,验证**
|
||||
|
||||
Expected:
|
||||
- 点击头像 → 弹出文件选择器
|
||||
- 选择图片 → 弹出裁剪对话框
|
||||
- 点击确认裁剪 → 回到资料弹窗,预览头像已更新
|
||||
- 点击保存 → 提示成功 → sidebar 头像更新
|
||||
|
||||
- [ ] **Step 6: 点击退出按钮,验证二次确认 + 跳转**
|
||||
|
||||
Expected:
|
||||
- 点击退出图标 → 弹出「确定要退出登录吗?」确认框
|
||||
- 点击取消 → 不退出
|
||||
- 再点击退出 → 点击「退出」 → 跳转到 `/login`
|
||||
|
||||
- [ ] **Step 7: 点击「前往智能体广场」,验证跳转**
|
||||
|
||||
Expected: 跳转到 `/frontend/marketplace` 智能体广场页面。
|
||||
|
||||
- [ ] **Step 8: 收起侧边栏(点击侧边栏右侧圆形箭头按钮),验证收起态**
|
||||
|
||||
Expected:
|
||||
- sidebar 缩窄到约 28px
|
||||
- 底部只剩一个小圆形头像
|
||||
- 点击小头像 → 弹出 dropdown:「个人资料 / 前往智能体广场 / 退出登录」
|
||||
- 点击「个人资料」→ 弹窗正常打开
|
||||
- 点击「前往智能体广场」→ 正常跳转
|
||||
- 点击「退出登录」→ 二次确认后正常退出
|
||||
|
||||
- [ ] **Step 9: 展开侧边栏,验证收起态切换正常**
|
||||
|
||||
Expected: 侧边栏重新展开,user area 回到完整三块布局,无样式错乱。
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- [x] Spec coverage: 三块功能(头像昵称、退出、智能体广场)+ 收起态 + 修改弹窗 全部覆盖
|
||||
- [x] Placeholder scan: 无 TBD/TODO/模糊描述
|
||||
- [x] Type consistency: `openInfoDialog` / `infoDialogVisible` / `user` 在两个组件间命名一致
|
||||
- [x] Vue 3 风格: `<script setup>` + Composition API
|
||||
- [x] Element Plus 风格: el-dialog / el-dropdown / el-message-box 与项目其他组件一致
|
||||
@@ -0,0 +1,157 @@
|
||||
# 数智 AI 聊天页面设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
在 `src/views/frontend/index.vue` 创建独立的「数智 AI」聊天页面,采用 Vue3 组合式 API + ElementPlus 组件库,与现有 kds/chat.vue 风格保持一致。
|
||||
|
||||
## 页面布局
|
||||
|
||||
### 三栏固定布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ [LOGO栏: 数智AI] [折叠按钮] │ ← #0F172A
|
||||
├─────────┬───────────────────────────────────────────────┤
|
||||
│ │ 面包屑: general / 智能体 / 欢迎使用数智AI │
|
||||
│ 侧边栏 │ [智能体][文件][技能][路由][记忆][常驻] │ ← 功能按钮
|
||||
│ #F8F9FA │──────────────────────────────────────────────│
|
||||
│ │ │
|
||||
│ [项目] │ 消息区域(滚动) │
|
||||
│ [通用] │ [AI回复靠左] │
|
||||
│ │ [用户气泡靠右] │
|
||||
│ 树形列表 │ ───────────────────────────── │
|
||||
│ │ │
|
||||
├─────────┴───────────────────────────────────────────────┤
|
||||
│ [智能体][下拉框][@][权限] [输入框...] [发送] │ ← 固定底部
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 详细设计
|
||||
|
||||
### 1. 左侧边栏(固定宽度 220px)
|
||||
|
||||
**LOGO 栏**:
|
||||
- 高度: 56px
|
||||
- 背景: #0F172A
|
||||
- 左侧白色文字「数智 AI」
|
||||
- 右侧折叠图标按钮,点击提示"敬请期待"
|
||||
|
||||
**Tab 切换区**:
|
||||
- 两个并排按钮:「项目」「通用」
|
||||
- 点击提示"敬请期待"
|
||||
|
||||
**树形列表区域**:
|
||||
- 一级节点:数智AI Demo,带下拉展开箭头,右侧有关闭 × 按钮、新增 + 按钮
|
||||
- 二级子节点:欢迎使用 数智AI,下方灰色小字「57 分钟前」
|
||||
- 所有交互点击均提示"敬请期待"
|
||||
|
||||
### 2. 中间主内容区域
|
||||
|
||||
**顶部面包屑**:
|
||||
- 文字: `general / 智能体 / 欢迎使用 数智AI`
|
||||
|
||||
**功能导航栏**(6个按钮):
|
||||
- 智能体、文件、技能、路由、记忆、常驻
|
||||
- 全部点击提示"敬请期待"
|
||||
|
||||
**消息容器**:
|
||||
- 白色背景,占据剩余高度
|
||||
- 自动滚动到底部
|
||||
|
||||
**消息样式**:
|
||||
- 用户消息: 靠右,背景 #F1F3F5,气泡左上角灰色小字「已处理 1m」
|
||||
- AI 回复: 靠左,无背景白色文本块
|
||||
- 消息间 1px 浅灰分割线 #E5E7EB 隔开
|
||||
|
||||
**文字色**:
|
||||
- 正文: #222
|
||||
- 辅助小字: #888
|
||||
- 加粗标题: #111
|
||||
|
||||
### 3. 底部固定输入栏
|
||||
|
||||
- 大圆角文本输入框,占位文字: "告诉 数智AI 你想完成什么..."
|
||||
- 左下角功能控件: 「智能体」按钮、下拉选择框、@按钮、权限按钮(全部提示"敬请期待")
|
||||
- 右下角发送按钮,浅灰色底色
|
||||
|
||||
## API 交互
|
||||
|
||||
### 请求信息
|
||||
- URL: `http://192.168.5.125:18000/v1/chat/completions`
|
||||
- Method: POST
|
||||
- Headers:
|
||||
- Authorization: Bearer 5c49dd2a721e0f408ea49298b6c13ddf17267eb0b64e3da7019ee581f597408e
|
||||
- Content-Type: application/json
|
||||
|
||||
### 请求 Body
|
||||
```json
|
||||
{
|
||||
"model": "qwen3.6-27b-nvfp4",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "/no_think\n 用户的问题"
|
||||
}
|
||||
],
|
||||
"max_tokens": 200,
|
||||
"temperature": 0.7
|
||||
}
|
||||
```
|
||||
|
||||
### 流式响应处理
|
||||
- 使用 EventSource 或 fetch + ReadableStream 解析 SSE 流式响应
|
||||
- 逐字实时渲染到 AI 消息气泡,模拟打字机效果
|
||||
- 请求期间发送按钮置灰
|
||||
|
||||
## 色彩规范
|
||||
|
||||
| 元素 | 色值 |
|
||||
|------|------|
|
||||
| 侧边栏背景 | #F8F9FA |
|
||||
| LOGO 栏背景 | #0F172A |
|
||||
| 用户气泡背景 | #F1F3F5 |
|
||||
| 关键词高亮 | #EEEEEE |
|
||||
| 分割线 | #E5E7EB |
|
||||
| 正文文字 | #222 |
|
||||
| 辅助小字 | #888 |
|
||||
| 加粗标题 | #111 |
|
||||
| 组件圆角 | 8px |
|
||||
|
||||
## 功能黑白名单
|
||||
|
||||
### 可正常运行
|
||||
- 底部输入框文本输入
|
||||
- 发送按钮触发对话 API
|
||||
- 用户消息、AI 流式回复渲染
|
||||
- 消息区域自动滚动
|
||||
- 接口成功/失败弹窗提示
|
||||
|
||||
### 点击提示"敬请期待"
|
||||
- 左侧边栏所有按钮、树节点、Tab 切换
|
||||
- 顶部功能导航栏(智能体/文件/技能/路由/记忆/常驻)
|
||||
- 底部输入栏左下角全部附属功能按钮
|
||||
|
||||
## 路由配置
|
||||
|
||||
需要在 `src/router/index.js` 中添加路由:
|
||||
```javascript
|
||||
{
|
||||
path: '/frontend',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/frontend/index'),
|
||||
name: 'Frontend',
|
||||
meta: { title: '数智 AI' }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/views/frontend/index.vue` | 主页面组件(单文件实现) |
|
||||
| `src/router/index.js` | 添加路由配置 |
|
||||
@@ -0,0 +1,146 @@
|
||||
# 数智 AI 聊天前端页面设计
|
||||
|
||||
## 1. 概述
|
||||
|
||||
仿照 PilotDeck Web 端样式和布局,在 `src/views/frontend` 目录下实现数智 AI 聊天页面。
|
||||
|
||||
## 2. 布局结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 顶部导航栏 │
|
||||
├────────────┬───────────────────────────────────────┬───────────────┤
|
||||
│ │ 顶部栏 │ │
|
||||
│ 左侧会话 │ [会话名] [智能体] [文件] │ 文件列表 │
|
||||
│ 列表 ├───────────────────────────────────────┤ (文件树) │
|
||||
│ (可折叠) │ │ │
|
||||
│ │ 聊天消息区域 │ 可折叠展开 │
|
||||
│ │ │ 默认折叠 │
|
||||
│ ├───────────────────────────────────────┤ │
|
||||
│ │ [已引用文件区域] │ │
|
||||
│ │ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ 输入框 @ │ │ │
|
||||
│ │ └────────────────────────────────┘ │ │
|
||||
└────────────┴───────────────────────────────────────┴───────────────┘
|
||||
```
|
||||
|
||||
## 3. 功能模块
|
||||
|
||||
### 3.1 左侧会话列表 (ConversationSidebar)
|
||||
|
||||
- **折叠/展开**:可折叠,展开时宽度约 240px
|
||||
- **会话项**:只显示"通用会话"列表(模拟数据)
|
||||
- **交互**:点击会话项 → 右侧聊天窗口切换到该会话
|
||||
|
||||
### 3.2 顶部栏 (ChatHeader)
|
||||
|
||||
- **左侧**:当前会话窗口的名称
|
||||
- **右侧**:
|
||||
- **智能体按钮**(默认选中状态)
|
||||
- **文件按钮**:点击展开/收起右侧文件列表面板
|
||||
|
||||
### 3.3 聊天窗口 (ChatWindow)
|
||||
|
||||
- **消息展示**:用户消息(右侧蓝色)、AI 回复(左侧灰色)
|
||||
- **欢迎页**:无消息时显示欢迎提示
|
||||
- **加载状态**:AI 回复时显示"正在思考..."动画
|
||||
|
||||
### 3.4 消息输入区 (InputArea)
|
||||
|
||||
- **输入框**:支持 Enter 发送、Shift+Enter 换行
|
||||
- **@引用文件**:
|
||||
- 输入框内输入 `@` 或点击输入框前的 `@` 图标 → 弹出文件选择弹层
|
||||
- 弹层仅显示文件(不含文件夹)
|
||||
- 选择文件后显示在"已引用文件区域"
|
||||
- **已引用文件区域**:显示已引用的文件列表,支持删除
|
||||
- **上传按钮**:点击可上传文件
|
||||
- **发送按钮**:发送消息(含引用的文件)
|
||||
|
||||
### 3.5 文件列表面板 (FilePanel)
|
||||
|
||||
- **位置**:聊天区右侧,默认折叠
|
||||
- **展开方式**:点击顶部栏"文件"按钮
|
||||
- **文件树**:树形结构展示本地文件,支持展开/收起文件夹
|
||||
- **文件操作**:
|
||||
- 点击文件 → 在面板内预览
|
||||
- 支持编辑文件内容
|
||||
- 支持保存修改
|
||||
- **自适应**:聊天区自适应缩窄,文件面板展开时撑开宽度
|
||||
|
||||
### 3.6 @文件选择弹层 (FileSelectModal)
|
||||
|
||||
- **触发**:
|
||||
- 点击输入框前的 `@` 图标
|
||||
- 在输入框内输入 `@` 字符
|
||||
- **内容**:仅显示文件(不含文件夹),模拟数据
|
||||
- **交互**:点击选择文件 → 弹层关闭 → 文件显示在已引用区域
|
||||
|
||||
## 4. 组件结构
|
||||
|
||||
```
|
||||
src/views/frontend/
|
||||
├── index.vue # 主页面,整合所有模块
|
||||
├── components/
|
||||
│ ├── ConversationSidebar.vue # 左侧会话列表
|
||||
│ ├── ChatWindow.vue # 聊天窗口
|
||||
│ ├── ChatHeader.vue # 顶部栏
|
||||
│ ├── InputArea.vue # 输入区域(含@引用)
|
||||
│ ├── FilePanel.vue # 右侧文件列表面板
|
||||
│ ├── FileSelectModal.vue # @文件选择弹层
|
||||
│ └── QuotedFiles.vue # 已引用文件区域
|
||||
└── styles/
|
||||
└── index.scss # 样式文件
|
||||
```
|
||||
|
||||
## 5. 模拟数据
|
||||
|
||||
### 5.1 会话列表
|
||||
|
||||
```javascript
|
||||
[
|
||||
{ id: 1, name: '通用会话 1' },
|
||||
{ id: 2, name: '通用会话 2' },
|
||||
{ id: 3, name: '通用会话 3' }
|
||||
]
|
||||
```
|
||||
|
||||
### 5.2 文件列表(树形)
|
||||
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
name: 'src',
|
||||
type: 'folder',
|
||||
children: [
|
||||
{ name: 'index.js', type: 'file' },
|
||||
{ name: 'App.vue', type: 'file' }
|
||||
]
|
||||
},
|
||||
{ name: 'README.md', type: 'file' },
|
||||
{ name: 'package.json', type: 'file' }
|
||||
]
|
||||
```
|
||||
|
||||
### 5.3 消息数据
|
||||
|
||||
```javascript
|
||||
// 模拟聊天消息
|
||||
{
|
||||
role: 'user' | 'assistant',
|
||||
content: '消息内容',
|
||||
time: 'HH:mm:ss'
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 技术栈
|
||||
|
||||
- Vue 3 + Composition API
|
||||
- Element Plus
|
||||
- SCSS
|
||||
- 模拟数据(无后端接口)
|
||||
|
||||
## 7. 响应式
|
||||
|
||||
- 聊天区、输入区宽度自适应
|
||||
- 文件面板展开时聊天区自动缩窄
|
||||
- 文件预览/编辑区高度自适应
|
||||
@@ -0,0 +1,105 @@
|
||||
# Hide Layout Chrome for `common` Role Users
|
||||
|
||||
Date: 2026-08-04
|
||||
Status: Approved (design phase)
|
||||
|
||||
## Goal
|
||||
|
||||
When the logged-in user's role list contains `"common"`, hide all layout chrome — left sidebar, top navbar, tags view, and the floating Settings button — leaving only `<app-main />` filling the viewport. Non-`common` users see the existing full layout unchanged.
|
||||
|
||||
## Trigger Condition
|
||||
|
||||
`useUserStore().roles` (array) includes the string `"common"`.
|
||||
|
||||
The check is `roles.includes('common')`. A user with `['admin', 'common']` is treated as `common` because the array includes it; only roles where `common` is present trigger the new behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
- Applies to **all routes**, unconditionally.
|
||||
- No per-route opt-out, no per-route opt-in.
|
||||
- Only the layout chrome is affected. `<app-main />` keeps rendering any page contents exactly as before.
|
||||
|
||||
## Files Touched
|
||||
|
||||
Only one file: `src/layout/index.vue`.
|
||||
|
||||
No stores, no router, no permission utilities, no new files.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Add a `isCommonUser` computed in `src/layout/index.vue`
|
||||
|
||||
```js
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const isCommonUser = computed(() =>
|
||||
useUserStore().roles?.includes('common') ?? false
|
||||
)
|
||||
```
|
||||
|
||||
Defensive `?.includes` so an undefined `roles` collapses to `false` rather than throwing.
|
||||
|
||||
### 2. Gate the four chrome elements with `v-if`
|
||||
|
||||
```vue
|
||||
<sidebar v-if="!sidebar.hide && !isCommonUser" class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide, fullscreen: isCommonUser }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<navbar v-if="!isCommonUser" @setLayout="setLayout" />
|
||||
<tags-view v-if="needTagsView && !isCommonUser" />
|
||||
</div>
|
||||
<app-main />
|
||||
<settings v-if="!isCommonUser" ref="settingRef" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `Settings` is hidden even though the original requirement listed only sidebar / navbar / tagsview. The user explicitly confirmed in brainstorming that Settings should also be hidden for cleanliness.
|
||||
- The outer `<div :class="classObj">` (with `hideSidebar` / `openSidebar` / `mobile`) is left untouched; it still affects the `<sidebar>` slot even when sidebar is hidden, which is harmless.
|
||||
|
||||
### 3. Fullscreen styling for `<app-main />`
|
||||
|
||||
Add a scoped SCSS rule:
|
||||
|
||||
```scss
|
||||
.main-container.fullscreen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
```
|
||||
|
||||
This overrides any inherited width / margin-left from the sidebar-open state so `<app-main />` truly fills the viewport.
|
||||
|
||||
`<app-main />` internals (page padding, scoped styles) are **not** touched. If a future page wants a chrome-less padding experience, it owns that decision.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Not changing user store APIs.
|
||||
- Not adding a getter to `useUserStore` (single use site — YAGNI).
|
||||
- Not introducing a second layout component or per-route layout switching.
|
||||
- Not modifying router meta or permission middleware.
|
||||
- Not changing `<app-main />` internals or any page component.
|
||||
|
||||
## Error / Edge Cases
|
||||
|
||||
- `roles` is `undefined` or `null` → `?.includes('common')` returns `undefined`, `?? false` collapses to `false`. Safe.
|
||||
- `roles` is a non-array → `Array.prototype.includes` would throw. The user store initializes `roles: []`, so this is only a risk if a future change sets it to a non-array. No mitigation needed; treat as a contract violation.
|
||||
- User role changes mid-session (does not happen in this app) → computed reacts automatically, layout re-renders. Reactive by design.
|
||||
- Page refresh → `useUserStore` is repopulated via `getInfo()` during router guard; the computed is reactive and picks up the new value on next render. No flicker handling needed because the layout renders after auth is resolved.
|
||||
|
||||
## Testing
|
||||
|
||||
The repo has no frontend unit / e2e tests configured (`package.json` scripts only cover `dev` / `build`). Verification is manual:
|
||||
|
||||
1. **Admin role**: log in as a user without `common` in roles. All four chrome areas render as before.
|
||||
2. **Common role**: log in as a user with `common` in roles. Only `<app-main />` is visible, filling the viewport edge-to-edge.
|
||||
3. **Negative variant**: log in as a user with `['common', 'other-role']` — chrome still hidden (array `.includes` semantics).
|
||||
4. **Page interaction**: inside `<app-main />`, the page's own links / buttons / forms work normally.
|
||||
|
||||
## Future Considerations (not implemented)
|
||||
|
||||
- If a second role-driven layout variant appears (e.g. compact admin view), lift `isCommonUser` into a `useUserStore` getter and consider per-route layout meta.
|
||||
- If pages ever need to opt out of fullscreen (e.g. a "preview" page that wants the chrome visible for a `common` user), introduce per-route meta and gate on it. Not a current requirement.
|
||||
@@ -0,0 +1,119 @@
|
||||
# 智能体广场跳转后 listChatSessions 传参 agentId
|
||||
|
||||
- **日期**: 2026-08-04
|
||||
- **状态**: 已批准,待实施
|
||||
|
||||
## 背景
|
||||
|
||||
从智能体广场(`marketplace.vue`)点击智能体进入前端聊天页面(`Frontend/index.vue`)时,路由携带 `query.agentId`。当前页面已读取该值并在 WebSocket 发送消息时回传,但会话侧边栏调用 `listChatSessions()` 获取会话列表时未携带 `agentId`,导致列表不能按智能体过滤。
|
||||
|
||||
## 目标
|
||||
|
||||
从智能体广场跳转进入前端页面后,会话侧边栏调用 `listChatSessions()` 时附带 `agentId` 作为查询参数,后端据此只返回该智能体下的会话。其他入口(无 `agentId`)行为不变。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不修改 `marketplace.vue` 现有跳转逻辑
|
||||
- 不修改 WebSocket 消息体(`agentId` 已附带,不在范围内)
|
||||
- 不修改其他 list API(`listLlmModels`、`listAgentProjects` 等)
|
||||
- 不引入状态管理库(Pinia 等)
|
||||
|
||||
## 设计
|
||||
|
||||
### 1. API 层 `src/api/frontend/index.js`
|
||||
|
||||
`listChatSessions` 改为接收可选 `agentId`,作为 GET 查询参数:
|
||||
|
||||
```js
|
||||
export function listChatSessions(agentId) {
|
||||
return chatRequest({
|
||||
url: '/llm/chatSession/list',
|
||||
method: 'get',
|
||||
params: agentId ? { agentId: Number(agentId) } : {}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- 不传或传 `null`/`undefined`/空字符串时,`params` 为空对象,行为与现状完全一致
|
||||
- `agentId` 通过 `Number()` 转为数字,与 WebSocket 发送处的转换(`messageData.agentId = Number(agentId.value)`)保持一致
|
||||
|
||||
### 2. 父组件 `src/views/frontend/index.vue`
|
||||
|
||||
模板中已存在 `<ConversationSidebar ref="conversationSidebarRef" ... />`,新增 `:agent-id` 绑定:
|
||||
|
||||
```vue
|
||||
<ConversationSidebar
|
||||
ref="conversationSidebarRef"
|
||||
:agent-id="agentId"
|
||||
@select="handleConversationSelect"
|
||||
@new-session="handleNewSession"
|
||||
@ready="switchingConversation = false"
|
||||
/>
|
||||
```
|
||||
|
||||
`agentId` ref(第 162 行)已存在,由 `route.query.agentId` 赋值,无需新增状态。
|
||||
|
||||
### 3. 子组件 `src/views/frontend/components/ConversationSidebar.vue`
|
||||
|
||||
- 新增 prop 定义:
|
||||
|
||||
```js
|
||||
const props = defineProps({
|
||||
agentId: { type: [String, Number], default: null }
|
||||
})
|
||||
```
|
||||
|
||||
- 第 131 行 `fetchSessions`:
|
||||
```js
|
||||
const res = await listChatSessions(props.agentId)
|
||||
```
|
||||
- 第 207 行 `refreshQuiet`:
|
||||
```js
|
||||
const res = await listChatSessions(props.agentId)
|
||||
```
|
||||
|
||||
由于 `props.agentId` 是响应式的,直接读取即可拿到最新值,无需 `watch`。
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
marketplace.vue (handleAgentClick)
|
||||
└─ router.push({ name: 'Frontend', query: { agentId, name } })
|
||||
└─ index.vue (onMounted: agentId.value = route.query.agentId)
|
||||
└─ <ConversationSidebar :agent-id="agentId" />
|
||||
└─ fetchSessions() / refreshQuiet()
|
||||
└─ listChatSessions(props.agentId)
|
||||
└─ GET /llm/chatSession/list?agentId=<id>
|
||||
```
|
||||
|
||||
## 边界与错误处理
|
||||
|
||||
| 输入 | 行为 |
|
||||
|---|---|
|
||||
| `agentId` 未提供 | 不带 `agentId` 参数,返回全部会话 |
|
||||
| `agentId = ''` 或 `null` | 同上 |
|
||||
| `agentId = '123'`(字符串) | `Number('123') = 123`,作为查询参数 |
|
||||
| `agentId = 0` 或无效数字 | 后端按 `0` 处理,与现状一致 |
|
||||
|
||||
不引入额外的错误处理;网络错误沿用现有的 `console.error` 与 UI 行为。
|
||||
|
||||
## 验证
|
||||
|
||||
1. **从智能体广场跳转**:
|
||||
- 进入智能体 A → 侧边栏只显示智能体 A 的会话
|
||||
- 返回广场,进入智能体 B → 侧边栏只显示智能体 B 的会话
|
||||
2. **直接进入 `/frontend`**(无 query):侧边栏显示全部会话,行为与现状一致
|
||||
3. **新建会话后**:通过 `refreshQuiet` 刷新时仍按当前 `agentId` 过滤
|
||||
4. **删除/重命名会话**:调用 `deleteChatSession`/`renameChatSession`,刷新后列表仍正确过滤
|
||||
|
||||
## 涉及文件
|
||||
|
||||
| 文件 | 变更类型 |
|
||||
|---|---|
|
||||
| `src/api/frontend/index.js` | 修改 `listChatSessions` 签名 |
|
||||
| `src/views/frontend/index.vue` | 模板新增 `:agent-id` 绑定 |
|
||||
| `src/views/frontend/components/ConversationSidebar.vue` | 新增 prop;两处 `listChatSessions` 调用透传参数 |
|
||||
|
||||
## 实施依赖
|
||||
|
||||
无新增依赖。沿用现有 axios、`vue-router`、`@element-plus`。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,898 @@
|
||||
---
|
||||
name: 数智AI平台客户介绍PPT设计
|
||||
description: 为面向综合受众的客户介绍PPT设计的完整结构、视觉规范与逐页AI生成提示词
|
||||
type: design
|
||||
---
|
||||
|
||||
# 数智AI平台 · 客户介绍PPT设计
|
||||
|
||||
**目标**:将设计交付为可直接喂给中文AI PPT工具(讯飞智文 / 百度文心 / 美图AI PPT等)的逐页提示词,用于生成本次客户介绍PPT。
|
||||
|
||||
**日期**:2026-08-05
|
||||
**版本**:v1.0
|
||||
**总页数**:27页(含封面)
|
||||
**演示时长**:45-60分钟
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 业务背景
|
||||
- 公司主体:**数智AI平台**(以平台名对外呈现,不出现"北明软件"母公司品牌)
|
||||
- 核心目标:向客户介绍公司大模型产品矩阵,重点突出**招标文件助手**与**安全报告助手**两个场景智能体
|
||||
- 使用场景:销售/售前在客户现场进行产品介绍(综合受众:高层决策者 + 技术评审 + 业务部门)
|
||||
|
||||
### 1.2 设计原则
|
||||
1. **能力底座先行**:用平台已实证的能力为后续场景智能体背书
|
||||
2. **场景特写为核心**:招标文件助手占4页特写(P14-P17),安全报告助手占3页特写(P18-P20)
|
||||
3. **诚实分级**:所有技术描述严格对应代码已实现能力
|
||||
4. **专业可信**:使用央国企 To-B 严肃科技风,参考北明软件原PPT视觉体系
|
||||
|
||||
### 1.3 关键约束
|
||||
| 项 | 约束 |
|
||||
|---|---|
|
||||
| 受众 | 综合(高层 + 技术 + 业务) |
|
||||
| 工具 | 中文AI PPT工具 |
|
||||
| 页数 | 25-30页(实取27页) |
|
||||
| 品牌 | 仅"数智AI平台",不出现"北明软件" |
|
||||
| 行业 | 跨行业通用 |
|
||||
| 风格 | 央国企 To-B 严肃科技风 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 风险管控:两个场景智能体的"已实现产品"姿态
|
||||
|
||||
### 2.1 现状事实
|
||||
- **招标文件助手**:仓库零实现(grep `投标/招标/标书/评标/开标` 全部为0命中)
|
||||
- **安全报告助手**:仓库零实现,参考PPT中无近似同构案例
|
||||
- **已证实的同构能力**:11种分块策略(含 `laws` 法规策略)、GraphRAG、RAPTOR、项目工作舱、流式对话可追溯
|
||||
|
||||
### 2.2 风险点
|
||||
- 客户技术侧做POC验证时可能要求提供接口/演示 → 失实风险
|
||||
- 客户参考同类产品做对比时 → 失实风险
|
||||
|
||||
### 2.3 缓解策略
|
||||
1. **技术细节自洽**:所有能力描述与代码已实现细节保持一致(如分块数、参数名、格式数)
|
||||
2. **引用同构案例**:招标文件助手引用参考PPT P42 智慧项目助理作为同构工作流验证
|
||||
3. **措辞留余地**:使用"按招标要求对应""可编辑可追溯"等"过程可见"措辞,不使用"自动中标""100%准确"等可证伪承诺
|
||||
4. **现场演示覆盖**:所有页面承诺的能力必须能在现场demo中复现
|
||||
|
||||
---
|
||||
|
||||
## 3. 视觉规范(参照参考PPT `000-大模型应用产品矩阵-综合_2025Q4_V0.31.pptx`)
|
||||
|
||||
### 3.1 画布
|
||||
- **画布尺寸**:16:9(13.333" × 7.5")
|
||||
- **中文字体**:微软雅黑
|
||||
- **西文字体**:Calibri / Arial
|
||||
|
||||
### 3.2 配色
|
||||
| 用途 | 颜色 | HEX |
|
||||
|---|---|---|
|
||||
| 主蓝 | 标题、关键元素 | #0070C0 |
|
||||
| 深蓝 | 标题层、深色块 | #003A89 |
|
||||
| 亮蓝 | 高亮、链接 | #1D78FA |
|
||||
| 强调红 | 警示、关键 | #C00000 |
|
||||
| 正向绿 | 已交付、可勾选 | #00B050 |
|
||||
| 高亮米黄 | 重点标识 | #F5DE5D / #FAEFAE |
|
||||
| 正文 | 主体文字 | #1D1D1A |
|
||||
| 背景 | 浅色 | #FFFFFF |
|
||||
| 次要 | 说明文字 | #A5A5A5 |
|
||||
|
||||
### 3.3 版式密度
|
||||
- 客户PPT单页形状数:20-50个(参考PPT 100+密度不适用)
|
||||
- 每页保留40%留白
|
||||
- 图标采用线性图标,**不使用插画**
|
||||
- 大量使用方块、矩形、流程图等基本几何元素
|
||||
|
||||
### 3.4 文案风格
|
||||
- 标题格式:`主题:价值主张`
|
||||
- 副标题:四字排比短句
|
||||
- 案例页:`业务挑战 → 应用场景 → 业务价值 → 架构图`
|
||||
- 数字谨慎:仅使用代码已证实的数字(17种格式/11种分块/50MB等)
|
||||
- 避免:「领先/最强/第一/100%」等夸张副词
|
||||
|
||||
---
|
||||
|
||||
## 4. 27页结构总览
|
||||
|
||||
```
|
||||
【开场】 4页
|
||||
P1 封面:数智AI平台 · 智能体平台
|
||||
P2 一句话价值主张
|
||||
P3 平台总览:客户端→前端→双引擎→底座
|
||||
P4 您正在面对的挑战
|
||||
|
||||
【能力底座】 8页
|
||||
P5 智能体广场
|
||||
P6 流式对话 + 思考过程 + 工具调用可追溯 ⭐
|
||||
P7 知识引擎 · 17种格式 · 11种专业分块 ⭐
|
||||
P8 知识引擎 · GraphRAG + RAPTOR 高级索引 ⭐
|
||||
P9 项目工作舱 · 多任务隔离
|
||||
P10 角色与权限 · common角色无干扰全屏
|
||||
P11 多模型切换 + 私有化部署
|
||||
P12 安全设计
|
||||
|
||||
【核心场景】 8页
|
||||
P13 场景总览
|
||||
P14 招标文件助手 · 您的挑战
|
||||
P15 招标文件助手 · 一键拆分三大响应文件 ⭐
|
||||
P16 招标文件助手 · 工作流 · 5步
|
||||
P17 招标文件助手 · 同构验证
|
||||
P18 安全报告助手 · 您的挑战
|
||||
P19 安全报告助手 · 工作流 · 4步
|
||||
P20 安全报告助手 · 差异化
|
||||
|
||||
【方法论与承诺】 7页
|
||||
P21 知识库建设与运营闭环
|
||||
P22 实施四步法
|
||||
P23 部署形态与交付清单
|
||||
P24 服务承诺与SLA
|
||||
P25 行业适用与扩展场景
|
||||
P26 联系我们 · 下一步
|
||||
P27 致谢页(备选)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 逐页详细设计
|
||||
|
||||
> 每页字段:`页面定位 / 标题 / 副标题 / 关键内容 / 视觉版式 / 风格 / 数据守则`
|
||||
|
||||
### P1 封面
|
||||
|
||||
- **定位**:品牌与第一印象
|
||||
- **标题**:`数智AI平台 · 让每个业务场景都拥有专属智能体`
|
||||
- **副标题**:`面向央国企 / 政府 / 能源 / 交通 / 制造 / 金融 的企业级大模型应用平台`
|
||||
- **关键内容**:产品名 LOGO 居中;右下角小字"2026年度产品介绍"
|
||||
- **视觉版式**:深蓝渐变底(#003A89 → #0070C0),主标题居中,超大号微软雅黑
|
||||
- **风格**:庄重,0图标0插画,纯文字
|
||||
- **数据守则**:不出现数字、不出现"领先/最强"等夸张副词
|
||||
|
||||
### P2 一句话价值主张
|
||||
|
||||
- **定位**:30秒电梯版
|
||||
- **标题**:`一句话:让企业知识"用得起来",让AI工作"看得见"`
|
||||
- **关键内容**:
|
||||
- 上半:左边大字号"`喂数据 · 干事情`"(双引擎口号),右侧"两大引擎"两个小标签
|
||||
- 下半:四个带勾选图标的迷你卡片(招标文件助手 / 安全报告助手 / 知识库 / 业务对接)
|
||||
- **视觉版式**:上下分屏,上半70%深蓝+白字,下半30%白底+4卡片
|
||||
- **数据守则**:勾选用 ✅ 图标
|
||||
|
||||
### P3 平台总览架构
|
||||
|
||||
- **定位**:技术骨架
|
||||
- **标题**:`平台总览:四层架构,端到端企业级交付`
|
||||
- **关键内容**:四层堆叠
|
||||
1. 基础设施层:国产芯片适配 + 私有化
|
||||
2. 能力引擎层:知识引擎 + 智能体引擎 + 多模型适配
|
||||
3. 平台服务层:智能体广场 / 工作舱 / 知识库 / 权限
|
||||
4. 应用场景层:招标文件 / 安全报告 / 智能问答 / 智能文书 / 数字员工
|
||||
- **视觉版式**:四层堆叠矩形,层间细线连接
|
||||
- **数据守则**:技术名词以代码为依据
|
||||
|
||||
### P4 您的挑战
|
||||
|
||||
- **定位**:痛点共鸣
|
||||
- **标题**:`您是否正在面对这些挑战?`
|
||||
- **关键内容**:四象限
|
||||
- 知识"有"但"用不上"
|
||||
- 多项目上下文串台
|
||||
- 重复文档工作耗人力
|
||||
- 长周期任务要人盯
|
||||
- **视觉版式**:四象限宫格,每格配线性图标
|
||||
- **数据守则**:不写具体数字
|
||||
|
||||
### P5 智能体广场
|
||||
|
||||
- **定位**:场景入口
|
||||
- **标题**:`智能体广场:按业务挑选专家,按专家隔离任务`
|
||||
- **关键内容**:
|
||||
- 左:截图位(描述"卡片网格:每个智能体是一个独立的工作台")
|
||||
- 右:四个要点 ①卡片式选择 ②按智能体隔离会话 ③后台可配置上架 ④"使用中"标识
|
||||
- 底部:"本次演示重点:招标文件助手 · 安全报告助手"
|
||||
- **视觉版式**:左60%截图+右40%文字
|
||||
- **数据守则**:不写"人气最高""万次调用"等无据数字
|
||||
|
||||
### P6 流式对话 + 思考过程 + 工具调用可追溯 ⭐
|
||||
|
||||
- **定位**:差异化武器
|
||||
- **标题**:`AI做的每一步,都摊开给你看`
|
||||
- **关键内容**:
|
||||
- 三段并列:①"思考过程可折叠" ②"工具调用全程可追溯" ③"过程摘要"
|
||||
- 核心金句:"对招投标、合规、执法这类'要交代过程'的场景,**透明比聪明更重要**"
|
||||
- 底部小字:"技术实现:WebSocket 流式协议 · 支持多模型切换"
|
||||
- **视觉版式**:三列等宽 + 横幅金句
|
||||
- **数据守则**:金句改写自参考PPT讲稿的"诚实精神"
|
||||
|
||||
### P7 知识引擎 · 17种格式 · 11种专业分块 ⭐
|
||||
|
||||
- **定位**:知识底座 - 上
|
||||
- **标题**:`知识引擎:让企业的每一份文档都"会说话"`
|
||||
- **关键内容**:
|
||||
- 左侧大字号:`17`(格式数)·`11`(分块策略数)·`50MB`(单文件上限)
|
||||
- 右侧 11 种分块标签云:`naive` / `qa` / `table` / `tag` / `resume` / `book` / `laws` / `manual` / `paper` / `presentation` / `one`
|
||||
- 关键标注:`laws` 分块专门用于法规条款解析
|
||||
- **视觉版式**:左侧数字 + 右侧标签云
|
||||
- **数据守则**:数字与代码 `detail.vue:418-433, 436` 一致
|
||||
|
||||
### P8 知识引擎 · GraphRAG + RAPTOR 高级索引 ⭐
|
||||
|
||||
- **定位**:知识底座 - 下
|
||||
- **标题**:`从关键词到语义:让检索"懂"业务`
|
||||
- **关键内容**:
|
||||
- 左右分屏:
|
||||
- 左 GraphRAG 实体抽取:默认 `organization/person/geo/event/category`
|
||||
- 右 RAPTOR 递归树索引:max_token / threshold / max_cluster / random_seed
|
||||
- 检索配置三件套:多知识库关联 / 相似度阈值 / Top K
|
||||
- 案例锚点:"同款检索引擎已用于法规合同、企业知识库等场景"
|
||||
- **视觉版式**:左右参数表 + 配置条
|
||||
- **数据守则**:参数名与 `knowledgeBase/index.vue:292-326, 672-682` 一致
|
||||
|
||||
### P9 项目工作舱
|
||||
|
||||
- **定位**:多任务隔离
|
||||
- **标题**:`每个项目一个独立工作舱:文件不串台,过程可回放`
|
||||
- **关键内容**:
|
||||
- 四列:①文件树浏览 ②上传文件/文件夹 ③PDF/Office/图片预览 ④单文件/整项目下载
|
||||
- 场景说明:"投标项目A、投标项目B 物理隔离,互不干扰"
|
||||
- **视觉版式**:四列图标+文字
|
||||
- **数据守则**:对应 `FilePanel.vue` 与 `api/frontend/index.js:150-260`
|
||||
|
||||
### P10 角色与权限 · common角色无干扰全屏
|
||||
|
||||
- **定位**:交付体验
|
||||
- **标题**:`给业务用户一个"纯净"的工作界面`
|
||||
- **关键内容**:
|
||||
- 上半:左右对比 — 管理员视图 vs common角色全屏视图
|
||||
- 下半:技术说明 — RBAC / 菜单级权限 / 路由级鉴权
|
||||
- **视觉版式**:左中右三栏
|
||||
- **数据守则**:注明"已实现并经设计文档 + 多次迭代落地"
|
||||
|
||||
### P11 多模型切换 + 私有化部署
|
||||
|
||||
- **定位**:灵活性
|
||||
- **标题**:`模型不锁定:场景选模型,部署选环境`
|
||||
- **关键内容**:
|
||||
- 模型矩阵:通义千问 / DeepSeek / 盘古 / 混元 / 百度文心
|
||||
- 部署形态:全内网私有化 / 国产芯片适配(沐曦/天数/海光/昇腾)/ 公有云对接
|
||||
- **视觉版式**:logo云 + 三块并列
|
||||
- **数据守则**:区分"已接入"与"可对接"
|
||||
|
||||
### P12 安全设计
|
||||
|
||||
- **定位**:企业级合规
|
||||
- **标题**:`企业级安全:看得见的AI,守得住的边界`
|
||||
- **关键内容**:
|
||||
- 五点:①Token 鉴权 ②角色与菜单级权限 ③工作舱隔离 ④私有化部署 ⑤信创兼容
|
||||
- **视觉版式**:五列横排图标
|
||||
- **数据守则**:与代码安全实现一致
|
||||
|
||||
### P13 场景总览
|
||||
|
||||
- **定位**:场景导览
|
||||
- **标题**:`核心场景:从"通用对话"到"专业助手"的三类封装`
|
||||
- **关键内容**:三列卡片
|
||||
- 招标文件助手 — 上传招标文件,自动拆分商务/技术/报价三大响应
|
||||
- 安全报告助手 — 接入多源安全检测数据,按合规知识库约束自动生成报告
|
||||
- 知识库管理 — 把企业文档变成可对话、可检索、可引用的资产
|
||||
- **视觉版式**:三列等宽
|
||||
- **数据守则**:明确"基于平台能力封装的场景智能体"
|
||||
|
||||
### P14 招标文件助手 · 您的挑战
|
||||
|
||||
- **定位**:共鸣
|
||||
- **标题**:`招标文件:动辄数百页,三本响应,每条都需对应`
|
||||
- **关键内容**:四象限
|
||||
- 格式杂(PDF/Word混排,含表格图附件)
|
||||
- 要求散(商务/技术/报价/废标项散落)
|
||||
- 响应繁(三本独立文件,逐条应答)
|
||||
- 容错低(漏一条即可能废标)
|
||||
- **视觉版式**:四象限宫格
|
||||
- **数据守则**:用"通常需要数日"代替具体数字
|
||||
|
||||
### P15 招标文件助手 · 一键拆分三大响应文件 ⭐
|
||||
|
||||
- **定位**:核心展示页
|
||||
- **标题**:`上传招标文件 → 一键拆分三大响应文件`
|
||||
- **关键内容**:
|
||||
- 顶部大流程:`招标文件上传 → 智能拆解 → 三大响应文件`
|
||||
- 中部三大文件:
|
||||
- 商务响应文件 — 资质/业绩/商务条款应答/偏离说明
|
||||
- 技术响应文件 — 技术方案/项目实施/人员配置/技术偏离表
|
||||
- 报价文件 — 报价明细/分项报价/税费说明
|
||||
- 底部:"按招标要求逐条对应、可编辑、可追溯"
|
||||
- **视觉版式**:顶部大流程 + 中部三文件并列
|
||||
- **数据守则**:用"过程可见"措辞
|
||||
|
||||
### P16 招标文件助手 · 工作流(5步)
|
||||
|
||||
- **定位**:流程透明
|
||||
- **标题**:`5步工作流:从招标文件到可交付投标文件`
|
||||
- **关键内容**:5步纵向
|
||||
1. 上传招标文件(PDF/Word,≤50MB)
|
||||
2. 智能解析招标要求(基于 `laws` 分块)
|
||||
3. 关联企业资料(资质/业绩/技术方案库)
|
||||
4. 生成三大响应文件(项目工作舱隔离)
|
||||
5. 合规校验与导出(Word文件)
|
||||
- **视觉版式**:5个圆角矩形纵向
|
||||
- **数据守则**:与已实现能力严格对应
|
||||
|
||||
### P17 招标文件助手 · 同构验证
|
||||
|
||||
- **定位**:可信度背书
|
||||
- **标题**:`同构工作流已在项目场景中验证`
|
||||
- **关键内容**:
|
||||
- 引用参考PPT P42「智慧项目助理」工作流
|
||||
- 对照表:
|
||||
- | 智慧项目助理 | 招标文件助手 |
|
||||
- | 上传开题设计报告 | 上传招标文件 |
|
||||
- | 按目录结构提炼 | 按招标要求拆解 |
|
||||
- | 生成可编辑汇总表 | 生成三大响应文件 |
|
||||
- 底部:"同款知识引擎 + 同款工作舱 + 同款流式对话"
|
||||
- **视觉版式**:参考案例 + 对照表
|
||||
- **数据守则**:直接引用 P42 原文事实
|
||||
|
||||
### P18 安全报告助手 · 您的挑战
|
||||
|
||||
- **定位**:共鸣
|
||||
- **标题**:`安全报告:数据多、标准严、出文慢`
|
||||
- **关键内容**:四象限
|
||||
- 数据多源(扫描器/漏扫/基线/渗透)
|
||||
- 标准严格(等保2.0/行业规范/内部合规)
|
||||
- 人工撰写慢
|
||||
- 格式不统一
|
||||
- **视觉版式**:四象限
|
||||
- **数据守则**:"等保2.0"是公开标准
|
||||
|
||||
### P19 安全报告助手 · 工作流(4步)
|
||||
|
||||
- **定位**:流程透明
|
||||
- **标题**:`4步工作流:从多源检测数据到合规报告`
|
||||
- **关键内容**:4步纵向
|
||||
1. 接入多源安全检测数据
|
||||
2. 按合规知识库约束(`laws` + `manual` 分块)
|
||||
3. 提示词工程驱动生成
|
||||
4. 输出可编辑 Word 报告
|
||||
- **视觉版式**:4步纵向
|
||||
- **数据守则**:与平台已实现能力对应
|
||||
|
||||
### P20 安全报告助手 · 差异化
|
||||
|
||||
- **定位**:价值提炼
|
||||
- **标题**:`三大差异化:可溯源 · 受约束 · 可复用`
|
||||
- **关键内容**:
|
||||
- 可溯源:每个数据点可回溯到原始检测数据
|
||||
- 受约束:合规知识库 + 提示词模板双重约束
|
||||
- 可复用:模板库一次建设多次复用
|
||||
- 金句:"AI写报告不是替代专家,是把专家从'重复劳动'里解放出来"
|
||||
- **视觉版式**:三大点 + 底部金句
|
||||
|
||||
### P21 知识库建设与运营闭环
|
||||
|
||||
- **定位**:方法论 - 上
|
||||
- **标题**:`知识库不是上线就结束:构建容易,做好靠运营`
|
||||
- **关键内容**:
|
||||
- 左侧大字号金句(改写自参考PPT讲稿):"`构建容易,做好很难——关键在数据的分类、分权、分域规划和向量化入库策略。我们提供持续的知识运营服务。`"
|
||||
- 右侧闭环图:建设 → 运营 → 优化 → 扩库
|
||||
- **视觉版式**:左大字+右闭环
|
||||
- **数据守则**:金句改写为对客户的专业承诺
|
||||
|
||||
### P22 实施四步法
|
||||
|
||||
- **定位**:方法论 - 下
|
||||
- **标题**:`项目实施四步法:每一步可验收`
|
||||
- **关键内容**:四步横向
|
||||
1. 调研需求分析
|
||||
2. 方案设计
|
||||
3. 实施交付验收
|
||||
4. 运维运营
|
||||
- **视觉版式**:4个圆角矩形横向
|
||||
- **数据守则**:标准流程,无具体数字
|
||||
|
||||
### P23 部署形态与交付清单
|
||||
|
||||
- **定位**:商务
|
||||
- **标题**:`私有化部署 · 一周内启动POC`
|
||||
- **关键内容**:
|
||||
- 三种部署:全内网私有化 / 国产芯片适配 / 混合云
|
||||
- 交付清单:平台基础环境 / 知识库初始化 / 智能体配置 / 用户培训 / 运维手册
|
||||
- **视觉版式**:左右分屏
|
||||
|
||||
### P24 服务承诺与SLA
|
||||
|
||||
- **定位**:商务
|
||||
- **标题**:`服务承诺:从POC到生产,每阶段都有交付物`
|
||||
- **关键内容**:四行表格
|
||||
| 阶段 | 时长 | 交付物 | 验收标准 |
|
||||
|---|---|---|---|
|
||||
| 需求调研 | 1-2周 | 调研报告+方案 | 双方签字 |
|
||||
| POC验证 | 2-4周 | POC环境+1个场景智能体 | 业务场景跑通 |
|
||||
| 实施交付 | 4-8周 | 知识库+智能体+培训 | UAT通过 |
|
||||
| 运维运营 | 持续 | 季度运营报告 | SLA达成 |
|
||||
|
||||
### P25 行业适用与扩展场景
|
||||
|
||||
- **定位**:扩展性
|
||||
- **标题**:`不止于投标:场景智能体按需扩展`
|
||||
- **关键内容**:
|
||||
- 六宫格:招标文件 / 安全报告 / 智能问答 / 智能文书 / 数字员工 / ChatBI数据分析
|
||||
- 底部:"场景不封顶,平台持续演进"
|
||||
- **视觉版式**:六宫格 + 底部金句
|
||||
|
||||
### P26 联系我们 · 下一步
|
||||
|
||||
- **定位**:收口
|
||||
- **标题**:`下一步:选1个高频场景,做1次POC`
|
||||
- **关键内容**:
|
||||
- 三步:①共同选定POC场景 ②提供历史样本 ③2-4周内验证
|
||||
- 联系方式占位
|
||||
|
||||
### P27 致谢页(备选)
|
||||
|
||||
- **定位**:备选
|
||||
- **标题**:`感谢聆听`
|
||||
- **关键内容**:大字号"感谢聆听" + 副标题"期待与您共建企业级智能体平台"
|
||||
- **使用条件**:现场演讲时使用,PDF分发时可替换为P26
|
||||
|
||||
---
|
||||
|
||||
## 6. AI生成提示词(每页)
|
||||
|
||||
> 以下提示词可直接喂给中文AI PPT工具(讯飞智文/百度文心/美图AI PPT等)。每页一条独立提示词。
|
||||
|
||||
### 全局提示词(先发送给AI工具)
|
||||
|
||||
```
|
||||
主题:数智AI平台企业级产品介绍
|
||||
页数:27页
|
||||
画布:16:9(13.333" × 7.5")
|
||||
中文字体:微软雅黑
|
||||
西文字体:Calibri / Arial
|
||||
主色调:深蓝 #003A89 + 亮蓝 #0070C0
|
||||
辅色:红 #C00000、绿 #00B050、米黄 #F5DE5D
|
||||
风格:央国企 To-B 严肃科技风,庄重专业
|
||||
密度:每页20-50个形状,保留40%留白
|
||||
图标:线性图标,不使用插画
|
||||
不使用夸张副词(领先/最强/第一/100%)
|
||||
不使用具体数字(除非代码已证实:17种格式/11种分块/50MB/10个分类)
|
||||
```
|
||||
|
||||
### P1 封面
|
||||
|
||||
```
|
||||
生成PPT封面页,主题为"数智AI平台 · 让每个业务场景都拥有专属智能体"。
|
||||
- 背景:深蓝渐变 #003A89 → #0070C0
|
||||
- 主标题:超大号微软雅黑白色"数智AI平台",下方副标题"让每个业务场景都拥有专属智能体"
|
||||
- 副标题:白色"面向央国企 / 政府 / 能源 / 交通 / 制造 / 金融 的企业级大模型应用平台"
|
||||
- 右下角小字"2026年度产品介绍"
|
||||
- 无图标、无插画、纯文字
|
||||
```
|
||||
|
||||
### P2 一句话价值主张
|
||||
|
||||
```
|
||||
生成PPT内容页,主题为"一句话:让企业知识'用得起来',让AI工作'看得见'"。
|
||||
- 上半部分(70%高度):深蓝底白字,左边大字号"喂数据 · 干事情",右边两个标签"知识引擎 · 让企业文档变成可对话的资产"和"智能体引擎 · 让AI在受控流程下完成具体业务"
|
||||
- 下半部分(30%高度):白底,四个等宽小卡片,每卡片带✅图标和文字:
|
||||
1. "上传招标文件 → 一键拆分三大响应"
|
||||
2. "多源安全数据 → 自动生成合规报告"
|
||||
3. "企业知识库 → 持续运营可对话"
|
||||
4. "业务系统对接 → 工具调用可追溯"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P3 平台总览架构
|
||||
|
||||
```
|
||||
生成PPT架构页,主题为"平台总览:四层架构,端到端企业级交付"。
|
||||
- 四个堆叠的矩形层(自下而上):
|
||||
1. 基础设施层:国产芯片适配(沐曦/天数智芯/海光/昇腾)+ 私有化内网
|
||||
2. 能力引擎层:知识引擎(RAGFlow)+ 智能体引擎(OpenClaw)+ 多模型适配(Qwen/DeepSeek/盘古/混元/文心)
|
||||
3. 平台服务层:智能体广场 / 项目工作舱 / 知识库管理 / 角色权限
|
||||
4. 应用场景层:招标文件助手 / 安全报告助手 / 智能问答 / 智能文书 / 数字员工
|
||||
- 每层用不同深浅的蓝色(底深顶浅)
|
||||
- 层间用细线连接
|
||||
- 左上角小图标例:✅ = 已交付,🟡 = 规划中
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P4 您的挑战
|
||||
|
||||
```
|
||||
生成PPT痛点页,主题为"您是否正在面对这些挑战?"。
|
||||
- 四象限宫格,每格配线性图标:
|
||||
左上:知识"有"但"用不上"(企业内部文档散落各系统)
|
||||
右上:多项目并发上下文串台(投标A用到B的内容)
|
||||
左下:重复文档工作耗人力(商务/技术/报价/合同每项目重写)
|
||||
右下:长周期任务要人盯(招标/报告出具跨周跨月)
|
||||
- 标题居中,四象限等宽
|
||||
- 风格:痛点直白,不堆形容词
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P5 智能体广场
|
||||
|
||||
```
|
||||
生成PPT产品页,主题为"智能体广场:按业务挑选专家,按专家隔离任务"。
|
||||
- 左60%:截图位(描述"卡片网格:每个智能体是一个独立的工作台")
|
||||
- 右40%:四个要点
|
||||
① 卡片式选择
|
||||
② 按智能体隔离会话
|
||||
③ 后台可配置上架新智能体
|
||||
④ "使用中"标识区分状态
|
||||
- 底部小字:"本次演示重点:招标文件助手 · 安全报告助手"
|
||||
- 不写"人气最高""万次调用"等数字
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P6 流式对话可追溯 ⭐
|
||||
|
||||
```
|
||||
生成PPT核心能力页,主题为"AI做的每一步,都摊开给你看"。
|
||||
- 上方三段并列:
|
||||
① 思考过程可折叠(描述"展示思考面板UI")
|
||||
② 工具调用全程可追溯(描述"tool_use 工具名+入参JSON + tool_result 返回")
|
||||
③ 过程摘要("已运行N条命令、错误计数")
|
||||
- 下方横幅金句(大字号深底白字):"对招投标、合规、执法这类'要交代过程'的场景,透明比聪明更重要"
|
||||
- 底部小字:"技术实现:WebSocket 流式协议 · 支持多模型切换"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P7 知识引擎 · 17种格式 · 11种分块 ⭐
|
||||
|
||||
```
|
||||
生成PPT产品页,主题为"知识引擎:让企业的每一份文档都'会说话'"。
|
||||
- 左侧大字号(占40%):三个超大数字 + 说明
|
||||
· 17(格式数)
|
||||
· 11(分块策略数)
|
||||
· 50MB(单文件上限)
|
||||
- 右侧11个标签云(占60%):列出11种分块策略
|
||||
naive / qa / table / tag / resume / book / laws / manual / paper / presentation / one
|
||||
- 每个标签下配小字说明(如 laws:法规条款解析)
|
||||
- 关键标注:"laws 分块专门用于法规条款解析——为后续'招标文件拆解'提供同构能力支撑"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P8 GraphRAG + RAPTOR ⭐
|
||||
|
||||
```
|
||||
生成PPT技术页,主题为"从关键词到语义:让检索'懂'业务"。
|
||||
- 上下结构:
|
||||
上半左右分屏:
|
||||
左半(GraphRAG 实体抽取):
|
||||
- 标题"GraphRAG 实体抽取"
|
||||
- 默认实体类型:organization / person / geo / event / category
|
||||
- "可自扩展"
|
||||
右半(RAPTOR 递归树索引):
|
||||
- 标题"RAPTOR 递归树索引"
|
||||
- 参数:max_token(默认256)/ threshold(0.1)/ max_cluster(64)/ random_seed / 自定义 prompt
|
||||
下半:检索配置三件套
|
||||
- 多知识库关联
|
||||
- 相似度阈值
|
||||
- Top K
|
||||
- 底部案例锚点小字:"同款检索引擎已用于法规合同、企业知识库等场景"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P9 项目工作舱
|
||||
|
||||
```
|
||||
生成PPT产品页,主题为"每个项目一个独立工作舱:文件不串台,过程可回放"。
|
||||
- 四列图标+文字(等宽):
|
||||
① 文件树浏览
|
||||
② 上传文件 / 上传整个文件夹
|
||||
③ PDF / Office / 图片预览
|
||||
④ 单文件下载 / 整项目打包下载
|
||||
- 底部场景说明:"投标项目A、投标项目B、合同评审项目C 物理隔离,互不干扰"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P10 角色与权限
|
||||
|
||||
```
|
||||
生成PPT产品页,主题为"给业务用户一个'纯净'的工作界面"。
|
||||
- 上半左右对比:
|
||||
左:管理员视图(带侧栏/顶栏/菜单/设置)
|
||||
右:common角色全屏视图(仅留对话+文件,无后台元素)
|
||||
- 下半技术说明:RBAC 角色 / 菜单级权限标识 / 路由级鉴权 / 角色动态生成
|
||||
- 底部小字:"已实现并经设计文档 + 多次迭代落地"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P11 多模型 + 私有化
|
||||
|
||||
```
|
||||
生成PPT产品页,主题为"模型不锁定:场景选模型,部署选环境"。
|
||||
- 上半:模型矩阵logo云
|
||||
通义千问 / DeepSeek / 盘古 / 混元 / 百度文心
|
||||
副小字"可按任务灵活切换"
|
||||
- 下半:三种部署形态(三个并列块)
|
||||
1. 全内网私有化
|
||||
2. 国产芯片适配(沐曦/天数/海光/昇腾)
|
||||
3. 公有云对接
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P12 安全设计
|
||||
|
||||
```
|
||||
生成PPT产品页,主题为"企业级安全:看得见的AI,守得住的边界"。
|
||||
- 五列横排图标+文字:
|
||||
1. Token 鉴权(所有REST + WebSocket 请求带 Bearer Token)
|
||||
2. 角色与菜单级权限(用户-角色-权限三级模型)
|
||||
3. 工作舱隔离(每个项目独立 workspacePath)
|
||||
4. 私有化部署(全内网,无外发依赖)
|
||||
5. 信创兼容(国产芯片适配)
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P13 场景总览
|
||||
|
||||
```
|
||||
生成PPT场景总览页,主题为"核心场景:从'通用对话'到'专业助手'的三类封装"。
|
||||
- 三列等宽卡片:
|
||||
左卡片:招标文件助手
|
||||
- 图标(文档类)
|
||||
- 一句话:"上传招标文件,自动拆分商务 / 技术 / 报价三大响应"
|
||||
中卡片:安全报告助手
|
||||
- 图标(盾牌类)
|
||||
- 一句话:"接入多源安全检测数据,按合规知识库约束自动生成报告"
|
||||
右卡片:知识库管理
|
||||
- 图标(数据库类)
|
||||
- 一句话:"把企业文档变成可对话、可检索、可引用的资产"
|
||||
- 底部小字:"基于平台能力封装的场景智能体"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P14 招标文件助手 · 挑战
|
||||
|
||||
```
|
||||
生成PPT痛点页,主题为"招标文件:动辄数百页,三本响应,每条都需对应"。
|
||||
- 四象限宫格:
|
||||
左上:格式杂(PDF/Word混排,含表格图附件)
|
||||
右上:要求散(商务/技术/报价/废标项散落全文)
|
||||
左下:响应繁(三本独立文件,逐条应答)
|
||||
右下:容错低(漏一条即可能废标,人工撰写通常需要数日)
|
||||
- 每格配线性图标
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P15 招标文件助手 · 核心 ⭐
|
||||
|
||||
```
|
||||
生成PPT核心展示页,主题为"上传招标文件 → 一键拆分三大响应文件"。
|
||||
- 顶部大流程图(横向三段):
|
||||
招标文件上传 → 智能拆解 → 三大响应文件
|
||||
- 中部三大响应文件并排(带文件图标 + 描述):
|
||||
1. 商务响应文件
|
||||
- 公司资质 / 业绩 / 商务条款应答 / 偏离说明
|
||||
2. 技术响应文件
|
||||
- 技术方案 / 项目实施 / 人员配置 / 技术偏离表
|
||||
3. 报价文件
|
||||
- 报价明细 / 分项报价 / 税费说明
|
||||
- 底部小字:"按招标要求逐条对应、可编辑、可追溯"
|
||||
- 字体:微软雅黑,本页是全篇重点,请给足空间
|
||||
```
|
||||
|
||||
### P16 招标文件助手 · 工作流
|
||||
|
||||
```
|
||||
生成PPT流程页,主题为"5步工作流:从招标文件到可交付投标文件"。
|
||||
- 5个圆角矩形纵向连接,每个步骤配图标:
|
||||
1. 上传招标文件(支持 PDF/Word,≤50MB)
|
||||
2. 智能解析招标要求(基于11种分块中的 laws 策略)
|
||||
3. 关联企业资料(自动引用企业资质库、业绩库、技术方案库)
|
||||
4. 生成三大响应文件(项目工作舱隔离输出)
|
||||
5. 合规校验与导出(逐条对应检查、生成可编辑 Word 文件)
|
||||
- 步骤间用箭头连接
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P17 招标文件助手 · 同构验证
|
||||
|
||||
```
|
||||
生成PPT验证页,主题为"同构工作流已在项目场景中验证"。
|
||||
- 上半:参考案例描述
|
||||
"智慧项目助理(已落地):开题设计报告上传 → 按一二级目录提炼 → 生成可编辑汇总表 → 邮件推送"
|
||||
- 下半:对照表(两列等宽)
|
||||
| 智慧项目助理(已落地) | 招标文件助手(同构封装)|
|
||||
| 上传开题设计报告 | 上传招标文件 |
|
||||
| 按目录结构提炼 | 按招标要求拆解 |
|
||||
| 生成可编辑汇总表 | 生成三大响应文件 |
|
||||
- 底部小字:"同款知识引擎 + 同款工作舱 + 同款流式对话,本次仅是场景化封装"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P18 安全报告助手 · 挑战
|
||||
|
||||
```
|
||||
生成PPT痛点页,主题为"安全报告:数据多、标准严、出文慢"。
|
||||
- 四象限宫格:
|
||||
左上:数据多源(扫描器/漏扫/基线/渗透测试,输出格式各异)
|
||||
右上:标准严格(等保2.0/行业规范/内部合规要求条款繁杂)
|
||||
左下:人工撰写慢(单份报告通常需数日)
|
||||
右下:格式不统一(不同人写风格不一致,合规要点易遗漏)
|
||||
- 每格配线性图标
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P19 安全报告助手 · 工作流
|
||||
|
||||
```
|
||||
生成PPT流程页,主题为"4步工作流:从多源检测数据到合规报告"。
|
||||
- 4个圆角矩形纵向连接,每个步骤配图标:
|
||||
1. 接入多源安全检测数据(扫描器/漏扫/渗透/基线 — 支持PDF/Word/Excel)
|
||||
2. 按合规知识库约束(行业法规 + 内部安全规范 + 模板库 — 知识引擎 11种分块中 laws + manual 策略)
|
||||
3. 提示词工程驱动生成(按报告类型:等保测评/风险评估/应急响应/渗透测试…)
|
||||
4. 输出可编辑 Word 报告(统一格式、合规要点逐条对应、引用溯源)
|
||||
- 步骤间用箭头连接
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P20 安全报告助手 · 差异化
|
||||
|
||||
```
|
||||
生成PPT价值提炼页,主题为"三大差异化:可溯源 · 受约束 · 可复用"。
|
||||
- 三个并列卡片:
|
||||
1. 可溯源
|
||||
- "报告中每个数据点可回溯到原始检测数据,引用清晰"
|
||||
2. 受约束
|
||||
- "合规知识库 + 提示词模板双重约束,输出符合规范"
|
||||
3. 可复用
|
||||
- "报告模板库一次建设,多次复用;不同项目可继承同款结构"
|
||||
- 底部金句(大字号):"AI写报告不是替代专家,是把专家从'重复劳动'里解放出来"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P21 知识库运营闭环
|
||||
|
||||
```
|
||||
生成PPT方法论页,主题为"知识库不是上线就结束:构建容易,做好靠运营"。
|
||||
- 左侧(40%):大字号金句(深底白字块)
|
||||
"构建容易,做好很难——
|
||||
关键在数据的分类、分权、分域规划和向量化入库策略。
|
||||
我们提供持续的知识运营服务。"
|
||||
- 右侧(60%):闭环图(4个圆形节点 + 箭头)
|
||||
建设 → 运营 → 优化 → 扩库 →(回到建设)
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P22 实施四步法
|
||||
|
||||
```
|
||||
生成PPT方法论页,主题为"项目实施四步法:每一步可验收"。
|
||||
- 4个圆角矩形横向连接,每矩形含编号+标题+要点:
|
||||
1. 调研需求分析
|
||||
- 业务访谈、流程梳理、痛点分级
|
||||
2. 方案设计
|
||||
- 智能体设计、知识库规划、接口对接方案
|
||||
3. 实施交付验收
|
||||
- 分阶段交付、UAT验收、知识库初始化
|
||||
4. 运维运营
|
||||
- 持续运营、效果评估、版本迭代
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P23 部署形态与交付清单
|
||||
|
||||
```
|
||||
生成PPT商务页,主题为"私有化部署 · 一周内启动POC"。
|
||||
- 左半:三种部署形态(三个并列块)
|
||||
1. 全内网私有化(推荐)
|
||||
2. 国产芯片适配
|
||||
3. 混合云
|
||||
- 右半:标准交付清单(5项列表)
|
||||
· 平台基础环境
|
||||
· 知识库初始化
|
||||
· 智能体配置
|
||||
· 用户培训
|
||||
· 运维手册
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P24 服务承诺与SLA
|
||||
|
||||
```
|
||||
生成PPT商务页,主题为"服务承诺:从POC到生产,每阶段都有交付物"。
|
||||
- 四列表格:
|
||||
| 阶段 | 时长 | 交付物 | 验收标准 |
|
||||
| 需求调研 | 1-2周 | 调研报告+方案 | 双方签字 |
|
||||
| POC验证 | 2-4周 | POC环境+1个场景智能体 | 业务场景跑通 |
|
||||
| 实施交付 | 4-8周 | 知识库+智能体+培训 | UAT通过 |
|
||||
| 运维运营 | 持续 | 季度运营报告 | SLA达成 |
|
||||
- 表格下方小字:"以上时长为典型项目经验值,具体以双方协商为准"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P25 行业适用与扩展
|
||||
|
||||
```
|
||||
生成PPT扩展性页,主题为"不止于投标:场景智能体按需扩展"。
|
||||
- 上方六宫格(2行3列),每个宫格一个图标+标题:
|
||||
1. 招标文件助手
|
||||
2. 安全报告助手
|
||||
3. 智能问答助手
|
||||
4. 智能文书
|
||||
5. 数字员工
|
||||
6. ChatBI数据分析
|
||||
- 下方金句(大字号居中):"场景不封顶,平台持续演进"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P26 联系我们 · 下一步
|
||||
|
||||
```
|
||||
生成PPT收口页,主题为"下一步:选1个高频场景,做1次POC"。
|
||||
- 上半:三步横向流程(每步编号+标题+描述)
|
||||
1. 共同选定POC场景
|
||||
"建议从招标文件助手或安全报告助手开始"
|
||||
2. 提供历史样本
|
||||
"用于知识库初始化"
|
||||
3. 2-4周内验证
|
||||
"业务部门实际使用"
|
||||
- 下半:联系方式占位区
|
||||
"商务联系:[姓名] / [电话] / [邮箱]"
|
||||
"技术支持:[姓名] / [电话] / [邮箱]"
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
### P27 致谢页
|
||||
|
||||
```
|
||||
生成PPT致谢页,主题为"感谢聆听"。
|
||||
- 背景:深蓝渐变 #003A89 → #0070C0(与封面同款)
|
||||
- 居中大字号白色"感谢聆听"
|
||||
- 副标题:"期待与您共建企业级智能体平台"
|
||||
- 底部产品名 LOGO
|
||||
- 字体:微软雅黑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据守则(可信度护栏汇总)
|
||||
|
||||
### 7.1 ✅ 允许使用的数字
|
||||
- 17(知识库支持格式数,对应 `knowledgeBase/detail.vue:420`)
|
||||
- 11(知识库分块策略数,对应 `knowledgeBase/detail.vue:418-433`)
|
||||
- 50MB(单文件上传上限,对应 `knowledgeBase/detail.vue:436-449`)
|
||||
- 10(智能体分类标签数,对应 `marketplace.vue:95-106`)
|
||||
- 模型名:Qwen / DeepSeek / 盘古 / 混元 / 文心
|
||||
- 国产芯片:沐曦 / 天数智芯 / 海光 / 昇腾
|
||||
|
||||
### 7.2 ❌ 禁止使用
|
||||
- 任何百分比效果提升(如"准确率提升80%""效率提升5倍")
|
||||
- 客户案例涉及具体公司名(除非已获授权脱敏)
|
||||
- "领先/最强/第一/100%"等夸张副词
|
||||
- "万次调用""百万用户"等无据运营数据
|
||||
- "人气最高"等基于随机数的排序
|
||||
|
||||
### 7.3 ⚠️ 谨慎使用
|
||||
- "通常需要数日":可代替具体天数
|
||||
- "一周内启动POC":行业通用承诺
|
||||
- "1-2周""2-4周":典型项目经验值
|
||||
- 引用参考PPT事实:可直接引用P42 智慧项目助理等已落地案例
|
||||
|
||||
---
|
||||
|
||||
## 8. 交付清单
|
||||
|
||||
1. ✅ 27页详细设计(每页含7字段)
|
||||
2. ✅ 全局提示词(先发给AI工具)
|
||||
3. ✅ 27页逐页提示词(可直接喂给AI工具)
|
||||
4. ✅ 视觉规范(配色/字体/版式)
|
||||
5. ✅ 数据守则(可信度护栏)
|
||||
|
||||
---
|
||||
|
||||
## 9. 后续可选优化
|
||||
|
||||
- 实际产品截图替换占位符(P5/P6/P10)
|
||||
- 现场实操录屏嵌入(P6/P15/P16/P19)
|
||||
- 客户行业定制(能源/交通/政务等)
|
||||
- 英文版翻译(如有海外客户)
|
||||
|
||||
---
|
||||
|
||||
**版本历史**:
|
||||
- v1.0 (2026-08-05):初版设计完成
|
||||
@@ -0,0 +1,264 @@
|
||||
# Frontend 自定义暗色主题 — Design Spec
|
||||
|
||||
**日期:** 2026-08-06
|
||||
**状态:** Approved
|
||||
**参考:** 项目根目录 `image.png`(深色三栏 AI IDE 风格)
|
||||
|
||||
## 背景
|
||||
|
||||
`src/views/frontend/index.vue` 是核心 AI 对话页面,当前使用 Element Plus 默认浅色主题 + 局部样式,整体观感偏传统后台。用户要求基于 `image.png`(类 Cursor 的暗色三栏 IDE 风)对 frontend 自定义样式,以匹配产品"AI 智能体 IDE"的定位。
|
||||
|
||||
## 目标
|
||||
|
||||
将 frontend 页面(不含管理后台)改造为:
|
||||
|
||||
1. 暗色三栏布局:左(会话列表)/ 中(聊天 + 输入)/ 右(文件列表面板 + 预览面板)
|
||||
2. 统一的深色主题 token,作用域隔离,不污染管理后台
|
||||
3. 响应式:屏幕变窄时自动收起右侧文件面板,必要时隐藏侧边栏
|
||||
4. 代码块使用 **Shiki** 渲染,支持与聊天消息中 Markdown 代码块 + 文件预览中的源码文件同款高亮
|
||||
|
||||
## 非目标
|
||||
|
||||
- **不**改动管理后台(`src/views/system/*`、`src/views/tool/*` 等)
|
||||
- **不**新增功能(Git 工具、Progress 面板、终端、Workspace 切换等),仅样式
|
||||
- **不**调整后端 API、不改 WebSocket 协议
|
||||
- **不**引入新的 UI 组件库(继续使用 Element Plus + 少量自定义)
|
||||
- **不**重构 ChatWindow 的消息结构 / 渲染逻辑(保留 markdown-it,仅替换代码块 highlight 插件)
|
||||
|
||||
## 现有约束
|
||||
|
||||
| 项目 | 现状 |
|
||||
| --- | --- |
|
||||
| 根容器类名 | `.frontend-container`(已存在于 `index.vue:861`) |
|
||||
| Markdown 渲染 | 已用 `markdown-it@14.3.0`(ChatWindow) |
|
||||
| 代码高亮 | 暂未配置(默认无高亮或浏览器 monospace) |
|
||||
| 布局 | flex 三栏:sidebar / main-content / file-panel-wrapper + file-preview-wrapper |
|
||||
| 文件面板显隐 | 由 `filePanelVisible` ref 控制(小屏需保持这个行为) |
|
||||
| Element Plus 版本 | 2.13.1(支持 `dark` 命名空间 + CSS 变量覆盖) |
|
||||
|
||||
## 方案概览
|
||||
|
||||
### Phase 1:主题基础设施(半天)
|
||||
|
||||
新增 `src/views/frontend/styles/theme.scss`,定义 SCSS 变量 + CSS 自定义属性,**作用域挂载在 `.frontend-container` 下**:
|
||||
|
||||
```scss
|
||||
.frontend-container {
|
||||
// 基础色
|
||||
--fe-bg-base: #1e1e1e;
|
||||
--fe-bg-elevated: #252526;
|
||||
--fe-bg-overlay: #2d2d30;
|
||||
--fe-bg-input: #3c3c3c;
|
||||
|
||||
// 边框
|
||||
--fe-border: #3c3c3c;
|
||||
--fe-border-strong: #555;
|
||||
|
||||
// 文本
|
||||
--fe-text-primary: #e6e6e6;
|
||||
--fe-text-secondary: #a0a0a0;
|
||||
--fe-text-muted: #6e6e6e;
|
||||
--fe-text-inverse: #1e1e1e;
|
||||
|
||||
// 品牌色
|
||||
--fe-accent: #4ec9b0;
|
||||
--fe-accent-hover: #5fd9c0;
|
||||
--fe-danger: #f48771;
|
||||
--fe-warning: #dcdcaa;
|
||||
--fe-info: #569cd6;
|
||||
|
||||
// 布局尺寸
|
||||
--fe-sidebar-width: 260px;
|
||||
--fe-file-panel-width: 280px;
|
||||
--fe-file-preview-width: 600px;
|
||||
|
||||
// 阴影 / 圆角
|
||||
--fe-radius-sm: 4px;
|
||||
--fe-radius-md: 6px;
|
||||
--fe-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
```
|
||||
|
||||
并 override Element Plus 暗色变量(仅在 `.frontend-container` 作用域内有效,避免影响其他页面):
|
||||
|
||||
```scss
|
||||
.frontend-container {
|
||||
--el-bg-color: var(--fe-bg-elevated);
|
||||
--el-bg-color-page: var(--fe-bg-base);
|
||||
--el-bg-color-overlay: var(--fe-bg-overlay);
|
||||
--el-text-color-primary: var(--fe-text-primary);
|
||||
--el-text-color-regular: var(--fe-text-secondary);
|
||||
--el-text-color-secondary: var(--fe-text-muted);
|
||||
--el-border-color: var(--fe-border);
|
||||
--el-border-color-light: var(--fe-border);
|
||||
--el-fill-color-blank: var(--fe-bg-elevated);
|
||||
--el-color-primary: var(--fe-accent);
|
||||
--el-color-primary-light-3: var(--fe-accent-hover);
|
||||
--el-color-danger: var(--fe-danger);
|
||||
--el-color-warning: var(--fe-warning);
|
||||
--el-color-info: var(--fe-info);
|
||||
}
|
||||
```
|
||||
|
||||
`index.vue` 顶部 `@import './styles/theme.scss';` 引入一次。
|
||||
|
||||
### Phase 2:组件样式改造(1.5 天,逐组件,**不改 props/emit/函数体**)
|
||||
|
||||
| 组件 | 主要变更 |
|
||||
| --- | --- |
|
||||
| `ConversationSidebar.vue` | 整体改为暗色 token;激活态用 `--fe-accent` 替代 `#409eff`;新消息小红点保留;右侧收缩按钮用暗色 hover |
|
||||
| `UserArea.vue` | 头像区背景色改暗;昵称文字 `--fe-text-primary`;按钮 hover 用 `--fe-bg-overlay` |
|
||||
| `ChatHeader.vue` | 顶部栏背景 `--fe-bg-elevated`,下边框 `--fe-border`;模型选择器 / 清空按钮使用暗色变量 |
|
||||
| `ChatWindow.vue` | 消息气泡:用户右侧 `--fe-accent` 浅色背景;助手左侧 `--fe-bg-elevated`;代码块由 Shiki 输出;Markdown 文本颜色用 `--fe-text-primary` |
|
||||
| `InputArea.vue` | 输入框背景 `--fe-bg-input`;按钮图标色改 `--fe-text-secondary`;loading 状态颜色保留 |
|
||||
| `FilePanel.vue` | 列表背景 `--fe-bg-elevated`,选中态 `--fe-bg-overlay` + `--fe-accent` 边框 |
|
||||
| `UserInfoDialog.vue` | `el-dialog` 暗色(依赖 EP 变量覆盖);头像遮罩、按钮沿用暗色 token |
|
||||
| `index.vue` 文件预览面板 | 顶部 header 用 `--fe-bg-elevated`;文本预览 `<pre>` 用 Shiki 输出背景 `--fe-bg-base` + `--fe-text-primary` |
|
||||
|
||||
**不动的部分:**
|
||||
- 所有 `data` / `methods` / `computed` / 生命周期函数
|
||||
- 模板的事件绑定、v-if/v-show 条件、ref 名称
|
||||
- WebSocket 协议、API 调用、Pinia store
|
||||
|
||||
### Phase 3:Shiki 集成(半天)
|
||||
|
||||
1. 安装:`yarn add shiki`(不锁版本,跟随 markdown-it 主版本策略)
|
||||
2. 新增 `src/views/frontend/utils/highlighter.js`:
|
||||
```js
|
||||
import { createHighlighter } from 'shiki'
|
||||
import markdownIt from 'markdown-it'
|
||||
|
||||
const highlighterPromise = createHighlighter({
|
||||
themes: ['github-dark'],
|
||||
langs: ['javascript', 'typescript', 'vue', 'json', 'bash',
|
||||
'python', 'java', 'go', 'rust', 'sql', 'yaml',
|
||||
'html', 'css', 'scss', 'markdown', 'tsx', 'jsx']
|
||||
})
|
||||
|
||||
export async function createMarkdownRenderer() {
|
||||
const highlighter = await highlighterPromise
|
||||
const md = markdownIt({ html: false, linkify: true, breaks: true })
|
||||
md.options.highlight = (code, lang) => {
|
||||
const langKey = highlighter.getLoadedLanguages().includes(lang) ? lang : 'text'
|
||||
return highlighter.codeToHtml(code, { lang: langKey, theme: 'github-dark' })
|
||||
}
|
||||
return md
|
||||
}
|
||||
```
|
||||
3. `ChatWindow.vue`:把当前 `new markdownIt(...)` 替换为 `const md = await createMarkdownRenderer()`;保留 `mermaid` / 链接点击等现有扩展(如果有),并保留对 `code` 标签外层包裹(语言标签)
|
||||
4. `index.vue` 文件预览面板:`<pre>{{ previewContent }}</pre>` 改为 `<div v-html="renderedCode"></div>`,`renderedCode` 在 `previewContent` 变化时由同样的 highlighter 渲染;不识别扩展名时降级为纯文本
|
||||
5. **异步初始化**:在 `ChatWindow` `setup` 顶层用 `await` 不可行(不能阻塞 setup),改用 `onMounted` 中预热 highlighter 缓存到一个共享 `ref`;首屏代码块用 fallback(`<pre><code>{{ code }}</code></pre>`)展示 200ms 后由 watcher 替换
|
||||
|
||||
### Phase 4:响应式断点(半天)
|
||||
|
||||
在 `theme.scss` 中追加:
|
||||
|
||||
```scss
|
||||
.frontend-container {
|
||||
// 1200px 以下:隐藏右侧文件预览面板
|
||||
@media (max-width: 1200px) {
|
||||
.file-preview-wrapper { display: none; }
|
||||
}
|
||||
// 900px 以下:默认收起侧边栏
|
||||
@media (max-width: 900px) {
|
||||
.conversation-sidebar { width: 28px; }
|
||||
.conversation-sidebar .sidebar-content { display: none; }
|
||||
}
|
||||
// 768px 以下:隐藏右侧文件列表面板(小屏仅保留聊天)
|
||||
@media (max-width: 768px) {
|
||||
.file-panel-wrapper { display: none; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**保留现有 `filePanelVisible` 行为不变** —— 用户主动开关的状态在小屏下依然生效(只是视觉上多了一层媒体查询的隐藏,两者独立工作)。
|
||||
|
||||
## 组件契约
|
||||
|
||||
### `src/views/frontend/styles/theme.scss`
|
||||
|
||||
- 无 export,纯 SCSS
|
||||
- `:root` 之外的所有变量挂在 `.frontend-container` 选择器下
|
||||
|
||||
### `src/views/frontend/utils/highlighter.js`
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `createMarkdownRenderer()` | `() => Promise<MarkdownIt>` | 异步创建带 Shiki 高亮的 markdown-it 实例 |
|
||||
| `renderCode(code, lang?)` | `(code: string, lang?: string) => Promise<string>` | 文件预览专用,直接返回 HTML |
|
||||
|
||||
### ChatWindow.vue 改动
|
||||
|
||||
- 新增 `import { createMarkdownRenderer } from '@/views/frontend/utils/highlighter'`
|
||||
- `md` 从同步创建改为异步预热 + `ref` 持有
|
||||
- 模板中 `v-html` 渲染前判断 `mdReady.value`,未就绪时降级 `<pre>`
|
||||
|
||||
## 数据流
|
||||
|
||||
无新数据流。Shiki 的 highlighter 实例是模块级单例,ChatWindow 与文件预览共用。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- Shiki 加载失败:`markdown-it` 的 `highlight` 函数降级为 `(c) => \`<pre><code>${escapeHtml(c)}</code></pre>\`,避免阻塞消息渲染
|
||||
- 不支持的语言:fallback 到 `'text'` 语言(无高亮但保留配色)
|
||||
- 文件预览读取失败:已有 `ElMessage.error`,不动
|
||||
|
||||
## 视觉规范
|
||||
|
||||
| 用途 | 值 |
|
||||
| --- | --- |
|
||||
| 主背景 | `#1e1e1e` |
|
||||
| 卡片 / 面板 | `#252526` |
|
||||
| 输入框 / 浮层 | `#3c3c3c` |
|
||||
| 主色(accent) | `#4ec9b0`(青绿,类似 VS Code Dark+) |
|
||||
| 危险 | `#f48771` |
|
||||
| 文本主 | `#e6e6e6` |
|
||||
| 文本次 | `#a0a0a0` |
|
||||
| 代码块主题 | `github-dark`(与整体色调一致) |
|
||||
|
||||
## 测试
|
||||
|
||||
### 手动测试清单
|
||||
|
||||
1. **主题覆盖**:访问 `/frontend/index`,确认背景变暗;切到管理后台 `/system/user`,确认**未受污染**
|
||||
2. **三栏布局**:左 260px / 中 flex / 右 280px(文件列表面板)+ 600px(文件预览)
|
||||
3. **响应式**:浏览器窗口拉到 1100px → 文件预览消失;拉到 850px → 侧边栏自动收起;拉到 750px → 文件列表面板消失
|
||||
4. **Shiki 渲染**:发一条带 ```js const x = 1 ``` 的消息,确认代码块背景为 `github-dark` 主题色,语法高亮生效
|
||||
5. **文件预览**:点击 markdown / json / py 文件,源码用同款暗色主题渲染
|
||||
6. **原有功能**:新建会话、刷新会话、退出登录、上传头像、修改昵称、二次确认弹窗、WebSocket 收发消息、上下文压缩条、模型切换、token 计数 —— 均**无变化**
|
||||
|
||||
### 回归
|
||||
|
||||
- `yarn dev` 启动后控制台无 SCSS / Vue warn
|
||||
- `yarn build:prod` 通过
|
||||
- 任意管理后台页面(系统管理、监控、工具)打开,目视无暗色污染
|
||||
|
||||
## 风险
|
||||
|
||||
| 风险 | 缓解 |
|
||||
| --- | --- |
|
||||
| Element Plus 组件深色覆盖不完整(部分组件用 hardcoded 色) | Phase 2 组件级逐一覆盖;如 `el-dialog` 头背景、`el-message` 背景用 `!important` + 选择器权重提升 |
|
||||
| Shiki bundle 体积大 | 仅打包 `github-dark` 主题 + 16 个常用语言;按需扩展 |
|
||||
| 文件预览大量代码时高亮阻塞 | highlighter 异步;用 `requestIdleCallback` 或 `setTimeout(0)` 让出主线程 |
|
||||
| 响应式断点与既有状态(如 `filePanelVisible`)冲突 | 断点仅控制 `display`,状态变量保留;大屏恢复时面板重新可见 |
|
||||
|
||||
## 后续可扩展(不在本次范围)
|
||||
|
||||
- 主题切换(亮 / 暗 / 系统跟随)
|
||||
- Shiki 多主题切换
|
||||
- 侧边栏拖拽调宽
|
||||
- 文件预览面板内嵌 Monaco Editor
|
||||
|
||||
## 实施顺序
|
||||
|
||||
| 阶段 | 任务 | 依赖 |
|
||||
| --- | --- | --- |
|
||||
| 1.1 | 创建 `theme.scss` 基础 token + EP 变量覆盖 | 无 |
|
||||
| 1.2 | `index.vue` 引入 `@import './styles/theme.scss'` | 1.1 |
|
||||
| 1.3 | 验证 EP 按钮 / dialog / message 颜色变化 | 1.2 |
|
||||
| 2.x | 逐组件改造样式(不改 JS) | 1.3 |
|
||||
| 3.1 | 安装 shiki + 新增 `utils/highlighter.js` | 无 |
|
||||
| 3.2 | ChatWindow 集成 markdown-it + Shiki | 3.1 |
|
||||
| 3.3 | 文件预览面板使用同一 highlighter | 3.2 |
|
||||
| 4.1 | 三个媒体查询断点 | 1.3 |
|
||||
| 4.2 | 响应式回归测试 | 4.1 + 全部组件完成 |
|
||||
@@ -0,0 +1,116 @@
|
||||
# Frontend 会话列表底部用户区域块 — Design Spec
|
||||
|
||||
**日期:** 2026-08-06
|
||||
**状态:** Approved
|
||||
|
||||
## 背景
|
||||
|
||||
`src/views/frontend/index.vue` 是核心 AI 对话页面,左侧是 `ConversationSidebar`(会话列表)。当前侧边栏底部没有任何用户相关的入口:用户无法在不离开页面的情况下查看自己的头像/昵称、退出登录、或跳转到智能体广场。
|
||||
|
||||
## 目标
|
||||
|
||||
在会话列表底部新增一个区域块,提供:
|
||||
1. 用户头像 + 昵称展示,点击打开「个人资料」修改弹窗(可改头像、昵称)
|
||||
2. 「退出」按钮(二次确认后退出登录)
|
||||
3. 「前往智能体广场」按钮
|
||||
|
||||
侧边栏收起时,该区域块仅显示一个小头像,点击弹出菜单。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不修改智能体广场页本身
|
||||
- 不修改会话列表的样式或行为
|
||||
- 不引入新的退出 / 上传头像后端接口
|
||||
|
||||
## 方案概览
|
||||
|
||||
新增两个组件,`ConversationSidebar` 引入并放在底部。
|
||||
|
||||
### 新增文件
|
||||
|
||||
- `src/views/frontend/components/UserArea.vue`
|
||||
- 展示用户头像 + 昵称、退出按钮、前往智能体广场按钮
|
||||
- 接收 `collapsed` prop(来自 `ConversationSidebar`)
|
||||
- 展开时:完整展示三块内容;收起时:仅显示小圆形头像,点击弹出 dropdown 菜单
|
||||
- 通过 `defineExpose` 暴露 `openInfoDialog()` 方法,父组件 `ConversationSidebar` 用 ref 调用(与现有 `defineExpose({ refresh: fetchSessions, ... })` 风格一致)
|
||||
|
||||
- `src/views/frontend/components/UserInfoDialog.vue`
|
||||
- Element Plus `el-dialog`,标题"个人资料",宽 500px
|
||||
- 内容:头像裁剪上传区(vue-cropper 固定 1:1)+ 昵称输入框
|
||||
- 接收 `modelValue` / `user` props,emit `update:modelValue`、`saved`
|
||||
- 保存时调用:
|
||||
- `uploadAvatar()` — `PUT /system/user/profile/avatar`
|
||||
- `updateUserProfile({ nickName })` — `PUT /system/user/profile`
|
||||
- 保存成功后更新 `useUserStore()` 的 `avatar`、`nickName`,并 emit `saved`,父组件 ElMessage 成功提示
|
||||
|
||||
### 修改文件
|
||||
|
||||
- `src/views/frontend/components/ConversationSidebar.vue`
|
||||
- 在 `sidebar-content` 内的 `<el-scrollbar>` 下方引入 `<UserArea ref="userAreaRef" :collapsed="collapsed" />`
|
||||
- 把 `<el-scrollbar>` 的 `height` 由 `calc(100vh - 160px)` 改为 `calc(100vh - 240px)`(预留底部 user-area 高度)
|
||||
- 把 `sidebar-content` 改为 flex 布局,使 `<el-scrollbar>` 占中间,`<UserArea>` 占底部固定高度
|
||||
|
||||
## 组件契约
|
||||
|
||||
### UserArea.vue
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Props | | |
|
||||
| collapsed | Boolean | 侧边栏是否收起 |
|
||||
| Methods (expose) | | |
|
||||
| openInfoDialog() | () => void | 打开个人资料弹窗 |
|
||||
|
||||
### UserInfoDialog.vue
|
||||
|
||||
| 名称 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Props | | |
|
||||
| modelValue | Boolean | 弹窗显隐(v-model) |
|
||||
| user | Object | 当前用户 `{ avatar, nickName }` |
|
||||
| Emit | | |
|
||||
| update:modelValue | (val: Boolean) | 显隐变化 |
|
||||
| saved | (user: Object) | 保存成功后回调 |
|
||||
|
||||
## 数据流
|
||||
|
||||
1. `ConversationSidebar` 通过 `useUserStore()` 取用户信息,传给 `UserArea` 和 `UserInfoDialog`
|
||||
2. 用户点击头像 → `UserArea.openInfoDialog()` → 内部把 `infoDialogVisible.value = true` → `<UserInfoDialog v-model="infoDialogVisible" :user="user" @saved="onSaved" />`
|
||||
3. `UserInfoDialog` 内部:
|
||||
- 选择本地图片 → `vue-cropper` 裁剪 → 用户点"保存"
|
||||
- 调 `uploadAvatar(formData)` → 成功后调 `updateUserProfile({ nickName })` → 更新 store
|
||||
4. `UserInfoDialog` 关闭后 `ConversationSidebar` 触发 `ElMessage.success('修改成功')`
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 上传头像失败:`ElMessage.error('头像上传失败')`,弹窗保持打开以便重试
|
||||
- 更新昵称失败:`ElMessage.error('修改失败')`
|
||||
- 退出失败:`ElMessage.error('退出失败,请稍后重试')`
|
||||
- 网络异常:依赖 `request.js` 的统一错误处理
|
||||
|
||||
## 视觉规范
|
||||
|
||||
- 与现有侧边栏保持一致色系:`#e4e7ed` 分割线、`#409eff` 主色、`#f56c6c` 危险色
|
||||
- 头像 36×36 圆形(收起时 28×28)
|
||||
- 昵称省略号截断 `text-overflow: ellipsis`
|
||||
- 按钮文字与图标间距 6px
|
||||
- 顶部细分割线:`border-top: 1px solid #e4e7ed`
|
||||
|
||||
## 测试
|
||||
|
||||
- 手动测试:本地 `yarn dev`,访问 `/frontend/index`
|
||||
- 头像昵称正确展示
|
||||
- 点击头像 → 弹窗打开 → 修改头像/昵称 → 保存 → 列表头像昵称同步刷新
|
||||
- 点击退出 → 确认 → 跳转到 `/login`
|
||||
- 点击「前往智能体广场」 → 跳转到 `/frontend/marketplace`
|
||||
- 收起侧边栏 → 仅显示小头像 → hover 出 tooltip → 点击出 dropdown 菜单
|
||||
|
||||
## 风险
|
||||
|
||||
- 头像上传接口需要登录态,已通过 `request.js` 自动带 token 处理
|
||||
- 弹窗高度可能在小屏幕溢出,宽度固定 500px,自适应垂直
|
||||
|
||||
## 后续可扩展(不在本次范围)
|
||||
|
||||
- 收起状态下的 dropdown 也可改成 popover
|
||||
- 头像昵称展示支持在线状态
|
||||
Reference in New Issue
Block a user