mock数据,AI对话,全部应用

This commit is contained in:
2025-12-12 18:17:38 +08:00
parent 8b211fbad6
commit 0a72416365
41 changed files with 5667 additions and 205 deletions

View File

@@ -0,0 +1,90 @@
<template>
<div class="iframe-view">
<iframe
v-if="iframeUrl"
:src="iframeUrl"
class="iframe-content"
frameborder="0"
@load="handleLoad"
/>
<div v-else class="iframe-error">
<el-icon class="error-icon"><WarningFilled /></el-icon>
<p>无效的 iframe 地址</p>
</div>
<div v-if="loading" class="iframe-loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Loading, WarningFilled } from '@element-plus/icons-vue'
const route = useRoute()
const loading = ref(true)
// 从路由 meta 中获取 iframe URL
const iframeUrl = computed(() => {
return route.meta.iframeUrl as string || ''
})
function handleLoad() {
loading.value = false
}
onMounted(() => {
console.log('[IframeView] 加载 iframe:', iframeUrl.value)
})
</script>
<style lang="scss" scoped>
.iframe-view {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.iframe-content {
width: 100%;
height: 100%;
border: none;
}
.iframe-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--el-text-color-secondary);
.error-icon {
font-size: 48px;
margin-bottom: 16px;
color: var(--el-color-warning);
}
}
.iframe-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--el-bg-color);
gap: 12px;
.el-icon {
font-size: 32px;
color: var(--el-color-primary);
}
}
</style>

View File

@@ -0,0 +1,68 @@
.apps-view {
padding: 32px 48px;
height: 100%;
overflow-y: auto;
background: #f9fafb;
}
.page-header {
margin-bottom: 24px;
h1 {
font-size: 24px;
color: #1f2937;
margin-bottom: 8px;
}
p {
color: #6b7280;
font-size: 14px;
}
}
.toolbar-section {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 24px;
.search-input {
max-width: 400px;
}
}
.category-tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
flex-wrap: wrap;
.tab-item {
padding: 8px 20px;
border-radius: 20px;
font-size: 14px;
color: #6b7280;
background: #fff;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
&:hover {
color: #7c3aed;
border-color: #7c3aed;
}
&.active {
color: #fff;
background: #7c3aed;
border-color: #7c3aed;
}
}
}
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}

View File

@@ -0,0 +1,256 @@
<template>
<div class="apps-view">
<header class="page-header">
<h1>全部应用</h1>
<p>选择智能体开始对话助力您的工作效率提升</p>
</header>
<div class="toolbar-section">
<el-input
v-model="searchText"
placeholder="搜索智能体..."
size="large"
:prefix-icon="Search"
clearable
class="search-input"
/>
<el-button type="primary" size="large" @click="handleCreateClick">
<el-icon><Plus /></el-icon>
新增智能体
</el-button>
</div>
<div class="category-tabs">
<span
v-for="cat in categories"
:key="cat.key"
class="tab-item"
:class="{ active: activeCategory === cat.key }"
@click="activeCategory = cat.key"
>
{{ cat.label }}
</span>
</div>
<div class="agents-grid">
<AgentCard
v-for="agent in filteredAgents"
:key="agent.id"
:agent="agent"
@click="handleAgentClick(agent)"
/>
</div>
<!-- 新增/编辑智能体对话框 -->
<AgentEdit
v-model="showEditDialog"
:agent="editingAgent"
@save="handleSaveAgent"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { Search, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import AgentCard from './components/AgentCard/AgentCard.vue'
import AgentEdit from './components/AgentEdit/AgentEdit.vue'
interface Agent {
id: string
name: string
description: string
icon?: string
imageUrl?: string
color: string
category: string
usage: number
apiUrl?: string
}
const router = useRouter()
const searchText = ref('')
const activeCategory = ref('all')
const showEditDialog = ref(false)
const editingAgent = ref<Agent | null>(null)
const categories = [
{ key: 'all', label: '全部' },
{ key: 'content', label: '内容创作' },
{ key: 'office', label: '办公助手' },
{ key: 'business', label: '业务助手' },
{ key: 'lifeline', label: '城市生命线' }
]
// Mock 数据 - 实际使用时从 API 加载
const agents = ref<Agent[]>([
{
id: 'default',
name: '城市生命线助手',
description: '智能城市基础设施安全管理,提供隐患排查、应急预案、数据分析等服务',
icon: '🏙️',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
category: 'lifeline',
usage: 15680
},
{
id: 'emergency',
name: '应急处理助手',
description: '专注于突发事件处理、应急预案制定和响应流程优化',
icon: '🚨',
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
category: 'lifeline',
usage: 8320
},
{
id: 'analysis',
name: '数据分析助手',
description: '城市设施运行数据分析、故障风险预测、分析报告生成',
icon: '📊',
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
category: 'lifeline',
usage: 11200
},
{
id: 'safety',
name: '安全检查助手',
description: '协助进行安全隐患排查、标准流程执行、整改计划制定',
icon: '🔍',
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
category: 'lifeline',
usage: 9540
},
{
id: 'xiaohongshu',
name: '小红书文案生成',
description: '一键生成爆款小红书文案,支持多种风格,自动添加热门话题标签',
icon: '📕',
color: '#ff2442',
category: 'content',
usage: 12580
},
{
id: 'contract',
name: '泰豪合同助手',
description: '智能合同审核、条款分析、风险提示,提高合同处理效率',
icon: '📄',
color: '#7c3aed',
category: 'business',
usage: 8320
},
{
id: 'video',
name: '泰豪短视频助手',
description: '短视频脚本创作、文案优化、热门话题推荐',
icon: '🎬',
color: '#10b981',
category: 'content',
usage: 5640
},
{
id: 'email',
name: '邮件写作助手',
description: '商务邮件、会议邀请、工作汇报等各类邮件智能生成',
icon: '✉️',
color: '#6366f1',
category: 'office',
usage: 7230
},
{
id: 'translate',
name: '多语言翻译',
description: '支持中英日韩等多语言互译,专业术语精准翻译',
icon: '🌐',
color: '#14b8a6',
category: 'office',
usage: 11200
}
])
// 筛选后的智能体
const filteredAgents = computed(() => {
let result = agents.value
if (activeCategory.value !== 'all') {
result = result.filter(a => a.category === activeCategory.value)
}
if (searchText.value) {
const keyword = searchText.value.toLowerCase()
result = result.filter(a =>
a.name.toLowerCase().includes(keyword) ||
a.description.toLowerCase().includes(keyword)
)
}
return result
})
// 点击智能体卡片
const handleAgentClick = (agent: Agent) => {
// TODO: 实际使用时路由跳转到聊天页面
router.push({
path: '/aichat',
query: { agentId: agent.id }
})
}
// 点击新增按钮
const handleCreateClick = () => {
editingAgent.value = null
showEditDialog.value = true
}
// 保存智能体
const handleSaveAgent = async (agentData: Partial<Agent>) => {
try {
if (editingAgent.value) {
// TODO: 调用更新 API
// await agentAPI.update(editingAgent.value.id, agentData)
const index = agents.value.findIndex(a => a.id === editingAgent.value!.id)
if (index > -1) {
agents.value[index] = { ...agents.value[index], ...agentData }
}
ElMessage.success('智能体更新成功')
} else {
// TODO: 调用创建 API
// const newAgent = await agentAPI.create(agentData)
const newAgent: Agent = {
id: 'custom_' + Date.now(),
name: agentData.name || '',
description: agentData.description || '',
icon: agentData.imageUrl ? '' : '🤖',
imageUrl: agentData.imageUrl,
color: agentData.color || '#7c3aed',
category: agentData.category || 'office',
apiUrl: agentData.apiUrl,
usage: 0
}
agents.value.unshift(newAgent)
ElMessage.success('智能体创建成功')
}
showEditDialog.value = false
} catch (error) {
console.error('保存失败:', error)
ElMessage.error('保存失败,请稍后重试')
}
}
// TODO: 组件挂载时加载数据
// onMounted(async () => {
// try {
// const data = await agentAPI.getList()
// agents.value = data
// } catch (error) {
// console.error('加载失败:', error)
// }
// })
</script>
<style lang="scss" scoped>
@import url('./AgentPlatformView.scss');
</style>

View File

@@ -0,0 +1,259 @@
# 智能体平台 AgentPlatformView
## 功能概览
完整的智能体管理平台,支持浏览、搜索、分类、新增和编辑智能体。
## 文件结构
```
Agents/
├── AgentPlatformView.vue # 主视图组件
├── AgentPlatformView.scss # 主视图样式
├── components/
│ ├── AgentCard/ # 智能体卡片组件
│ │ ├── AgentCard.vue
│ │ └── AgentCard.scss
│ └── AgentEdit/ # 智能体编辑对话框组件
│ ├── AgentEdit.vue
│ └── AgentEdit.scss
└── README.md # 本文档
```
## 核心功能
### 1. 智能体展示
- ✅ 网格布局展示智能体卡片
- ✅ 显示图标/图片、名称、描述、使用次数
- ✅ 支持点击跳转到聊天页面
- ✅ 悬停动画效果
### 2. 搜索和筛选
- ✅ 搜索框实时搜索(名称/描述)
- ✅ 分类标签筛选(全部/内容创作/办公助手/业务助手/城市生命线)
- ✅ 组合搜索和分类筛选
### 3. 智能体管理
- ✅ 新增智能体
- ✅ 编辑智能体(预留接口)
- ✅ 图片上传(支持预览)
- ✅ 表单验证
### 4. 高级配置
- ✅ API 链接配置
- ✅ Dify API Key 配置
- ✅ 自定义引导词
- ✅ 提示卡片配置最多3个
## Mock 数据
### 智能体列表
```typescript
[
{
id: 'default',
name: '城市生命线助手',
description: '智能城市基础设施安全管理...',
icon: '🏙️',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
category: 'lifeline',
usage: 15680
},
// ... 其他智能体
]
```
### 分类列表
- **all**: 全部
- **content**: 内容创作
- **office**: 办公助手
- **business**: 业务助手
- **lifeline**: 城市生命线
## 组件说明
### AgentPlatformView主视图
主要功能:
- 展示智能体网格
- 搜索和分类筛选
- 调用子组件
Props: 无
Events: 无
### AgentCard卡片组件
显示单个智能体信息
Props:
- `agent`: Agent - 智能体数据
Events:
- `click`: () => void - 卡片点击事件
### AgentEdit编辑对话框
智能体新增/编辑表单
Props:
- `modelValue`: boolean - 对话框显示状态
- `agent?`: Agent | null - 编辑的智能体数据null 为新增模式)
Events:
- `update:modelValue`: (value: boolean) => void - 更新显示状态
- `save`: (data: Partial<Agent>) => void - 保存事件
## 数据接口
### Agent 类型定义
```typescript
interface Agent {
id?: string
name: string
description: string
icon?: string
imageUrl?: string
color?: string
category: string
usage?: number
apiUrl?: string
difyApiKey?: string
welcomeMessage?: string
suggestions?: string[]
}
```
## 接入真实 API
### 1. 加载智能体列表
`AgentPlatformView.vue``onMounted` 中:
```typescript
import { onMounted } from 'vue'
import { agentAPI } from '@/api/agent'
onMounted(async () => {
try {
const data = await agentAPI.getList()
agents.value = data
} catch (error) {
console.error('加载失败:', error)
ElMessage.error('加载智能体列表失败')
}
})
```
### 2. 创建智能体
`handleSaveAgent` 函数中(新增时):
```typescript
const newAgent = await agentAPI.create(agentData)
agents.value.unshift(newAgent)
ElMessage.success('智能体创建成功')
```
### 3. 更新智能体
`handleSaveAgent` 函数中(编辑时):
```typescript
await agentAPI.update(editingAgent.value.id, agentData)
const index = agents.value.findIndex(a => a.id === editingAgent.value!.id)
if (index > -1) {
agents.value[index] = { ...agents.value[index], ...agentData }
}
ElMessage.success('智能体更新成功')
```
### 4. 图片上传
`AgentEdit.vue``handleFileChange` 中:
```typescript
const uploadedUrl = await uploadAPI.upload(file)
formData.value.imageUrl = uploadedUrl
```
## 路由配置
```typescript
{
path: '/agents',
name: 'Agents',
component: () => import('@/views/public/Agents/AgentPlatformView.vue'),
meta: {
title: '智能体平台'
}
}
```
## 样式特点
### 设计风格
- 网格响应式布局auto-fill, minmax(280px, 1fr)
- 现代卡片设计
- 流畅动画效果
- 渐变色支持
### 主题色
- 主色调:紫色 `#7c3aed`
- 背景色:浅灰 `#f9fafb`
- 边框色:`#e5e7eb`
- 文字色:`#1f2937`
### 动画效果
- 卡片悬停上移4px + 阴影
- 分类标签:颜色渐变
- 上传区域:边框颜色变化
## 依赖
- Vue 3
- Element Plus
- TypeScript
- Vue Router
## 注意事项
1. 所有字段使用 4 个空格缩进(符合用户规则)
2. Mock 数据在组件内,便于测试
3. 已预留 API 接口位置,用 TODO 标注
4. **图片上传使用 FileUpload 组件的 cover 模式**
- 智能体封面120x120 像素,最大 5MB
- 提示词图标60x60 像素,最大 2MB
- 自动上传到服务器,返回 URL
5. 表单验证在保存时进行
6. 提示词最多3个动态添加/删除
7. **依赖 shared 包的 FileUpload 组件**(需确保正确配置)
## 待优化功能
- [ ] 智能体删除功能
- [ ] 智能体排序功能
- [ ] 批量操作
- [ ] 智能体详情页
- [ ] 统计数据展示
- [ ] 导入/导出功能
- [ ] 权限控制
## API 接口设计建议
```typescript
// agent API
export const agentAPI = {
// 获取列表
async getList(params?: {
category?: string
keyword?: string
}): Promise<Agent[]> {},
// 创建
async create(data: Partial<Agent>): Promise<Agent> {},
// 更新
async update(id: string, data: Partial<Agent>): Promise<Agent> {},
// 删除
async delete(id: string): Promise<void> {},
// 获取详情
async getDetail(id: string): Promise<Agent> {}
}
```

View File

@@ -0,0 +1,69 @@
.agent-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 24px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #7c3aed;
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.12);
transform: translateY(-4px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
.agent-icon {
width: 52px;
height: 52px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
.agent-name {
font-size: 17px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.agent-desc {
font-size: 13px;
color: #6b7280;
line-height: 1.6;
margin-bottom: 16px;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 40px;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
.usage-count {
font-size: 12px;
color: #9ca3af;
}
}
}

View File

@@ -0,0 +1,43 @@
<template>
<div class="agent-card" @click="$emit('click')">
<div class="card-header">
<div class="agent-icon" :style="{ background: agent.imageUrl ? 'transparent' : agent.color }">
<img v-if="agent.imageUrl" :src="agent.imageUrl" :alt="agent.name" />
<span v-else>{{ agent.icon }}</span>
</div>
</div>
<h3 class="agent-name">{{ agent.name }}</h3>
<p class="agent-desc">{{ agent.description }}</p>
<div class="card-footer">
<span class="usage-count">{{ agent.usage }} 次使用</span>
<el-button type="primary" size="small" round>开始对话</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
interface Agent {
id: string
name: string
description: string
icon?: string
imageUrl?: string
color: string
category: string
usage: number
apiUrl?: string
}
interface Props {
agent: Agent
}
defineProps<Props>()
defineEmits<{
click: []
}>()
</script>
<style lang="scss" scoped>
@import url('./AgentCard.scss');
</style>

View File

@@ -0,0 +1,88 @@
.agent-cover-upload {
width: 120px;
height: 120px;
:deep(.area.cover) {
border-radius: 12px;
}
:deep(.image-wrapper) {
border-radius: 12px;
}
:deep(.image) {
border-radius: 12px;
}
}
.suggestions-input {
display: flex;
flex-direction: column;
gap: 12px;
.suggestion-card-item {
display: flex;
align-items: center;
gap: 12px;
.card-icon-upload {
flex-shrink: 0;
width: 80px !important;
height: 32px !important;
:deep(.file-upload.cover) {
width: 80px !important;
height: 32px !important;
}
:deep(.area.cover) {
width: 80px !important;
height: 32px !important;
padding: 0 !important;
min-height: unset !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
.content {
padding: 0;
margin: 0;
width: auto;
height: auto;
}
.text,
.tip {
display: none;
}
.icon {
margin: 0 !important;
.plus {
width: 16px;
height: 16px;
&::before {
width: 1px;
height: 16px;
}
&::after {
width: 16px;
height: 1px;
}
}
}
}
}
.card-text-input {
flex: 1;
}
.delete-btn {
flex-shrink: 0;
}
}
}

View File

@@ -0,0 +1,252 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑智能体' : '新增智能体'"
width="520px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form :model="formData" label-width="100px" label-position="top">
<el-form-item label="智能体图片">
<FileUpload
mode="cover"
v-model:coverImg="formData.imageUrl"
accept="image/*"
:maxSize="5 * 1024 * 1024"
class="agent-cover-upload"
/>
</el-form-item>
<el-form-item label="智能体名称" required>
<el-input v-model="formData.name" placeholder="请输入智能体名称" />
</el-form-item>
<el-form-item label="智能体介绍" required>
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入智能体功能介绍"
/>
</el-form-item>
<el-form-item label="API链接">
<el-input v-model="formData.apiUrl" placeholder="请输入API接口地址" />
</el-form-item>
<el-form-item label="Dify API Key">
<el-input
v-model="formData.difyApiKey"
placeholder="请输入Dify智能体API Key"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="引导词">
<el-input
v-model="formData.welcomeMessage"
type="textarea"
:rows="2"
placeholder="例如:今天需要我帮你做点什么吗?"
/>
</el-form-item>
<el-form-item label="提示卡片最多3个">
<div class="suggestions-input">
<div
v-for="(card, index) in (formData.suggestionCards || [])"
:key="index"
class="suggestion-card-item"
>
<!-- 图标上传区域 - 使用 FileUpload cover 模式 -->
<FileUpload
mode="cover"
v-model:coverImg="formData.suggestionCards![index].icon"
accept="image/*"
:maxSize="2 * 1024 * 1024"
class="card-icon-upload"
/>
<!-- 提示词文本输入 -->
<el-input
v-model="formData.suggestionCards![index].text"
:placeholder="`提示词 ${index + 1}`"
class="card-text-input"
/>
<!-- 删除按钮 -->
<el-button
v-if="(formData.suggestionCards?.length || 0) > 1"
:icon="Delete"
@click="removeSuggestion(index)"
class="delete-btn"
/>
</div>
<el-button
v-if="(formData.suggestionCards?.length || 0) < 3"
type="primary"
text
:icon="Plus"
@click="addSuggestion"
>
添加提示词
</el-button>
</div>
</el-form-item>
<el-form-item label="分类" required>
<el-select v-model="formData.category" placeholder="请选择分类" style="width: 100%">
<el-option label="内容创作" value="content" />
<el-option label="办公助手" value="office" />
<el-option label="业务助手" value="business" />
<el-option label="城市生命线" value="lifeline" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { Plus, Delete } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import FileUpload from 'shared/components/FileUpload'
interface SuggestionCard {
text: string
icon?: string
}
interface Agent {
id?: string
name: string
description: string
icon?: string
imageUrl?: string
color?: string
category: string
usage?: number
apiUrl?: string
difyApiKey?: string
welcomeMessage?: string
suggestions?: string[]
suggestionCards?: SuggestionCard[]
}
interface Props {
modelValue: boolean
agent?: Agent | null
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'save', data: Partial<Agent>): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const isEdit = computed(() => !!props.agent)
const formData = ref<Partial<Agent>>({
name: '',
description: '',
category: '',
imageUrl: '',
apiUrl: '',
difyApiKey: '',
welcomeMessage: '',
suggestionCards: [{ text: '', icon: '' }]
})
// 监听 agent 变化,填充表单
watch(() => props.agent, (newAgent) => {
if (newAgent) {
formData.value = {
name: newAgent.name,
description: newAgent.description,
category: newAgent.category,
imageUrl: newAgent.imageUrl || '',
apiUrl: newAgent.apiUrl || '',
difyApiKey: newAgent.difyApiKey || '',
welcomeMessage: newAgent.welcomeMessage || '',
suggestionCards: newAgent.suggestionCards && newAgent.suggestionCards.length > 0
? [...newAgent.suggestionCards]
: [{ text: '', icon: '' }]
}
} else {
// 重置表单
formData.value = {
name: '',
description: '',
category: '',
imageUrl: '',
apiUrl: '',
difyApiKey: '',
welcomeMessage: '',
suggestionCards: [{ text: '', icon: '' }]
}
}
}, { immediate: true })
const addSuggestion = () => {
if (formData.value.suggestionCards && formData.value.suggestionCards.length < 3) {
formData.value.suggestionCards.push({ text: '', icon: '' })
}
}
const removeSuggestion = (index: number) => {
if (formData.value.suggestionCards) {
formData.value.suggestionCards.splice(index, 1)
}
}
const handleClose = () => {
dialogVisible.value = false
}
const handleSave = () => {
// 验证
if (!formData.value.name?.trim()) {
ElMessage.warning('请输入智能体名称')
return
}
if (!formData.value.description?.trim()) {
ElMessage.warning('请输入智能体介绍')
return
}
if (!formData.value.category) {
ElMessage.warning('请选择分类')
return
}
// 过滤空的提示词卡片
const suggestionCards = formData.value.suggestionCards?.filter(s => s.text.trim()) || []
// 生成随机颜色(如果没有上传图片)
const colors = ['#7c3aed', '#10b981', '#f59e0b', '#3b82f6', '#ef4444', '#6366f1', '#14b8a6']
const randomColor = colors[Math.floor(Math.random() * colors.length)]
emit('save', {
...formData.value,
color: formData.value.imageUrl ? undefined : randomColor,
suggestionCards
})
}
</script>
<style lang="scss" scoped>
@import url('./AgentEdit.scss');
</style>

View File

@@ -0,0 +1,386 @@
.chat-view {
display: flex;
flex-direction: row;
height: 100%;
background: #fff;
position: relative;
}
// 左侧侧边栏样式 - ChatGPT Style
.chat-sidebar {
width: 260px;
background: #f7f7f8;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
transition: width 0.3s ease;
&.collapsed {
width: 60px;
.new-chat-btn {
padding: 10px;
justify-content: center;
}
}
.sidebar-header {
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
border-bottom: 1px solid #e5e7eb;
}
.new-chat-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 16px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f3f4f6;
border-color: #7c3aed;
color: #7c3aed;
}
}
.collapse-toggle {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 16px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f3f4f6;
color: #7c3aed;
}
}
.conversations-list {
flex: 1;
overflow-y: auto;
padding: 12px 8px;
}
.list-section {
margin-bottom: 16px;
.section-title {
padding: 8px 12px;
font-size: 12px;
color: #9ca3af;
font-weight: 500;
}
}
.conversation-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
color: #374151;
&:hover {
background: #e5e7eb;
.conv-actions {
opacity: 1;
}
}
&.active {
background: #e5e7eb;
}
.conv-title {
flex: 1;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-actions {
opacity: 0;
display: flex;
gap: 4px;
transition: opacity 0.2s;
.action-icon {
padding: 4px;
border-radius: 4px;
color: #6b7280;
&:hover {
background: #d1d5db;
color: #ef4444;
}
}
}
}
}
// 主聊天区域
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.chat-header {
padding: 16px 24px;
border-bottom: 1px solid #e5e7eb;
.agent-dropdown {
cursor: pointer;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 500;
color: #374151;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s;
&:hover {
background: #f3f4f6;
color: #7c3aed;
}
.agent-icon {
font-size: 20px;
}
}
}
.dropdown-agent-icon {
margin-right: 8px;
font-size: 16px;
}
:deep(.el-dropdown-menu__item.is-active) {
background: rgba(124, 58, 237, 0.1);
color: #7c3aed;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 40px 80px;
}
.welcome-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 400px;
}
.messages-container {
display: flex;
flex-direction: column;
gap: 24px;
.message {
display: flex;
gap: 12px;
&.user {
flex-direction: row-reverse;
.message-content {
align-items: flex-end;
.message-text {
background: #7c3aed;
color: #fff;
}
}
}
.message-avatar {
flex-shrink: 0;
.ai-avatar-small {
width: 40px;
height: 40px;
border-radius: 10px;
object-fit: contain;
background: #f3f4f6;
padding: 4px;
}
.user-avatar-small {
width: 40px;
height: 40px;
background: #e5e7eb;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
}
.message-content {
display: flex;
flex-direction: column;
gap: 4px;
max-width: 70%;
.message-text {
padding: 12px 16px;
background: #f3f4f6;
border-radius: 12px;
color: #1f2937;
line-height: 1.6;
white-space: pre-wrap;
}
.message-time {
font-size: 12px;
color: #9ca3af;
padding: 0 4px;
}
}
}
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 16px;
span {
width: 8px;
height: 8px;
background: #9ca3af;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out both;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.input-area {
padding: 20px 80px 30px;
background: #fff;
.input-wrapper {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 12px 16px;
textarea {
width: 100%;
border: none;
background: transparent;
resize: none;
outline: none;
font-size: 14px;
color: #1f2937;
min-height: 24px;
max-height: 120px;
&::placeholder {
color: #9ca3af;
}
}
.input-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
.action-buttons, .send-actions {
display: flex;
gap: 8px;
}
.action-btn {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #6b7280;
transition: all 0.2s;
border: none;
background: transparent;
cursor: pointer;
&:hover {
background: #e5e7eb;
color: #374151;
}
}
.send-btn {
width: 36px;
height: 36px;
background: #7c3aed;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
transition: all 0.2s;
border: none;
cursor: pointer;
&:hover:not(:disabled) {
background: #5b21b6;
}
&:disabled {
background: #d1d5db;
cursor: not-allowed;
}
}
}
}
}

View File

@@ -0,0 +1,427 @@
<template>
<div class="chat-view">
<!-- Left Sidebar - ChatGPT Style -->
<aside class="chat-sidebar" :class="{ collapsed: sidebarCollapsed }">
<div class="sidebar-header">
<button class="collapse-toggle" @click="sidebarCollapsed = !sidebarCollapsed">
<el-icon><Fold v-if="!sidebarCollapsed" /><Expand v-else /></el-icon>
<span v-if="!sidebarCollapsed">{{ sidebarCollapsed ? '展开' : '收起' }}</span>
</button>
<button class="new-chat-btn" @click="handleNewChat">
<el-icon><Plus /></el-icon>
<span v-if="!sidebarCollapsed">新建对话</span>
</button>
</div>
<div v-if="!sidebarCollapsed" class="conversations-list">
<div class="list-section">
<div class="section-title">今天</div>
<div
v-for="conv in todayConversations"
:key="conv.id"
class="conversation-item"
:class="{ active: currentConversationId === conv.id }"
@click="selectConversation(conv)"
>
<el-icon><ChatDotRound /></el-icon>
<span class="conv-title">{{ conv.title }}</span>
<div class="conv-actions">
<el-icon class="action-icon" @click.stop="deleteConversation(conv.id)"><Delete /></el-icon>
</div>
</div>
</div>
<div class="list-section" v-if="olderConversations.length > 0">
<div class="section-title">历史记录</div>
<div
v-for="conv in olderConversations"
:key="conv.id"
class="conversation-item"
:class="{ active: currentConversationId === conv.id }"
@click="selectConversation(conv)"
>
<el-icon><ChatDotRound /></el-icon>
<span class="conv-title">{{ conv.title }}</span>
<div class="conv-actions">
<el-icon class="action-icon" @click.stop="deleteConversation(conv.id)"><Delete /></el-icon>
</div>
</div>
</div>
</div>
</aside>
<!-- Main Chat Area -->
<div class="chat-main">
<!-- Header -->
<header class="chat-header">
<el-dropdown trigger="click" @command="handleAgentChange" class="agent-dropdown">
<div class="header-title">
<span class="agent-icon">{{ currentAgent.icon }}</span>
<span>{{ currentAgent.name }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="agent in agents"
:key="agent.id"
:command="agent.id"
:class="{ 'is-active': agent.id === currentAgent.id }"
>
<span class="dropdown-agent-icon">{{ agent.icon }}</span>
<span>{{ agent.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</header>
<!-- Chat Content -->
<div class="chat-content" ref="chatContentRef">
<!-- Welcome Message -->
<div v-if="messages.length === 0" class="welcome-section">
<ChatDefault
:agent="currentAgent"
@suggestion-click="handleSuggestionClick"
/>
</div>
<!-- Chat Messages -->
<div v-else class="messages-container">
<div
v-for="message in messages"
:key="message.id"
class="message"
:class="message.role"
>
<div class="message-avatar">
<img v-if="message.role === 'assistant'" src="/logo.jpg" alt="AI" class="ai-avatar-small" />
<div v-else class="user-avatar-small">👤</div>
</div>
<div class="message-content">
<div class="message-text">{{ message.content }}</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
</div>
<!-- Loading indicator -->
<div v-if="isLoading" class="message assistant">
<div class="message-avatar">
<img src="/logo.jpg" alt="AI" class="ai-avatar-small" />
</div>
<div class="message-content">
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
</div>
<!-- Input Area -->
<div class="input-area">
<div class="input-wrapper">
<textarea
v-model="inputText"
placeholder="请输入内容..."
@keydown.enter.prevent="handleSend"
rows="1"
ref="textareaRef"
></textarea>
<div class="input-actions">
<div class="action-buttons">
<button class="action-btn" title="附件">
<el-icon><Paperclip /></el-icon>
</button>
<button class="action-btn" title="表情">
<el-icon><Star /></el-icon>
</button>
<button class="action-btn" title="图片">
<el-icon><Picture /></el-icon>
</button>
<button class="action-btn" title="更多">
<el-icon><MoreFilled /></el-icon>
</button>
<button class="action-btn" title="截图">
<el-icon><CameraFilled /></el-icon>
</button>
</div>
<div class="send-actions">
<button class="action-btn" title="语音">
<el-icon><Microphone /></el-icon>
</button>
<button class="send-btn" @click="handleSend" :disabled="!inputText.trim()">
<el-icon><Promotion /></el-icon>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, nextTick, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import {
ArrowDown,
Paperclip,
Star,
Picture,
MoreFilled,
CameraFilled,
Microphone,
Promotion,
Plus,
Fold,
Expand,
ChatDotRound,
Delete
} from '@element-plus/icons-vue'
import ChatDefault from './components/ChatDefault/ChatDefault.vue'
const route = useRoute()
// Mock 数据 - 智能体列表
const agents = ref([
{
id: 'default',
name: '城市生命线助手',
icon: '🏙️',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
description: '我是城市生命线智能助手,专注于城市基础设施安全管理'
},
{
id: 'emergency',
name: '应急处理助手',
icon: '🚨',
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
description: '专注于应急事件处理和预案制定'
},
{
id: 'analysis',
name: '数据分析助手',
icon: '📊',
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
description: '提供数据分析和可视化服务'
},
{
id: 'safety',
name: '安全检查助手',
icon: '🔍',
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
description: '协助进行安全隐患排查和整改'
}
])
const currentAgent = ref(agents.value[0])
// 状态管理
const sidebarCollapsed = ref(false)
const currentConversationId = ref<number | null>(null)
const inputText = ref('')
const isLoading = ref(false)
const chatContentRef = ref<HTMLElement | null>(null)
const textareaRef = ref<HTMLTextAreaElement | null>(null)
// Mock 数据 - 对话历史
interface Conversation {
id: number
title: string
date: Date
messages: Message[]
}
interface Message {
id: string
content: string
role: 'user' | 'assistant'
timestamp: string
}
const conversations = ref<Conversation[]>([
{
id: 1,
title: '城市生命线关键设施咨询',
date: new Date(),
messages: []
},
{
id: 2,
title: '消防安全隐患处理方案',
date: new Date(),
messages: []
},
{
id: 3,
title: '排水系统优化建议',
date: new Date(Date.now() - 86400000),
messages: []
},
{
id: 4,
title: '应急预案讨论',
date: new Date(Date.now() - 172800000),
messages: []
}
])
const messages = ref<Message[]>([])
// 今天的对话
const todayConversations = computed(() => {
const today = new Date()
today.setHours(0, 0, 0, 0)
return conversations.value.filter(conv => new Date(conv.date) >= today)
})
// 历史对话
const olderConversations = computed(() => {
const today = new Date()
today.setHours(0, 0, 0, 0)
return conversations.value.filter(conv => new Date(conv.date) < today)
})
// 格式化时间
const formatTime = (timestamp: string) => {
if (!timestamp) return ''
const date = new Date(timestamp)
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// 滚动到底部
const scrollToBottom = async () => {
await nextTick()
if (chatContentRef.value) {
chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight
}
}
// 新建对话
const handleNewChat = () => {
const newConv: Conversation = {
id: Date.now(),
title: '新对话',
date: new Date(),
messages: []
}
conversations.value.unshift(newConv)
currentConversationId.value = newConv.id
messages.value = []
}
// 选择对话
const selectConversation = (conv: Conversation) => {
currentConversationId.value = conv.id
messages.value = conv.messages || []
scrollToBottom()
}
// 删除对话
const deleteConversation = (id: number) => {
const index = conversations.value.findIndex(c => c.id === id)
if (index > -1) {
conversations.value.splice(index, 1)
if (currentConversationId.value === id) {
currentConversationId.value = null
messages.value = []
}
}
}
// 切换智能体
const handleAgentChange = (agentId: string) => {
const agent = agents.value.find(a => a.id === agentId)
if (agent) {
currentAgent.value = agent
// 切换智能体时清空对话
messages.value = []
currentConversationId.value = null
}
}
// 处理建议点击
const handleSuggestionClick = async (suggestion: string) => {
inputText.value = suggestion
await handleSend()
}
// 发送消息 - Mock 实现
const handleSend = async () => {
const text = inputText.value.trim()
if (!text || isLoading.value) return
// 添加用户消息
const userMessage: Message = {
id: Date.now().toString(),
content: text,
role: 'user',
timestamp: new Date().toISOString()
}
messages.value.push(userMessage)
inputText.value = ''
await scrollToBottom()
// 显示加载状态
isLoading.value = true
// Mock API 响应
setTimeout(async () => {
const mockResponses = [
'根据城市生命线安全管理的相关规定,我为您分析如下...',
'这是一个很好的问题。让我详细为您解答...',
'基于您的需求,我建议采取以下措施...',
'从专业角度来看,这个问题需要综合考虑多个因素...'
]
const assistantMessage: Message = {
id: Date.now().toString() + '_ai',
content: mockResponses[Math.floor(Math.random() * mockResponses.length)],
role: 'assistant',
timestamp: new Date().toISOString()
}
messages.value.push(assistantMessage)
// 保存到当前对话
if (currentConversationId.value) {
const conv = conversations.value.find(c => c.id === currentConversationId.value)
if (conv) {
conv.messages = messages.value
// 更新对话标题(取第一条用户消息)
if (conv.title === '新对话' && messages.value.length > 0) {
const firstUserMsg = messages.value.find(m => m.role === 'user')
if (firstUserMsg) {
conv.title = firstUserMsg.content.slice(0, 20) + (firstUserMsg.content.length > 20 ? '...' : '')
}
}
}
}
isLoading.value = false
await scrollToBottom()
}, 1000 + Math.random() * 1000) // 随机延迟 1-2 秒
}
onMounted(() => {
// 从 URL 参数读取 agentId
const agentId = route.query.agentId as string
if (agentId) {
const agent = agents.value.find(a => a.id === agentId)
if (agent) {
currentAgent.value = agent
console.log('[AI Chat] 已切换到智能体:', agent.name)
}
}
console.log('AI Chat View mounted')
})
</script>
<style lang="scss" scoped>
@import url('./AIChatView.scss');
</style>

View File

@@ -0,0 +1,176 @@
# AI 聊天界面组件
## 功能概览
完整的 AI 聊天界面实现,包含侧边栏对话历史、智能体切换、消息交互等功能。
## 文件结构
```
Chat/
├── AIChatView.vue # 主聊天界面组件
├── AIChatView.scss # 主界面样式
├── components/
│ ├── ChatDefault/ # 欢迎界面组件
│ │ ├── ChatDefault.vue
│ │ └── ChatDefault.scss
│ └── ChatHistory/ # 历史记录组件(预留)
│ ├── ChatHistory.vue
│ └── ChatHistory.scss
└── README.md # 本文档
```
## 核心功能
### 1. 侧边栏功能
- ✅ 对话历史列表(今天/历史记录分组)
- ✅ 新建对话
- ✅ 删除对话
- ✅ 侧边栏收起/展开
### 2. 智能体系统
- ✅ 多智能体支持(城市生命线、应急处理、数据分析、安全检查)
- ✅ 智能体切换(下拉选择)
- ✅ 每个智能体独立的欢迎信息和建议
### 3. 消息交互
- ✅ 消息发送/接收
- ✅ 用户/AI 消息区分显示
- ✅ 打字中动画效果
- ✅ 消息时间显示
- ✅ 自动滚动到底部
### 4. 输入功能
- ✅ 多行文本输入
- ✅ Enter 发送Shift+Enter 换行)
- ✅ 附件、表情、图片等功能按钮UI 已实现)
- ✅ 发送按钮禁用状态
## Mock 数据
### 智能体列表
```typescript
[
{
id: 'default',
name: '城市生命线助手',
icon: '🏙️',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
description: '我是城市生命线智能助手,专注于城市基础设施安全管理'
},
// ... 其他智能体
]
```
### Mock API 响应
发送消息后,系统会在 1-2 秒后返回随机的 Mock 响应:
- "根据城市生命线安全管理的相关规定,我为您分析如下..."
- "这是一个很好的问题。让我详细为您解答..."
- "基于您的需求,我建议采取以下措施..."
- "从专业角度来看,这个问题需要综合考虑多个因素..."
## 样式特点
### 设计风格
- ChatGPT 风格的侧边栏布局
- 现代简约的配色方案
- 流畅的动画效果
- 响应式设计
### 主题色
- 主色调:紫色 `#7c3aed`
- 背景色:灰白 `#f7f7f8`
- 边框色:浅灰 `#e5e7eb`
- 文字色:深灰 `#374151`
## 使用方式
### 基本使用
```vue
<template>
<AIChatView />
</template>
<script setup>
import AIChatView from '@/views/public/Chat/AIChatView.vue'
</script>
```
### 集成到路由
```typescript
{
path: '/chat',
name: 'Chat',
component: () => import('@/views/public/Chat/AIChatView.vue')
}
```
## 后续接入真实 API
### 修改发送消息函数
`AIChatView.vue` 中找到 `handleSend` 函数,将 Mock 实现替换为真实 API 调用:
```typescript
// 替换这部分 Mock 代码
setTimeout(async () => {
const mockResponses = [...]
// ...
}, 1000)
// 改为真实 API 调用
try {
const response = await chatAPI.sendMessage({
message: text,
agentId: currentAgent.value.id,
conversationId: currentConversationId.value
})
const assistantMessage: Message = {
id: response.id,
content: response.content,
role: 'assistant',
timestamp: response.timestamp
}
messages.value.push(assistantMessage)
} catch (error) {
console.error('发送失败:', error)
}
```
### 加载历史对话
`onMounted` 中添加历史对话加载:
```typescript
onMounted(async () => {
try {
const history = await chatAPI.getConversations()
conversations.value = history
} catch (error) {
console.error('加载历史失败:', error)
}
})
```
## 依赖
- Vue 3
- Element Plus
- TypeScript
## 注意事项
1. 确保已安装 Element Plus 并正确配置图标
2. `/logo.jpg` 需要存在于 public 目录
3. 消息滚动使用了 `scrollTop`,需要在有高度的容器中使用
4. 缩进使用 4 个空格(符合用户规则)
## 待优化功能
- [ ] 消息流式输出
- [ ] 代码块语法高亮
- [ ] Markdown 渲染
- [ ] 消息重新生成
- [ ] 消息编辑
- [ ] 导出对话记录
- [ ] 附件上传功能实现
- [ ] 语音输入功能实现

View File

@@ -0,0 +1,82 @@
.chat-default {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
}
.ai-avatar {
width: 88px;
height: 88px;
margin-bottom: 24px;
.avatar-icon {
width: 100%;
height: 100%;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
color: #fff;
}
}
.welcome-text {
color: #6b7280;
font-size: 14px;
margin-bottom: 12px;
text-align: center;
}
.welcome-title {
font-size: 24px;
font-weight: 600;
color: #1f2937;
margin-bottom: 40px;
text-align: center;
}
.suggestion-cards {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
.suggestion-card {
width: 220px;
padding: 20px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: #7c3aed;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);
transform: translateY(-2px);
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
margin-bottom: 12px;
}
.card-text {
color: #374151;
font-size: 14px;
line-height: 1.5;
margin: 0;
}
}
}

View File

@@ -0,0 +1,109 @@
<template>
<div class="chat-default">
<!-- AI 头像 -->
<div class="ai-avatar">
<div class="avatar-icon" :style="{ background: agent.color }">
{{ agent.icon }}
</div>
</div>
<!-- 描述文字 -->
<p class="welcome-text">
{{ agent.description }}
</p>
<!-- 欢迎标题 -->
<h2 class="welcome-title">{{ welcomeTitle }}</h2>
<!-- 建议卡片 -->
<div class="suggestion-cards">
<div
v-for="(suggestion, index) in currentSuggestions"
:key="index"
class="suggestion-card"
@click="handleSuggestionClick(suggestion)"
>
<div class="card-icon" :style="{ background: agent.color }">
<component :is="cardIcons[index % cardIcons.length]" />
</div>
<p class="card-text">{{ suggestion }}</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { OfficeBuilding, Warning, Cloudy } from '@element-plus/icons-vue'
interface Agent {
id: string
name: string
icon: string
color: string
description: string
}
interface Props {
agent: Agent
}
const props = defineProps<Props>()
const emit = defineEmits<{
suggestionClick: [suggestion: string]
}>()
// 各智能体的建议内容
const agentSuggestions: Record<string, string[]> = {
default: [
'城市生命线关键设施有哪些?',
'消防安全隐患常见问题以及处理措施有哪些?',
'如何平衡排水能力和生态环境保护?'
],
emergency: [
'应急预案应该包含哪些关键内容?',
'突发事件的处理流程是什么?',
'如何建立高效的应急响应机制?'
],
analysis: [
'如何分析城市设施运行数据?',
'帮我生成一份数据分析报告',
'如何预测设施故障风险?'
],
safety: [
'安全检查的标准流程是什么?',
'常见安全隐患有哪些类型?',
'如何制定隐患整改计划?'
]
}
// 各智能体的欢迎标题
const agentWelcomeTitles: Record<string, string> = {
default: '今天需要我帮你做点什么吗?',
emergency: '需要我帮你处理什么应急事件?',
analysis: '需要我帮你分析什么数据?',
safety: '需要我帮你检查什么安全隐患?'
}
// 当前智能体的建议
const currentSuggestions = computed(() => {
return agentSuggestions[props.agent.id] || agentSuggestions.default
})
// 当前欢迎标题
const welcomeTitle = computed(() => {
return agentWelcomeTitles[props.agent.id] || agentWelcomeTitles.default
})
// 卡片图标
const cardIcons = [OfficeBuilding, Warning, Cloudy]
// 处理建议点击
const handleSuggestionClick = (suggestion: string) => {
emit('suggestionClick', suggestion)
}
</script>
<style lang="scss" scoped>
@import url('./ChatDefault.scss');
</style>

View File

@@ -71,6 +71,7 @@ import type { LoginParam } from 'shared/types'
import { authAPI } from 'shared/authAPI'
import { getAesInstance } from 'shared/utils'
import { TokenManager } from 'shared/api'
import { resetDynamicRoutes } from '@/router'
//
const router = useRouter()
@@ -130,22 +131,40 @@ async function handleLogin() {
const response = await authAPI.login(loginParam)
// 7.
if (response.data.success && response.data.data) {
const loginData = response.data.data
if (response.success && response.data) {
const loginData = response.data
// 8. Token
if (loginData.token) {
TokenManager.setToken(loginData.token, loginForm.rememberMe)
localStorage.setItem('token', loginData.token)
}
// 9.
// 9. LoginDomain LocalStorage
if (loginData) {
localStorage.setItem('loginDomain', JSON.stringify(loginData))
localStorage.setItem('views', JSON.stringify(loginData.userViews))
}
// 10.
if (loginData.user) {
localStorage.setItem('userInfo', JSON.stringify(loginData.user))
} else if (loginData.userInfo) {
localStorage.setItem('userInfo', JSON.stringify(loginData.userInfo))
}
// 11.
resetDynamicRoutes()
// 12.
ElMessage.success('登录成功!')
// 10.
router.push('/home')
// 13.
const redirect = router.currentRoute.value.query.redirect as string
router.push(redirect || '/')
} else {
//
ElMessage.error(response.data.message || '登录失败,请检查用户名和密码')
ElMessage.error(response.message || '登录失败,请检查用户名和密码')
}
} catch (error: any) {
console.error('登录失败:', error)