This commit is contained in:
2025-12-12 18:32:14 +08:00
parent e66eb6b575
commit e002f0d989
41 changed files with 36625 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,453 @@
<template>
<div class="agent-chat-view">
<!-- Header -->
<header class="chat-header">
<div class="header-left">
<el-button :icon="ArrowLeft" circle size="small" @click="goBack" />
<div class="agent-info">
<span class="agent-name">{{ agentName }}</span>
<span class="agent-status">在线</span>
</div>
</div>
</header>
<!-- Chat Content -->
<div class="chat-content" ref="chatContentRef">
<!-- Welcome Message -->
<div v-if="messages.length === 0" class="welcome-section">
<div class="ai-avatar">
<span class="avatar-emoji">{{ agentEmoji }}</span>
</div>
<h2 class="welcome-title">{{ agentName }}</h2>
<p class="welcome-text">{{ agentDescription }}</p>
<!-- Quick Actions -->
<div class="quick-actions">
<div
v-for="(action, index) in quickActions"
:key="index"
class="action-item"
@click="handleQuickAction(action)"
>
{{ action }}
</div>
</div>
</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">
<span v-if="message.role === 'assistant'" class="ai-avatar-small">{{ agentEmoji }}</span>
<div v-else class="user-avatar-small">👤</div>
</div>
<div class="message-content">
<div class="message-text">{{ message.content }}</div>
</div>
</div>
<!-- Loading indicator -->
<div v-if="isLoading" class="message assistant">
<div class="message-avatar">
<span class="ai-avatar-small">{{ agentEmoji }}</span>
</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="inputPlaceholder"
@keydown.enter.prevent="handleSend"
rows="1"
></textarea>
<button class="send-btn" @click="handleSend" :disabled="!inputText.trim()">
<el-icon><Promotion /></el-icon>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, Promotion } from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const inputText = ref('')
const isLoading = ref(false)
const chatContentRef = ref(null)
const messages = ref([])
const agentId = computed(() => route.query.id || 'default')
const agentName = computed(() => route.query.name || 'AI助手')
const agentConfig = {
xiaohongshu: {
emoji: '📕',
description: '我是小红书文案生成助手,可以帮你创作爆款笔记文案,支持种草、测评、教程等多种风格。',
placeholder: '描述你想要的文案主题,如:推荐一款平价护肤品...',
quickActions: ['帮我写一篇美食探店笔记', '生成护肤品种草文案', '写一篇旅行攻略', '创作穿搭分享文案']
},
contract: {
emoji: '📄',
description: '我是泰豪合同助手,可以帮你审核合同条款、识别风险点、提供修改建议。',
placeholder: '请粘贴合同内容或描述你的需求...',
quickActions: ['审核这份合同的风险点', '解读合同关键条款', '生成合同模板', '对比两份合同差异']
},
video: {
emoji: '🎬',
description: '我是泰豪短视频助手,专注于短视频脚本创作、文案优化和热门话题推荐。',
placeholder: '告诉我你想创作什么类型的短视频...',
quickActions: ['写一个产品介绍脚本', '生成热门话题文案', '优化视频标题', '创作口播文案']
},
bidding: {
emoji: '📋',
description: '我是招标文件助手,可以帮你解读招标文件、生成投标方案、进行合规性检查。',
placeholder: '请描述你的招标需求或粘贴招标文件内容...',
quickActions: ['解读招标文件要求', '生成投标技术方案', '检查投标文件合规性', '分析竞争对手情况']
},
service: {
emoji: '⚡',
description: '我是泰豪小电智能客服,可以帮您解答电力相关问题、查询用电信息、办理业务咨询。',
placeholder: '请描述您的用电问题或业务需求...',
quickActions: ['查询本月用电量', '咨询电费账单', '报修故障', '了解优惠政策']
},
default: {
emoji: '🤖',
description: '我是您的AI智能助手有什么可以帮助您的',
placeholder: '请输入您的问题...',
quickActions: ['介绍一下你的功能', '帮我写一段文案', '回答一个问题', '帮我分析数据']
}
}
const currentConfig = computed(() => agentConfig[agentId.value] || agentConfig.default)
const agentEmoji = computed(() => currentConfig.value.emoji)
const agentDescription = computed(() => currentConfig.value.description)
const inputPlaceholder = computed(() => currentConfig.value.placeholder)
const quickActions = computed(() => currentConfig.value.quickActions)
const goBack = () => {
router.push('/apps')
}
const scrollToBottom = async () => {
await nextTick()
if (chatContentRef.value) {
chatContentRef.value.scrollTop = chatContentRef.value.scrollHeight
}
}
const handleQuickAction = (action) => {
inputText.value = action
handleSend()
}
const generateResponse = (input) => {
const responses = {
xiaohongshu: `好的,我来帮你生成一篇小红书文案!\n\n📝 **${input}**\n\n---\n\n姐妹们今天必须给你们安利这个宝藏好物\n\n用了一周真的爱不释手效果绝绝子\n\n💫 亮点:\n• 性价比超高\n• 效果立竿见影\n• 回购无限次\n\n🏷 #好物分享 #真实测评 #平价好物 #必入清单\n\n---\n\n这篇文案你觉得怎么样需要我调整风格或者添加更多内容吗`,
contract: `我已经分析了您的需求,以下是我的建议:\n\n📋 **合同审核要点**\n\n1. **主体资格** - 需确认双方签约主体的合法性\n2. **权利义务** - 条款表述需更加明确\n3. **违约责任** - 建议增加违约金上限条款\n4. **争议解决** - 建议约定明确的管辖法院\n\n⚠ **风险提示**\n• 付款条件需细化\n• 交付标准需量化\n• 保密条款期限建议延长\n\n需要我针对某个条款详细解读吗`,
video: `好的!我来为您创作短视频脚本:\n\n🎬 **${input}**\n\n---\n\n**【开场 0-3秒】**\n钩子一句话抓住注意力\n\n**【正文 3-50秒】**\n• 痛点引入\n• 解决方案\n• 效果展示\n\n**【结尾 50-60秒】**\n引导互动 + 关注提示\n\n---\n\n🔥 推荐话题:\n#热门挑战 #干货分享 #涨知识\n\n需要我把脚本内容写得更详细吗`,
bidding: `收到您的招标需求:"${input}"\n\n📋 **招标文件分析**\n\n我已为您解读关键要点\n\n**1. 资质要求**\n• 企业资质等级要求\n• 业绩证明材料\n• 人员配置要求\n\n**2. 技术要求**\n• 技术方案要点\n• 实施计划安排\n• 质量保证措施\n\n**3. 商务要求**\n• 报价构成说明\n• 付款方式条款\n• 履约保证金\n\n⚠ **注意事项**\n• 投标截止时间\n• 必须响应的条款\n• 否决性条款\n\n需要我帮您生成投标技术方案吗`,
service: `您好!感谢使用泰豪小电智能客服 ⚡\n\n关于您的问题"${input}"\n\n📊 **查询结果**\n\n我已为您查询到相关信息\n\n**用电账户信息**\n• 户号3201****8856\n• 用户类型:居民用电\n• 电价0.52元/度\n\n**本月用电情况**\n• 当前读数2856度\n• 本月用电186度\n• 预计电费96.72元\n\n💡 **温馨提示**\n• 您可以通过微信公众号缴费\n• 峰谷电价可节省约15%电费\n• 如有故障可拨打24小时热线\n\n还有其他问题需要帮助吗`,
default: `收到您的问题:"${input}"\n\n我来为您详细解答\n\n这是一个很好的问题根据我的分析...\n\n如果您还有其他问题随时可以问我`
}
return responses[agentId.value] || responses.default
}
const handleSend = async () => {
const text = inputText.value.trim()
if (!text || isLoading.value) return
messages.value.push({
id: Date.now().toString(),
content: text,
role: 'user'
})
inputText.value = ''
await scrollToBottom()
isLoading.value = true
// Simulate API delay
setTimeout(async () => {
messages.value.push({
id: Date.now().toString() + '_ai',
content: generateResponse(text),
role: 'assistant'
})
isLoading.value = false
await scrollToBottom()
}, 1000)
}
</script>
<style lang="scss" scoped>
.agent-chat-view {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.chat-header {
padding: 12px 24px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
.header-left {
display: flex;
align-items: center;
gap: 16px;
.agent-info {
display: flex;
flex-direction: column;
.agent-name {
font-size: 16px;
font-weight: 600;
color: #1f2937;
}
.agent-status {
font-size: 12px;
color: #10b981;
}
}
}
}
.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;
.ai-avatar {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
.avatar-emoji {
font-size: 40px;
}
}
.welcome-title {
font-size: 22px;
font-weight: 600;
color: #1f2937;
margin-bottom: 12px;
}
.welcome-text {
color: #6b7280;
font-size: 14px;
text-align: center;
max-width: 500px;
margin-bottom: 32px;
line-height: 1.6;
}
}
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
max-width: 600px;
.action-item {
padding: 10px 20px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
border-radius: 20px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #7c3aed;
border-color: #7c3aed;
color: #fff;
}
}
}
.messages-container {
display: flex;
flex-direction: column;
gap: 24px;
.message {
display: flex;
gap: 12px;
&.user {
flex-direction: row-reverse;
.message-content .message-text {
background: #7c3aed;
color: #fff;
}
}
.message-avatar {
flex-shrink: 0;
.ai-avatar-small {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.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 {
max-width: 70%;
.message-text {
padding: 14px 18px;
background: #f3f4f6;
border-radius: 16px;
color: #1f2937;
line-height: 1.7;
white-space: pre-wrap;
}
}
}
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 16px;
background: #f3f4f6;
border-radius: 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 {
display: flex;
align-items: center;
gap: 12px;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 12px 16px;
textarea {
flex: 1;
border: none;
background: transparent;
resize: none;
outline: none;
font-size: 14px;
color: #1f2937;
min-height: 24px;
max-height: 120px;
&::placeholder {
color: #9ca3af;
}
}
.send-btn {
width: 40px;
height: 40px;
background: #7c3aed;
border: none;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
cursor: pointer;
transition: all 0.2s;
&:hover:not(:disabled) {
background: #5b21b6;
}
&:disabled {
background: #d1d5db;
cursor: not-allowed;
}
}
}
}
</style>

View File

@@ -0,0 +1,473 @@
<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="showCreateDialog = true">
<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">
<div
v-for="agent in filteredAgents"
:key="agent.id"
class="agent-card"
@click="handleAgentClick(agent)"
>
<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>
</div>
<!-- 新增智能体对话框 -->
<el-dialog
v-model="showCreateDialog"
title="新增智能体"
width="520px"
:close-on-click-modal="false"
>
<el-form :model="newAgent" label-width="80px" label-position="top">
<el-form-item label="智能体图片">
<div class="upload-area" @click="triggerUpload">
<input
type="file"
ref="fileInput"
accept="image/*"
@change="handleFileChange"
style="display: none"
/>
<div v-if="newAgent.imageUrl" class="preview-image">
<img :src="newAgent.imageUrl" alt="preview" />
<div class="change-tip">点击更换</div>
</div>
<div v-else class="upload-placeholder">
<el-icon class="upload-icon"><Plus /></el-icon>
<span>上传图片</span>
</div>
</div>
</el-form-item>
<el-form-item label="智能体名称">
<el-input v-model="newAgent.name" placeholder="请输入智能体名称" />
</el-form-item>
<el-form-item label="智能体介绍">
<el-input
v-model="newAgent.description"
type="textarea"
:rows="3"
placeholder="请输入智能体功能介绍"
/>
</el-form-item>
<el-form-item label="API链接">
<el-input v-model="newAgent.apiUrl" placeholder="请输入API接口地址" />
</el-form-item>
<el-form-item label="分类">
<el-select v-model="newAgent.category" placeholder="请选择分类" style="width: 100%">
<el-option label="内容创作" value="content" />
<el-option label="办公助手" value="office" />
<el-option label="业务助手" value="business" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button type="primary" @click="handleCreateAgent">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script 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 { useAgentStore } from '@/stores/agent'
const router = useRouter()
const agentStore = useAgentStore()
const searchText = ref('')
const activeCategory = ref('all')
const categories = [
{ key: 'all', label: '全部' },
{ key: 'content', label: '内容创作' },
{ key: 'office', label: '办公助手' },
{ key: 'business', label: '业务助手' }
]
const agents = ref([
{
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) => {
// 先设置当前智能体
agentStore.setCurrentAgent(agent.id)
router.push({
path: '/',
query: { agentId: agent.id }
})
}
// 新增智能体相关
const showCreateDialog = ref(false)
const fileInput = ref(null)
const newAgent = ref({
name: '',
description: '',
apiUrl: '',
category: '',
imageUrl: ''
})
const triggerUpload = () => {
fileInput.value?.click()
}
const handleFileChange = (e) => {
const file = e.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (event) => {
newAgent.value.imageUrl = event.target.result
}
reader.readAsDataURL(file)
}
}
const handleCreateAgent = () => {
if (!newAgent.value.name) {
ElMessage.warning('请输入智能体名称')
return
}
if (!newAgent.value.description) {
ElMessage.warning('请输入智能体介绍')
return
}
// 生成随机颜色
const colors = ['#7c3aed', '#10b981', '#f59e0b', '#3b82f6', '#ef4444', '#6366f1', '#14b8a6']
const randomColor = colors[Math.floor(Math.random() * colors.length)]
// 添加新智能体
agents.value.unshift({
id: 'custom_' + Date.now(),
name: newAgent.value.name,
description: newAgent.value.description,
icon: newAgent.value.imageUrl ? '' : '🤖',
imageUrl: newAgent.value.imageUrl,
color: randomColor,
category: newAgent.value.category || 'office',
apiUrl: newAgent.value.apiUrl,
usage: 0
})
// 重置表单
newAgent.value = {
name: '',
description: '',
apiUrl: '',
category: '',
imageUrl: ''
}
showCreateDialog.value = false
ElMessage.success('智能体创建成功')
}
</script>
<style lang="scss" scoped>
.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;
.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;
}
.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;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
.usage-count {
font-size: 12px;
color: #9ca3af;
}
}
}
.upload-area {
width: 120px;
height: 120px;
border: 2px dashed #d1d5db;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
&:hover {
border-color: #7c3aed;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: #9ca3af;
.upload-icon {
font-size: 28px;
}
span {
font-size: 13px;
}
}
.preview-image {
width: 100%;
height: 100%;
position: relative;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.change-tip {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: #fff;
text-align: center;
font-size: 12px;
padding: 4px 0;
opacity: 0;
transition: opacity 0.2s;
}
&:hover .change-tip {
opacity: 1;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,899 @@
<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">
<div class="ai-avatar">
<div class="avatar-icon" :style="{ background: currentAgent.color }">
{{ currentAgent.icon }}
</div>
</div>
<p class="welcome-text">
{{ currentAgent.description }}
</p>
<h2 class="welcome-title">{{ welcomeTitle }}</h2>
<!-- Suggestion Cards -->
<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: currentAgent.color }">
<component :is="cardIcons[index % cardIcons.length]" />
</div>
<p class="card-text">{{ suggestion }}</p>
</div>
</div>
</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 setup>
import { ref, onMounted, nextTick, watch, computed } from 'vue'
import {
ArrowDown,
Paperclip,
Star,
Picture,
MoreFilled,
CameraFilled,
Microphone,
Promotion,
Mute,
OfficeBuilding,
Warning,
Cloudy,
Plus,
Fold,
Expand,
ChatDotRound,
Delete
} from '@element-plus/icons-vue'
import { useRoute } from 'vue-router'
import { useChatStore } from '@/stores/chat'
import { useAgentStore } from '@/stores/agent'
const route = useRoute()
const chatStore = useChatStore()
const agentStore = useAgentStore()
// 智能体相关
const agents = computed(() => agentStore.agents)
const currentAgent = computed(() => agentStore.currentAgent)
const inputText = ref('')
const isLoading = ref(false)
const chatContentRef = ref(null)
const textareaRef = ref(null)
const sidebarCollapsed = ref(false)
const currentConversationId = ref(null)
// 对话历史记录
const conversations = ref([
{ 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 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 messages = ref([])
// 各智能体的建议内容
const agentSuggestions = {
default: [
'城市生命线关键设施有哪些?',
'消防安全隐患常见问题以及处理措施有哪些?',
'如何平衡排水能力和生态环境保护?'
],
xiaohongshu: [
'帮我写一篇关于美食探店的小红书文案',
'生成一篇旅行打卡的种草笔记',
'写一篇护肤心得分享文案'
],
contract: [
'帮我审核这份合同的风险点',
'分析合同中的关键条款',
'生成一份标准服务合同模板'
],
video: [
'帮我写一个产品介绍短视频脚本',
'生成一个美食探店的视频文案',
'写一个知识科普类短视频脚本'
],
email: [
'帮我写一封商务合作邀请邮件',
'生成一封会议通知邮件',
'写一封工作周报汇报邮件'
],
translate: [
'将这段中文翻译成英文',
'帮我翻译这份技术文档',
'将这段日文翻译成中文'
]
}
// 各智能体的欢迎标题
const agentWelcomeTitles = {
default: '今天需要我帮你做点什么吗?',
xiaohongshu: '想要创作什么样的爆款文案?',
contract: '需要我帮你处理什么合同?',
video: '想要创作什么样的短视频?',
email: '需要我帮你写什么邮件?',
translate: '需要翻译什么内容?'
}
// 当前智能体的建议
const currentSuggestions = computed(() => {
return agentSuggestions[currentAgent.value.id] || agentSuggestions.default
})
// 当前欢迎标题
const welcomeTitle = computed(() => {
return agentWelcomeTitles[currentAgent.value.id] || agentWelcomeTitles.default
})
const cardIcons = [OfficeBuilding, Warning, Cloudy]
const formatTime = (timestamp) => {
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 handleSuggestionClick = async (suggestion) => {
inputText.value = suggestion
await handleSend()
}
// 新建对话
const handleNewChat = () => {
const newConv = {
id: Date.now(),
title: '新对话',
date: new Date(),
messages: []
}
conversations.value.unshift(newConv)
currentConversationId.value = newConv.id
messages.value = []
}
// 选择对话
const selectConversation = (conv) => {
currentConversationId.value = conv.id
messages.value = conv.messages || []
}
// 删除对话
const deleteConversation = (id) => {
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) => {
agentStore.setCurrentAgent(agentId)
// 切换智能体时清空对话
messages.value = []
currentConversationId.value = null
}
const handleSend = async () => {
const text = inputText.value.trim()
if (!text || isLoading.value) return
// Add user message
const userMessage = {
id: Date.now().toString(),
content: text,
role: 'user',
timestamp: new Date().toISOString()
}
messages.value.push(userMessage)
inputText.value = ''
await scrollToBottom()
// Show loading
isLoading.value = true
try {
const response = await chatStore.sendMessage(text)
// Add assistant message
const assistantMessage = {
id: response.id || Date.now().toString() + '_ai',
content: response.content,
role: 'assistant',
timestamp: response.timestamp || new Date().toISOString()
}
messages.value.push(assistantMessage)
} catch (error) {
console.error('Send message error:', error)
// Add error message
messages.value.push({
id: Date.now().toString() + '_error',
content: '抱歉,发送失败,请稍后重试。',
role: 'assistant',
timestamp: new Date().toISOString()
})
} finally {
isLoading.value = false
await scrollToBottom()
}
}
onMounted(async () => {
// 处理路由参数中的智能体ID
const agentId = route.query.agentId
if (agentId) {
agentStore.setCurrentAgent(agentId)
}
try {
const data = await chatStore.getSuggestions()
if (data && data.length > 0) {
suggestions.value = data
}
} catch (error) {
console.log('Using default suggestions')
}
})
// 监听路由变化
watch(() => route.query.agentId, (newAgentId) => {
if (newAgentId) {
agentStore.setCurrentAgent(newAgentId)
messages.value = []
}
})
</script>
<style lang="scss" scoped>
.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;
.ai-avatar {
width: 88px;
height: 88px;
margin-bottom: 24px;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 16px;
object-fit: contain;
background: #f3f4f6;
padding: 8px;
}
.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;
}
.welcome-title {
font-size: 24px;
font-weight: 600;
color: #1f2937;
margin-bottom: 40px;
}
}
.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;
}
}
}
.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;
&: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;
&:hover:not(:disabled) {
background: #5b21b6;
}
&:disabled {
background: #d1d5db;
cursor: not-allowed;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,179 @@
<template>
<div class="emergency-view">
<header class="page-header">
<h1>应急预案</h1>
<p>城市生命线应急预案管理支持智能生成和编辑</p>
</header>
<div class="action-bar">
<el-button type="primary">
<el-icon><Plus /></el-icon>
新建预案
</el-button>
<el-button>
<el-icon><MagicStick /></el-icon>
AI生成
</el-button>
</div>
<div class="plan-grid">
<div v-for="plan in plans" :key="plan.id" class="plan-card">
<div class="card-header">
<div class="card-icon" :style="{ background: plan.color }">
<el-icon><component :is="plan.icon" /></el-icon>
</div>
<el-tag size="small" :type="plan.statusType">{{ plan.status }}</el-tag>
</div>
<h3>{{ plan.title }}</h3>
<p>{{ plan.description }}</p>
<div class="card-footer">
<span class="update-time">更新于 {{ plan.updateTime }}</span>
<div class="card-actions">
<el-button type="primary" link size="small">编辑</el-button>
<el-button type="primary" link size="small">下载</el-button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Plus, MagicStick, Warning, Cloudy, Lightning, SetUp } from '@element-plus/icons-vue'
const plans = ref([
{
id: 1,
title: '供水管网爆管应急预案',
description: '针对供水管网突发爆管事故的应急处置流程和措施',
icon: 'Cloudy',
color: '#3b82f6',
status: '已发布',
statusType: 'success',
updateTime: '2024-01-15'
},
{
id: 2,
title: '燃气泄漏应急预案',
description: '燃气管道泄漏事故的紧急响应和处置方案',
icon: 'Warning',
color: '#ef4444',
status: '已发布',
statusType: 'success',
updateTime: '2024-01-10'
},
{
id: 3,
title: '电力故障应急预案',
description: '大面积停电事故的应急响应和恢复方案',
icon: 'Lightning',
color: '#f59e0b',
status: '草稿',
statusType: 'info',
updateTime: '2024-01-18'
},
{
id: 4,
title: '城市内涝应急预案',
description: '暴雨导致城市内涝的预防和应急处置措施',
icon: 'SetUp',
color: '#10b981',
status: '审核中',
statusType: 'warning',
updateTime: '2024-01-20'
}
])
</script>
<style lang="scss" scoped>
.emergency-view {
padding: 32px 48px;
height: 100%;
overflow-y: auto;
}
.page-header {
margin-bottom: 24px;
h1 {
font-size: 24px;
color: #1f2937;
margin-bottom: 8px;
}
p {
color: #6b7280;
font-size: 14px;
}
}
.action-bar {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
.plan-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
.plan-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
transition: all 0.2s;
&:hover {
border-color: #7c3aed;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
.card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 24px;
}
}
h3 {
font-size: 16px;
color: #1f2937;
margin-bottom: 8px;
}
p {
font-size: 13px;
color: #6b7280;
margin-bottom: 16px;
line-height: 1.5;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 16px;
border-top: 1px solid #f3f4f6;
.update-time {
font-size: 12px;
color: #9ca3af;
}
}
}
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="hazard-view">
<header class="page-header">
<h1>隐患识别</h1>
<p>智能识别城市生命线潜在安全隐患提供预警和处置建议</p>
</header>
<div class="stats-section">
<div class="stat-card" v-for="stat in stats" :key="stat.label">
<div class="stat-value" :style="{ color: stat.color }">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
<div class="hazard-list">
<div class="list-header">
<h2>隐患列表</h2>
<el-button type="primary" size="small">
<el-icon><Plus /></el-icon>
上报隐患
</el-button>
</div>
<div class="hazard-table">
<div class="table-header">
<span class="col-type">类型</span>
<span class="col-desc">描述</span>
<span class="col-location">位置</span>
<span class="col-level">等级</span>
<span class="col-status">状态</span>
<span class="col-action">操作</span>
</div>
<div v-for="item in hazardList" :key="item.id" class="table-row">
<span class="col-type">{{ item.type }}</span>
<span class="col-desc">{{ item.description }}</span>
<span class="col-location">{{ item.location }}</span>
<span class="col-level">
<el-tag :type="getLevelType(item.level)" size="small">{{ item.level }}</el-tag>
</span>
<span class="col-status">
<el-tag :type="getStatusType(item.status)" size="small">{{ item.status }}</el-tag>
</span>
<span class="col-action">
<el-button type="primary" link size="small">查看</el-button>
<el-button type="primary" link size="small">处理</el-button>
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
const stats = [
{ label: '待处理隐患', value: 12, color: '#ef4444' },
{ label: '处理中', value: 8, color: '#f59e0b' },
{ label: '本月已处理', value: 45, color: '#10b981' },
{ label: '累计识别', value: 328, color: '#7c3aed' }
]
const hazardList = ref([
{ id: 1, type: '供水管网', description: '管道老化存在渗漏风险', location: '红谷滩区丰和大道', level: '高', status: '待处理' },
{ id: 2, type: '燃气管道', description: '阀门锈蚀需要更换', location: '东湖区八一大道', level: '中', status: '处理中' },
{ id: 3, type: '电力设施', description: '变压器负荷过高', location: '西湖区朝阳路', level: '高', status: '待处理' },
{ id: 4, type: '排水系统', description: '雨水井堵塞', location: '青山湖区北京路', level: '低', status: '已处理' }
])
const getLevelType = (level) => {
const map = { '高': 'danger', '中': 'warning', '低': 'info' }
return map[level] || 'info'
}
const getStatusType = (status) => {
const map = { '待处理': 'danger', '处理中': 'warning', '已处理': 'success' }
return map[status] || 'info'
}
</script>
<style lang="scss" scoped>
.hazard-view {
padding: 32px 48px;
height: 100%;
overflow-y: auto;
}
.page-header {
margin-bottom: 32px;
h1 {
font-size: 24px;
color: #1f2937;
margin-bottom: 8px;
}
p {
color: #6b7280;
font-size: 14px;
}
}
.stats-section {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 32px;
.stat-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
text-align: center;
.stat-value {
font-size: 32px;
font-weight: 600;
margin-bottom: 8px;
}
.stat-label {
color: #6b7280;
font-size: 14px;
}
}
}
.hazard-list {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
font-size: 18px;
color: #1f2937;
}
}
}
.hazard-table {
.table-header, .table-row {
display: grid;
grid-template-columns: 100px 1fr 180px 80px 80px 120px;
gap: 16px;
padding: 12px 16px;
align-items: center;
}
.table-header {
background: #f9fafb;
border-radius: 8px;
font-weight: 500;
color: #6b7280;
font-size: 13px;
}
.table-row {
border-bottom: 1px solid #f3f4f6;
font-size: 14px;
color: #374151;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f9fafb;
}
}
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<div class="knowledge-view">
<header class="page-header">
<h1>知识库</h1>
<p>城市生命线领域知识库包含政策法规技术标准案例分析等</p>
</header>
<div class="search-section">
<el-input
v-model="searchText"
placeholder="搜索知识库..."
size="large"
:prefix-icon="Search"
/>
</div>
<div class="knowledge-categories">
<div
v-for="category in categories"
:key="category.id"
class="category-card"
>
<div class="card-icon" :style="{ background: category.color }">
<el-icon><component :is="category.icon" /></el-icon>
</div>
<div class="card-content">
<h3>{{ category.name }}</h3>
<p>{{ category.description }}</p>
<span class="doc-count">{{ category.count }} 篇文档</span>
</div>
</div>
</div>
<div class="recent-docs">
<h2>最近浏览</h2>
<div class="doc-list">
<div v-for="doc in recentDocs" :key="doc.id" class="doc-item">
<el-icon><Document /></el-icon>
<div class="doc-info">
<span class="doc-title">{{ doc.title }}</span>
<span class="doc-time">{{ doc.time }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {
Search,
Document,
Reading,
Files,
DataAnalysis,
Warning
} from '@element-plus/icons-vue'
const searchText = ref('')
const categories = [
{ id: 1, name: '政策法规', description: '国家及地方相关政策法规文件', count: 128, color: '#7c3aed', icon: 'Reading' },
{ id: 2, name: '技术标准', description: '行业技术标准与规范', count: 86, color: '#10b981', icon: 'Files' },
{ id: 3, name: '案例分析', description: '典型案例分析与经验总结', count: 54, color: '#f59e0b', icon: 'DataAnalysis' },
{ id: 4, name: '应急管理', description: '应急预案与处置流程', count: 42, color: '#ef4444', icon: 'Warning' }
]
const recentDocs = [
{ id: 1, title: '城市供水管网安全运行管理规范', time: '2小时前' },
{ id: 2, title: '燃气管道安全检测技术标准', time: '昨天' },
{ id: 3, title: '城市内涝应急处置预案模板', time: '3天前' }
]
</script>
<style lang="scss" scoped>
.knowledge-view {
padding: 32px 48px;
height: 100%;
overflow-y: auto;
}
.page-header {
margin-bottom: 32px;
h1 {
font-size: 24px;
color: #1f2937;
margin-bottom: 8px;
}
p {
color: #6b7280;
font-size: 14px;
}
}
.search-section {
margin-bottom: 32px;
max-width: 600px;
}
.knowledge-categories {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 40px;
.category-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 24px;
display: flex;
gap: 16px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #7c3aed;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 24px;
flex-shrink: 0;
}
.card-content {
h3 {
font-size: 16px;
color: #1f2937;
margin-bottom: 4px;
}
p {
font-size: 13px;
color: #6b7280;
margin-bottom: 8px;
}
.doc-count {
font-size: 12px;
color: #7c3aed;
}
}
}
}
.recent-docs {
h2 {
font-size: 18px;
color: #1f2937;
margin-bottom: 16px;
}
.doc-list {
.doc-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
&:hover {
background: #f9fafb;
}
.el-icon {
color: #6b7280;
font-size: 20px;
}
.doc-info {
display: flex;
justify-content: space-between;
flex: 1;
.doc-title {
color: #374151;
}
.doc-time {
color: #9ca3af;
font-size: 12px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<div class="profile-view">
<div class="profile-header">
<h1>个人中心</h1>
<p class="subtitle">管理您的个人信息和账户设置</p>
</div>
<div class="profile-content">
<el-tabs v-model="activeTab">
<el-tab-pane label="基本信息" name="basic">
<el-card>
<div class="profile-section">
<div class="avatar-section">
<el-avatar :size="100" src="/avatar.svg">李志鹏</el-avatar>
<el-button type="primary" size="small" style="margin-top: 16px;">
<el-icon><Upload /></el-icon>
更换头像
</el-button>
</div>
<el-form :model="profileForm" label-width="100px" style="max-width: 600px;">
<el-form-item label="姓名">
<el-input v-model="profileForm.name" />
</el-form-item>
<el-form-item label="工号">
<el-input v-model="profileForm.employeeId" disabled />
</el-form-item>
<el-form-item label="部门">
<el-input v-model="profileForm.department" />
</el-form-item>
<el-form-item label="职位">
<el-input v-model="profileForm.position" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="profileForm.phone" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="profileForm.email" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveProfile">保存修改</el-button>
<el-button @click="resetProfile">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="账户安全" name="security">
<el-card>
<div class="security-section">
<div class="security-item">
<div class="security-info">
<h3>登录密码</h3>
<p>定期更换密码可以提高账户安全性</p>
</div>
<el-button type="primary" link @click="showPasswordDialog = true">修改密码</el-button>
</div>
<el-divider />
<div class="security-item">
<div class="security-info">
<h3>双因素认证</h3>
<p>开启后登录需要验证码更加安全</p>
</div>
<el-switch v-model="twoFactorEnabled" />
</div>
<el-divider />
<div class="security-item">
<div class="security-info">
<h3>登录设备管理</h3>
<p>查看和管理您的登录设备</p>
</div>
<el-button type="primary" link>查看设备</el-button>
</div>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="通知设置" name="notification">
<el-card>
<div class="notification-section">
<el-form label-width="150px">
<el-form-item label="邮件通知">
<el-switch v-model="notificationSettings.email" />
<span class="setting-desc">接收系统邮件通知</span>
</el-form-item>
<el-form-item label="短信通知">
<el-switch v-model="notificationSettings.sms" />
<span class="setting-desc">接收重要事项短信提醒</span>
</el-form-item>
<el-form-item label="工单提醒">
<el-switch v-model="notificationSettings.ticket" />
<span class="setting-desc">新工单分配时提醒</span>
</el-form-item>
<el-form-item label="系统公告">
<el-switch v-model="notificationSettings.announcement" />
<span class="setting-desc">接收系统公告推送</span>
</el-form-item>
</el-form>
</div>
</el-card>
</el-tab-pane>
<el-tab-pane label="操作日志" name="logs">
<el-card>
<el-table :data="operationLogs" style="width: 100%">
<el-table-column prop="time" label="时间" width="180" />
<el-table-column prop="action" label="操作" width="200" />
<el-table-column prop="ip" label="IP地址" width="150" />
<el-table-column prop="device" label="设备" />
</el-table>
</el-card>
</el-tab-pane>
</el-tabs>
</div>
<!-- 修改密码对话框 -->
<el-dialog v-model="showPasswordDialog" title="修改密码" width="400px">
<el-form :model="passwordForm" label-width="100px">
<el-form-item label="原密码">
<el-input v-model="passwordForm.oldPassword" type="password" show-password />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="passwordForm.newPassword" type="password" show-password />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="passwordForm.confirmPassword" type="password" show-password />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showPasswordDialog = false">取消</el-button>
<el-button type="primary" @click="changePassword">确认修改</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Upload } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const activeTab = ref('basic')
const profileForm = ref({
name: '李志鹏',
employeeId: 'TH20230001',
department: '技术研发部',
position: '高级工程师',
phone: '13800138000',
email: 'lizhipeng@taihao.com'
})
const twoFactorEnabled = ref(false)
const notificationSettings = ref({
email: true,
sms: true,
ticket: true,
announcement: false
})
const operationLogs = ref([
{ time: '2024-12-06 18:30:00', action: '登录系统', ip: '192.168.1.100', device: 'Windows 11 / Chrome' },
{ time: '2024-12-06 14:20:00', action: '修改个人信息', ip: '192.168.1.100', device: 'Windows 11 / Chrome' },
{ time: '2024-12-05 09:15:00', action: '登录系统', ip: '192.168.1.100', device: 'Windows 11 / Chrome' },
{ time: '2024-12-04 16:45:00', action: '创建工单', ip: '192.168.1.100', device: 'Windows 11 / Chrome' }
])
const showPasswordDialog = ref(false)
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const saveProfile = () => {
ElMessage.success('个人信息保存成功!')
}
const resetProfile = () => {
ElMessage.info('已重置为原始信息')
}
const changePassword = () => {
if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
ElMessage.error('两次输入的密码不一致!')
return
}
ElMessage.success('密码修改成功!')
showPasswordDialog.value = false
passwordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: ''
}
}
</script>
<style lang="scss" scoped>
.profile-view {
height: 100vh;
background: #f5f7fa;
overflow-y: auto;
}
.profile-header {
padding: 32px 40px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
h1 {
font-size: 28px;
margin: 0 0 8px 0;
color: #303133;
}
.subtitle {
font-size: 14px;
color: #909399;
margin: 0;
}
}
.profile-content {
padding: 24px 40px;
max-width: 1200px;
}
.profile-section {
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32px;
padding: 24px;
background: #f5f7fa;
border-radius: 8px;
}
}
.security-section {
.security-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
.security-info {
h3 {
font-size: 16px;
margin: 0 0 8px 0;
color: #303133;
}
p {
font-size: 14px;
color: #909399;
margin: 0;
}
}
}
}
.notification-section {
.setting-desc {
margin-left: 12px;
font-size: 13px;
color: #909399;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
<template>
<div class="workflow-view">
<div class="workflow-header">
<div class="header-left">
<h1>智能体编排</h1>
<p class="subtitle">可视化工作流编排平台拖拽节点构建智能工作流</p>
</div>
<div class="header-right">
<el-button type="primary" @click="saveWorkflow">
<el-icon><Check /></el-icon>
保存
</el-button>
<el-button @click="exportWorkflow">
<el-icon><Download /></el-icon>
导出
</el-button>
</div>
</div>
<div class="workflow-container">
<WorkflowEditor />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Check, Download } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import WorkflowEditor from '@/components/workflow/WorkflowEditor.vue'
const saveWorkflow = () => {
ElMessage.success('工作流保存成功')
}
const exportWorkflow = () => {
ElMessage.info('工作流导出功能开发中')
}
</script>
<style lang="scss" scoped>
.workflow-view {
height: 100%;
display: flex;
flex-direction: column;
background: #f5f7fa;
}
.workflow-header {
padding: 16px 24px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
.header-left {
h1 {
font-size: 20px;
margin: 0 0 4px 0;
color: #303133;
}
.subtitle {
font-size: 13px;
color: #909399;
margin: 0;
}
}
.header-right {
display: flex;
gap: 12px;
.el-button {
display: flex;
align-items: center;
gap: 6px;
}
}
}
.workflow-container {
flex: 1;
overflow: hidden;
}
</style>