原型
This commit is contained in:
4051
江西城市生命线-可交互原型/frontend/src/views/AdminView.vue
Normal file
4051
江西城市生命线-可交互原型/frontend/src/views/AdminView.vue
Normal file
File diff suppressed because it is too large
Load Diff
453
江西城市生命线-可交互原型/frontend/src/views/AgentChatView.vue
Normal file
453
江西城市生命线-可交互原型/frontend/src/views/AgentChatView.vue
Normal 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>
|
||||
473
江西城市生命线-可交互原型/frontend/src/views/AppsView.vue
Normal file
473
江西城市生命线-可交互原型/frontend/src/views/AppsView.vue
Normal 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>
|
||||
18847
江西城市生命线-可交互原型/frontend/src/views/BiddingView.vue
Normal file
18847
江西城市生命线-可交互原型/frontend/src/views/BiddingView.vue
Normal file
File diff suppressed because it is too large
Load Diff
899
江西城市生命线-可交互原型/frontend/src/views/ChatView.vue
Normal file
899
江西城市生命线-可交互原型/frontend/src/views/ChatView.vue
Normal 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>
|
||||
179
江西城市生命线-可交互原型/frontend/src/views/EmergencyView.vue
Normal file
179
江西城市生命线-可交互原型/frontend/src/views/EmergencyView.vue
Normal 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>
|
||||
180
江西城市生命线-可交互原型/frontend/src/views/HazardView.vue
Normal file
180
江西城市生命线-可交互原型/frontend/src/views/HazardView.vue
Normal 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>
|
||||
202
江西城市生命线-可交互原型/frontend/src/views/KnowledgeView.vue
Normal file
202
江西城市生命线-可交互原型/frontend/src/views/KnowledgeView.vue
Normal 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>
|
||||
276
江西城市生命线-可交互原型/frontend/src/views/ProfileView.vue
Normal file
276
江西城市生命线-可交互原型/frontend/src/views/ProfileView.vue
Normal 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>
|
||||
2017
江西城市生命线-可交互原型/frontend/src/views/ServiceView.vue
Normal file
2017
江西城市生命线-可交互原型/frontend/src/views/ServiceView.vue
Normal file
File diff suppressed because it is too large
Load Diff
88
江西城市生命线-可交互原型/frontend/src/views/WorkflowView.vue
Normal file
88
江西城市生命线-可交互原型/frontend/src/views/WorkflowView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user