ai-manus/chat-client/src/views/chatweb/homePage/index.vue

1969 lines
49 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dashboard-page">
<!-- 主要内容区域 -->
<main class="dashboard-main">
<!-- 问题输入界面 -->
<section v-if="showQuestionInput" class="question-input-section">
<div class="question-container">
<!-- <h1 class="question-title">今天有什么问题?</h1> -->
<div class="chat-input-container">
<!-- 左侧头像/图标 -->
<div class="chat-avatar">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<!-- 中间输入框区域 -->
<div class="input-wrapper">
<textarea
v-model="questionText"
class="chat-input"
placeholder="发消息或输入..."
@keyup.enter="handleQuestionSubmit"
rows="1"
></textarea>
</div>
<!-- 右侧按钮组 -->
<div class="input-actions">
<button class="action-btn voice-btn" title="语音输入">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
<line x1="12" y1="19" x2="12" y2="23"/>
<line x1="8" y1="23" x2="16" y2="23"/>
</svg>
</button>
<button class="action-btn send-btn" title="发送" @click="handleQuestionSubmit">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 2L11 13"/>
<path d="M22 2L15 22L11 13L2 9L22 2Z"/>
</svg>
</button>
</div>
</div>
</div>
</section>
<!-- 思考过程界面 -->
<section v-if="isThinking" class="thinking-section">
<div class="thinking-container">
<div class="thinking-spinner">
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
</div>
<h2 class="thinking-text">{{ thinkingText }}</h2>
<div class="thinking-dots">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
</section>
<!-- 结果卡片界面 -->
<section v-if="showResults" class="modules-section animate-fade-in animate-delay-400">
<div class="modules-container" ref="modulesContainer">
<ModuleCard
v-for="(config, index) in moduleConfigs"
:key="config.id"
:config="config"
:stats="getModuleStats(config.id)"
:is-active="selectedModule === config.id"
:is-draggable="isDragEnabled"
:initial-position="moduleLayouts[config.id]?.position || { x: 0, y: 0 }"
:initial-size="moduleLayouts[config.id]?.size || { width: 280, height: 200 }"
:initial-question="config.id === 'inputCard' ? submittedQuestion : ''"
:task-list="config.id === 'inputCard' ? taskList : []"
:train-data="getTrainData(config.id)"
:mechanic-data="getMechanicData(config.id)"
:diagnostic-codes="getDiagnosticCodes(config.id)"
:treatment-methods="getTreatmentMethods(config.id)"
:file-list="getFileList(config.id)"
:rule-list="getRuleList(config.id)"
:precaution-list="getPrecautionList(config.id)"
:route-data="getRouteData(config.id)"
:fault-causes="getFaultCauses(config.id)"
:typical-cases="getTypicalCases(config.id)"
:class="[
`animate-slide-in-up animate-delay-${(index + 1) * 100}`,
'draggable-module-card'
]"
:style="getModuleStyle(config.id)"
@click="handleModuleClick"
@dblclick="handleModuleDoubleClick"
@position-change="handlePositionChange"
@size-change="handleSizeChange"
@open-rule-pdf="handleOpenRulePdf"
@open-case-pdf="handleOpenCasePdf"
/>
</div>
</section>
</main>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-overlay">
<div class="spinner"></div>
<p>正在加载数据...</p>
</div>
<!-- 通知提示 -->
<div
v-if="notification.show"
class="notification animate-slide-in-down"
:class="`notification--${notification.type}`"
>
<span>{{ notification.message }}</span>
</div>
<!-- 详情弹窗 -->
<DetailModal
v-model="showDetailModal"
:module-type="selectedModuleForDetail"
:data="selectedModuleForDetail ? dashboardStore.getModuleData(selectedModuleForDetail) : []"
@refresh="handleDetailRefresh"
/>
<!-- 规章制度 PDF 预览弹窗(页面级) -->
<el-dialog
v-model="rulePdfDialogVisible"
title="PDF预览"
width="85%"
class="rule-pdf-dialog"
:close-on-click-modal="false"
:append-to-body="true"
center
>
<div v-loading="rulePdfLoading" class="preview-content">
<VueOfficePdf
v-if="currentRulePdfUrl"
:src="currentRulePdfUrl"
@rendered="() => { rulePdfLoading = false }"
@error="() => { rulePdfLoading = false; ElMessage.error('PDF预览失败') }"
class="pdf-preview"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, reactive, watch } from 'vue';
import { useDashboardStore } from '@/store/modules/dashboard';
import { useAclStore } from '@/store/modules/acl';
import { MODULE_CONFIGS } from '@/constants/modules';
import ModuleCard from '@/components/dashboard/ModuleCard.vue';
import DetailModal from '@/components/dashboard/DetailModal.vue';
import type { ModuleConfig, ModuleType, SearchResult, StatusStats, Position, Size, RuleRegulation, TypicalCase } from '@/types/dashboard';
import { webSocketService } from '@/utils/websocket';
import { baseURL } from '@/config';
import { v4 as uuidv4 } from 'uuid';
import { getEmuTreatment } from '@/api/dashboard';
import { ElDialog, ElMessage } from 'element-plus';
import VueOfficePdf from '@vue-office/pdf';
import { sendChatMessage } from '@/api/chat';
// 使用store
const dashboardStore = useDashboardStore();
const aclStore = useAclStore();
// 响应式数据
const showDetailModal = ref(false);
const selectedModuleForDetail = ref<ModuleType | null>(null);
const isDragEnabled = ref(true); // 拖拽控制开关
const modulesContainer = ref<HTMLElement>();
// 问答界面状态管理
const showQuestionInput = ref(true); // 控制问题输入框显示
const isThinking = ref(false); // 控制思考过程显示
const showResults = ref(false); // 控制结果卡片显示
const questionText = ref(''); // 用户输入的问题
const thinkingText = ref('正在思考...'); // 思考过程文字
const submittedQuestion = ref(''); // 保存已提交的问题用于传递给AI对话卡片
// WebSocket 相关
const sysMessageId = ref(''); // 系统消息ID
// 任务列表相关 - 用于在AI助手卡片中显示加载进度
interface TaskItem {
id: string;
title: string;
status: 'pending' | 'loading' | 'completed' | 'error';
dataKey: string; // 对应的数据字段名
}
const taskList = ref<TaskItem[]>([]);
// 通知相关
const notification = ref({
show: false,
message: '',
type: 'success' as 'success' | 'error' | 'info'
});
let notificationTimer: ReturnType<typeof setTimeout> | null = null;
// 模块布局状态
interface ModuleLayout {
position: Position;
size: Size;
}
// 模块配置
const moduleConfigs = MODULE_CONFIGS;
// 默认布局配置
const getDefaultLayout = (): Record<string, ModuleLayout> => {
const defaultLayouts: Record<string, ModuleLayout> = {};
const cardWidth = 280;
const cardHeight = 200;
const gap = 24;
const cols = 4;
moduleConfigs.forEach((config, index) => {
const row = Math.floor(index / cols);
const col = index % cols;
// 应急处置建议模块占两个位置
const width = config.id === 'emergencyDisposal' ? cardWidth * 2 + gap : cardWidth;
defaultLayouts[config.id] = {
position: {
x: col * (cardWidth + gap),
y: row * (cardHeight + gap)
},
size: {
width,
height: cardHeight
}
};
});
return defaultLayouts;
};
// 模块布局状态
const initializeModuleLayouts = () => {
const savedLayouts = localStorage.getItem('moduleLayouts');
let layouts;
if (savedLayouts) {
layouts = JSON.parse(savedLayouts);
// 获取默认布局以补充缺失的模块
const defaultLayouts = getDefaultLayout();
// 检查并补充缺失的模块
let hasUpdates = false;
moduleConfigs.forEach(config => {
if (!layouts[config.id]) {
layouts[config.id] = defaultLayouts[config.id];
hasUpdates = true;
}
});
// 如果有更新保存到localStorage
if (hasUpdates) {
localStorage.setItem('moduleLayouts', JSON.stringify(layouts));
}
} else {
layouts = getDefaultLayout();
}
return layouts;
};
const moduleLayouts = reactive<Record<string, ModuleLayout>>(initializeModuleLayouts());
// 计算属性
const searchQuery = computed({
get: () => dashboardStore.searchQuery,
set: (value: string) => dashboardStore.updateSearchQuery(value)
});
const searchResults = computed(() => dashboardStore.searchResults);
const selectedModule = computed(() => dashboardStore.selectedModule);
const isLoading = computed(() => dashboardStore.isLoading);
const totalTrains = computed(() => dashboardStore.totalTrains);
const runningTrains = computed(() => dashboardStore.runningTrains);
const maintenanceTrains = computed(() => dashboardStore.maintenanceTrains);
const activeMechanics = computed(() => dashboardStore.activeMechanics);
/**
* 获取模块样式
* 使用 transform: translate3d() 启用 GPU 加速,避免 layout 重计算
*/
const getModuleStyle = (moduleId: string) => {
const layout = moduleLayouts[moduleId];
if (!layout) return {};
return {
position: 'absolute',
transform: `translate3d(${layout.position.x}px, ${layout.position.y}px, 0)`,
width: `${layout.size.width}px`,
height: `${layout.size.height}px`,
// 移除 transition 以避免拖拽时的冲突
// 在 ModuleCard 组件中会根据拖拽状态动态控制过渡效果
};
};
/**
* 保存布局到本地存储
*/
const saveLayout = () => {
localStorage.setItem('moduleLayouts', JSON.stringify(moduleLayouts));
};
// 节流保存布局避免频繁写入localStorage
let saveLayoutTimer: ReturnType<typeof setTimeout> | null = null;
const throttledSaveLayout = () => {
if (saveLayoutTimer) {
clearTimeout(saveLayoutTimer);
}
saveLayoutTimer = setTimeout(() => {
saveLayout();
}, 100); // 100ms节流
};
/**
* 处理位置变化
* 优化:减少不必要的对象创建和响应式更新
*/
const handlePositionChange = (moduleId: string, position: Position) => {
const layout = moduleLayouts[moduleId];
if (!layout) return;
// 检查位置是否真的改变了
if (layout.position.x === position.x && layout.position.y === position.y) {
return;
}
// 直接修改现有对象,避免创建新对象
layout.position.x = position.x;
layout.position.y = position.y;
// 节流保存
throttledSaveLayout();
};
/**
* 处理大小变化
* 优化:减少不必要的对象创建和响应式更新
*/
const handleSizeChange = (moduleId: string, size: Size) => {
const layout = moduleLayouts[moduleId];
if (!layout) return;
// 检查大小是否真的改变了
if (layout.size.width === size.width && layout.size.height === size.height) {
return;
}
// 直接修改现有对象,避免创建新对象
layout.size.width = size.width;
layout.size.height = size.height;
// 节流保存
throttledSaveLayout();
};
/**
* 获取模块统计数据
*/
const getModuleStats = (moduleType: ModuleType): StatusStats => {
const data = dashboardStore.getModuleData(moduleType);
// 根据不同模块类型计算统计数据
switch (moduleType) {
case 'trainInfo':
return {
total: data.length,
active: data.filter((item: any) => item.status === 'running').length,
inactive: data.filter((item: any) => item.status === 'standby').length,
warning: data.filter((item: any) => item.status === 'maintenance').length
};
case 'mechanicInfo':
return {
total: data.length,
active: data.filter((item: any) => item.status === 'working').length,
inactive: data.filter((item: any) => item.status === 'break').length,
warning: data.filter((item: any) => item.status === 'available').length
};
case 'faultInfo':
return {
total: data.length,
active: data.filter((item: any) => item.severity === 'low').length,
inactive: data.filter((item: any) => item.severity === 'medium').length,
warning: data.filter((item: any) => item.severity === 'high').length
};
default:
return {
total: data.length,
active: Math.floor(data.length * 0.8),
inactive: Math.floor(data.length * 0.1),
warning: Math.floor(data.length * 0.1)
};
}
};
/**
* 获取模块标题
*/
const getModuleTitle = (moduleType: ModuleType): string => {
const config = moduleConfigs.find(c => c.id === moduleType);
return config?.title || '未知模块';
};
/**
* 获取车组数据 - 只返回第一条数据给卡片显示
*/
const getTrainData = (moduleType: ModuleType) => {
if (moduleType === 'trainInfo') {
const trainData = dashboardStore.trainInfo;
return trainData.length > 0 ? trainData[0] : undefined;
}
return undefined;
};
/**
* 获取机械师数据 - 只返回第一条数据给卡片显示
*/
const getMechanicData = (moduleType: ModuleType) => {
if (moduleType === 'mechanicInfo' || moduleType === 'personnelInfo') {
const mechanicData = dashboardStore.mechanicInfo;
return mechanicData.length > 0 ? mechanicData[0] : undefined;
}
return undefined;
};
/**
* 获取诊断代码数据
*/
const getDiagnosticCodes = (moduleType: ModuleType) => {
if (moduleType === 'faultInfo') {
return dashboardStore.diagnosticCodes;
}
return [];
};
/**
* 获取处置方法数据
*/
const getTreatmentMethods = (moduleType: ModuleType) => {
if (moduleType === 'emergencyDisposal') {
return dashboardStore.treatmentMethods;
}
return [];
};
/**
* 获取文件列表数据
*/
const getFileList = (moduleType: ModuleType) => {
if (moduleType === 'typicalCase') {
return dashboardStore.standardFiles;
}
return [];
};
/**
* 获取规章制度数据
*/
const getRuleList = (moduleType: ModuleType) => {
if (moduleType === 'ruleRegulation') {
return dashboardStore.ruleRegulations;
}
return [];
};
/**
* 获取注意事项数据
*/
const getPrecautionList = (moduleType: ModuleType) => {
if (moduleType === 'precautions') {
return dashboardStore.precautions;
}
return [];
};
/**
* 获取交路信息数据 - 只返回第一条数据给卡片显示
*/
const getRouteData = (moduleType: ModuleType) => {
if (moduleType === 'routeInfo') {
const routeData = dashboardStore.routeInfo;
return routeData.length > 0 ? routeData[0] : undefined;
}
return undefined;
};
/**
* 获取故障原因数据
*/
const getFaultCauses = (moduleType: ModuleType) => {
if (moduleType === 'emergencyDisposal') {
return dashboardStore.getFaultCauses();
}
return [];
};
/**
* 获取典型案例数据
*/
const getTypicalCases = (moduleType: ModuleType) => {
if (moduleType === 'typicalCase') {
return dashboardStore.typicalCases;
}
return [];
};
/**
* 处理搜索输入
*/
const handleSearch = (value: string) => {
dashboardStore.updateSearchQuery(value);
};
/**
* 处理搜索结果点击
*/
const handleSearchResultClick = (result: SearchResult) => {
dashboardStore.selectModule(result.type);
dashboardStore.updateSearchQuery('');
};
/**
* 处理模块卡片点击
*/
const handleModuleClick = (config: ModuleConfig) => {
// 单击只负责选中模块
dashboardStore.selectModule(config.id);
};
/**
* 处理模块卡片双击
*/
const handleModuleDoubleClick = (config: ModuleConfig) => {
// 双击直接打开详情弹窗
selectedModuleForDetail.value = config.id;
showDetailModal.value = true;
};
/**
* 处理详情弹窗刷新
*/
const handleDetailRefresh = (moduleType: ModuleType) => {
dashboardStore.refreshModuleData(moduleType);
};
/**
* 生成随机的系统消息ID
*/
const generateSysMessageId = (): string => {
const fullUUID = uuidv4().replace(/-/g, ''); // 生成UUID并去除连字符
return fullUUID.substring(0, 12); // 截取前12位
};
/**
* 建立 WebSocket 连接
*/
const connectWebSocket = () => {
try {
const urlList = baseURL.split(':');
const wsUrl = `ws:${urlList[1]}:16750/api/webSocketServer/${sysMessageId.value}`;
console.log('正在建立 WebSocket 连接:', wsUrl);
webSocketService.connect(wsUrl);
// 监听 WebSocket 消息
webSocketService.on('message', handleWebSocketMessage);
webSocketService.on('connect', () => {
console.log('HomePage WebSocket 已连接, sysMessageId:', sysMessageId.value);
showNotification('WebSocket 连接成功', 'success');
});
webSocketService.on('error', (error: any) => {
console.error('HomePage WebSocket 错误:', error);
showNotification('WebSocket 连接失败', 'error');
});
} catch (error) {
console.error('建立 WebSocket 连接失败:', error);
showNotification('建立 WebSocket 连接失败', 'error');
}
};
/**
* 处理 WebSocket 消息
* 支持接收所有卡片模块的数据
*/
const handleWebSocketMessage = (data: string) => {
try {
console.log('收到 WebSocket 消息:', data);
const messageData = JSON.parse(data);
let hasUpdates = false;
const updatedModules: string[] = [];
// 处理车组信息
if (messageData.trainInfo && Array.isArray(messageData.trainInfo)) {
dashboardStore.updateTrainInfo(messageData.trainInfo);
updateTaskStatus('trainInfo', 'completed');
hasUpdates = true;
updatedModules.push('车组信息');
}
// 处理机械师/人员信息
if (messageData.mechanicInfo && Array.isArray(messageData.mechanicInfo)) {
dashboardStore.updateMechanicInfo(messageData.mechanicInfo);
updateTaskStatus('mechanicInfo', 'completed');
hasUpdates = true;
updatedModules.push('人员信息');
}
// 处理诊断代码/故障信息
if (messageData.diagnosticCodes && Array.isArray(messageData.diagnosticCodes)) {
dashboardStore.updateDiagnosticCodes(messageData.diagnosticCodes);
updateTaskStatus('diagnosticCodes', 'completed');
hasUpdates = true;
updatedModules.push('故障信息');
}
// 处理处置方法(应急处置建议)
if (messageData.treatmentMethod && Array.isArray(messageData.treatmentMethod)) {
dashboardStore.updateTreatmentMethods(messageData.treatmentMethod);
updateTaskStatus('treatmentMethod', 'completed');
hasUpdates = true;
updatedModules.push('应急处置建议');
}
// 处理故障原因
if (messageData.faultCause && Array.isArray(messageData.faultCause)) {
dashboardStore.updateFaultCauses(messageData.faultCause);
hasUpdates = true;
updatedModules.push('故障原因');
}
// 处理典型案例
if (messageData.typicalCases && Array.isArray(messageData.typicalCases)) {
dashboardStore.updateTypicalCases(messageData.typicalCases);
hasUpdates = true;
updatedModules.push('典型案例');
}
// 处理标准文件
if (messageData.standardFiles && Array.isArray(messageData.standardFiles)) {
dashboardStore.updateStandardFiles(messageData.standardFiles);
updateTaskStatus('standardFiles', 'completed');
hasUpdates = true;
updatedModules.push('标准文件');
}
// 处理规章制度
if (messageData.ruleRegulations && Array.isArray(messageData.ruleRegulations)) {
dashboardStore.updateRuleRegulations(messageData.ruleRegulations);
updateTaskStatus('ruleRegulations', 'completed');
hasUpdates = true;
updatedModules.push('规章制度');
}
// 处理注意事项
if (messageData.precautions && Array.isArray(messageData.precautions)) {
dashboardStore.updatePrecautions(messageData.precautions);
updateTaskStatus('precautions', 'completed');
hasUpdates = true;
updatedModules.push('注意事项');
}
// 处理交路信息
if (messageData.routeInfo && Array.isArray(messageData.routeInfo)) {
dashboardStore.updateRouteInfo(messageData.routeInfo);
updateTaskStatus('routeInfo', 'completed');
hasUpdates = true;
updatedModules.push('交路信息');
}
// 如果有数据更新
if (hasUpdates) {
const updateMessage = `${updatedModules.join('、')} 数据已更新`;
showNotification(updateMessage, 'success');
console.log('已更新的模块:', updatedModules);
// 如果还在思考状态,立即显示结果
if (isThinking.value) {
isThinking.value = false;
showResults.value = true;
triggerCardAnimations();
}
} else {
console.warn('WebSocket 消息中没有可识别的数据字段');
}
} catch (error) {
console.error('处理 WebSocket 消息失败:', error);
showNotification('数据处理失败', 'error');
}
};
/**
* 初始化任务列表
*/
const initializeTaskList = () => {
taskList.value = [
{
id: 'emergencyDisposal',
title: '应急处置建议',
status: 'loading',
dataKey: 'treatmentMethod'
},
{
id: 'faultInfo',
title: '故障信息',
status: 'loading',
dataKey: 'diagnosticCodes'
},
{
id: 'routeInfo',
title: '交路信息',
status: 'loading',
dataKey: 'routeInfo'
},
{
id: 'trainInfo',
title: '车组信息',
status: 'loading',
dataKey: 'trainInfo'
},
{
id: 'personnelInfo',
title: '人员信息',
status: 'loading',
dataKey: 'mechanicInfo'
},
{
id: 'ruleRegulation',
title: '规章制度',
status: 'loading',
dataKey: 'ruleRegulations'
},
{
id: 'typicalCase',
title: '典型案例',
status: 'loading',
dataKey: 'standardFiles'
},
{
id: 'precautions',
title: '注意事项',
status: 'loading',
dataKey: 'precautions'
}
];
};
/**
* 更新任务状态
*/
const updateTaskStatus = (dataKey: string, status: 'completed' | 'error') => {
const task = taskList.value.find(t => t.dataKey === dataKey);
if (task) {
task.status = status;
}
};
/**
* 处理问题提交
* 实现问答交互流程:输入问题 → 思考过程 → 显示结果
*/
const handleQuestionSubmit = async () => {
if (!questionText.value.trim()) return;
// 生成随机的系统消息ID
sysMessageId.value = generateSysMessageId();
console.log('生成的 sysMessageId:', sysMessageId.value);
// 初始化任务列表
initializeTaskList();
// 建立 WebSocket 连接
connectWebSocket();
// 保存用户提交的问题
submittedQuestion.value = questionText.value.trim();
// 第一步:隐藏输入框,显示思考过程
showQuestionInput.value = false;
isThinking.value = true;
// 模拟思考过程的文字变化
const thinkingMessages = [
'正在分析您的问题...',
'正在搜索相关信息...',
'正在整理数据...',
'正在生成结果...'
];
let messageIndex = 0;
const thinkingInterval = setInterval(() => {
if (messageIndex < thinkingMessages.length) {
thinkingText.value = thinkingMessages[messageIndex];
messageIndex++;
}
}, 500);
try {
// 调用 sendChatMessage 方法发送聊天消息
const userId = aclStore.getUserId;
if (!userId) {
throw new Error('未获取到用户ID');
}
await sendChatMessage({
content: submittedQuestion.value,
userId: userId,
chatType: '4',
sysMessageId: sysMessageId.value
});
console.log('已向后端发送请求,等待 WebSocket 推送数据...');
} catch (error) {
console.error('调用后端接口失败:', error);
clearInterval(thinkingInterval);
isThinking.value = false;
showNotification('获取数据失败,请重试', 'error');
showQuestionInput.value = true;
return;
}
// 第二步2秒后隐藏思考过程显示结果卡片如果WebSocket还没有推送数据
setTimeout(() => {
clearInterval(thinkingInterval);
if (isThinking.value) {
isThinking.value = false;
showResults.value = true;
// 触发卡片逐个弹出动画
triggerCardAnimations();
}
}, 2000);
};
/**
* 触发卡片逐个弹出动画 - 增强版
* 使用多种动画变体和更丰富的视觉效果
*/
const triggerCardAnimations = () => {
// 重置所有卡片的动画状态
const cards = document.querySelectorAll('.draggable-module-card');
// 动画变体数组
const animationVariants = [
'animate-card-pop-enhanced',
'animate-card-pop-top-left',
'animate-card-pop-bottom-right',
'animate-card-pop-spiral'
];
cards.forEach((card, index) => {
const element = card as HTMLElement;
// 清除之前的动画类
element.classList.remove(...animationVariants);
element.classList.remove('card-particles', 'card-glow-effect');
// 初始状态:优化的隐藏状态
element.style.opacity = '0';
element.style.visibility = 'hidden';
element.style.transform = 'translateY(60px) translateX(-20px) scale(0.7) rotateX(15deg) rotateY(-10deg)';
element.style.filter = 'blur(6px) brightness(0.6)';
element.style.boxShadow = '0 0 0 rgba(0, 212, 255, 0)';
// 优化延迟计算:更平滑的节奏
const baseDelay = 100; // 减少基础延迟
const staggerDelay = index * 80; // 减少间隔,让节奏更紧凑
const randomOffset = Math.random() * 40; // 减少随机性,保持节奏感
const totalDelay = baseDelay + staggerDelay + randomOffset;
// 智能选择动画变体
let animationClass;
if (index === 0) {
// 第一个卡片使用增强版动画
animationClass = 'animate-card-pop-enhanced';
} else if (index % 4 === 1) {
// 每4个卡片中的第2个使用左上角动画
animationClass = 'animate-card-pop-top-left';
} else if (index % 4 === 2) {
// 每4个卡片中的第3个使用右下角动画
animationClass = 'animate-card-pop-bottom-right';
} else if (index % 4 === 3) {
// 每4个卡片中的第4个使用螺旋动画
animationClass = 'animate-card-pop-spiral';
} else {
// 其他使用增强版动画
animationClass = 'animate-card-pop-enhanced';
}
setTimeout(() => {
// 恢复可见性
element.style.visibility = 'visible';
// 添加动画类
element.classList.add(animationClass);
// 为特定卡片添加特效(更有规律)
if (index === 0 || index % 3 === 0) {
element.classList.add('card-particles');
}
if (index === 1 || index % 5 === 0) {
element.classList.add('card-glow-effect');
}
// 添加精细的延迟类
const delayClass = `animate-delay-${Math.min(Math.floor(index * 30 + 50), 500)}`;
element.classList.add(delayClass);
// 动画完成后的回调处理
const animationDuration = animationClass === 'animate-card-pop-enhanced' ? 1000 :
animationClass === 'animate-card-pop-spiral' ? 900 : 700;
setTimeout(() => {
// 清除内联样式恢复CSS控制
element.style.opacity = '';
element.style.visibility = '';
element.style.transform = '';
element.style.filter = '';
element.style.boxShadow = '';
// 移除动画类,但保留效果类
element.classList.remove(animationClass, delayClass);
// 添加悬停效果
element.classList.add('hover-glow');
// 触发完成回调
if (index === cards.length - 1) {
onAllAnimationsComplete();
}
}, animationDuration);
}, totalDelay);
});
};
/**
* 所有动画完成后的回调函数
*/
const onAllAnimationsComplete = () => {
// 可以在这里添加额外的效果,比如显示提示信息
console.log('所有卡片动画已完成');
// 可选:显示一个完成提示
setTimeout(() => {
showNotification('智能分析完成,可以开始操作了!', 'success');
}, 500);
};
/**
* 切换拖拽功能开关
*/
const toggleDragMode = () => {
isDragEnabled.value = !isDragEnabled.value;
};
/**
* 保存当前布局
*/
const saveCurrentLayout = () => {
try {
localStorage.setItem('savedLayout', JSON.stringify(moduleLayouts));
showNotification('布局已保存', 'success');
} catch (error) {
console.error('保存布局失败:', error);
showNotification('保存布局失败', 'error');
}
};
/**
* 还原到保存的布局
*/
const restoreLayout = () => {
try {
const savedLayout = localStorage.getItem('savedLayout');
if (savedLayout) {
const parsedLayout = JSON.parse(savedLayout);
// 更新当前布局
Object.keys(parsedLayout).forEach(moduleId => {
if (moduleLayouts[moduleId]) {
moduleLayouts[moduleId].position = parsedLayout[moduleId].position;
moduleLayouts[moduleId].size = parsedLayout[moduleId].size;
}
});
saveLayout();
showNotification('布局已还原', 'success');
} else {
// 如果没有保存的布局,还原到默认布局
resetToDefaultLayout();
}
} catch (error) {
console.error('还原布局失败:', error);
showNotification('还原布局失败', 'error');
}
};
/**
* 重置到默认布局
*/
const resetToDefaultLayout = () => {
try {
const defaultLayout = getDefaultLayout();
Object.keys(defaultLayout).forEach(moduleId => {
if (moduleLayouts[moduleId]) {
moduleLayouts[moduleId].position = defaultLayout[moduleId].position;
moduleLayouts[moduleId].size = defaultLayout[moduleId].size;
}
});
saveLayout();
showNotification('已重置为默认布局', 'success');
} catch (error) {
console.error('重置布局失败:', error);
showNotification('重置布局失败', 'error');
}
};
/**
* 显示通知
*/
const showNotification = (message: string, type: 'success' | 'error' | 'info' = 'success') => {
// 清除之前的定时器
if (notificationTimer) {
clearTimeout(notificationTimer);
}
// 设置通知内容
notification.value = {
show: true,
message,
type
};
// 3秒后自动隐藏
notificationTimer = setTimeout(() => {
notification.value.show = false;
}, 3000);
};
// 生命周期钩子
onMounted(async () => {
// 设置全局标识,表明当前在 homePage
(window as any).__isHomePage__ = true;
// 触发 VabTopBar 更新检测
window.dispatchEvent(new CustomEvent('homepage-mounted'));
// 加载数据
await dashboardStore.loadAllData();
// 监听来自 VabTopBar 的事件
window.addEventListener('toggle-drag-mode', ((e: CustomEvent) => {
isDragEnabled.value = e.detail.enabled;
}) as EventListener);
window.addEventListener('save-layout', (() => {
saveCurrentLayout();
}) as EventListener);
window.addEventListener('restore-layout', (() => {
restoreLayout();
}) as EventListener);
});
onUnmounted(() => {
// 清除全局标识
(window as any).__isHomePage__ = false;
// 触发 VabTopBar 更新检测
window.dispatchEvent(new CustomEvent('homepage-unmounted'));
// 清理事件监听器
window.removeEventListener('toggle-drag-mode', (() => {}) as EventListener);
window.removeEventListener('save-layout', (() => {}) as EventListener);
window.removeEventListener('restore-layout', (() => {}) as EventListener);
// 断开 WebSocket 连接并清理监听器
webSocketService.off('message', handleWebSocketMessage);
webSocketService.off('connect');
webSocketService.off('error');
// 注意:不要在这里调用 disconnect因为 App.vue 可能还在使用
});
// 规章制度 PDF 预览状态与方法(页面级)
const rulePdfDialogVisible = ref(false);
const currentRulePdfUrl = ref('');
const rulePdfLoading = ref(false);
const handleOpenRulePdf = (rule: RuleRegulation) => {
if (!rule.fileUrl) {
ElMessage.warning('该条目未配置预览文件');
return;
}
// 重置加载状态和PDF URL
rulePdfLoading.value = true;
currentRulePdfUrl.value = '';
// 显示弹窗
rulePdfDialogVisible.value = true;
// 延迟设置PDF URL确保组件重新渲染
setTimeout(() => {
currentRulePdfUrl.value = rule.fileUrl;
}, 100);
};
const handleOpenCasePdf = (caseItem: TypicalCase) => {
if (!caseItem.fileUrl) {
ElMessage.warning('该案例未配置预览文件');
return;
}
// 重置加载状态和PDF URL
rulePdfLoading.value = true;
currentRulePdfUrl.value = '';
// 显示弹窗
rulePdfDialogVisible.value = true;
// 延迟设置PDF URL确保组件重新渲染
setTimeout(() => {
currentRulePdfUrl.value = caseItem.fileUrl;
}, 100);
};
// 监听弹窗关闭,重置状态
watch(rulePdfDialogVisible, (newValue) => {
if (!newValue) {
// 弹窗关闭时重置状态
setTimeout(() => {
currentRulePdfUrl.value = '';
rulePdfLoading.value = false;
}, 300); // 延迟重置,避免关闭动画期间的闪烁
}
});
</script>
<style scoped lang="scss">
.dashboard-page {
min-height: 100vh;
background: url('@/assets/background.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
color: #ffffff;
overflow: hidden; // 隐藏滚动条保持避免出现滚动条
position: relative;
&::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 50%, rgba(0, 162, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(0, 255, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(0, 162, 255, 0.1) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
> * {
position: relative;
z-index: 1;
}
}
.dashboard-main {
padding: 20px;
max-width: 5500px;
margin: 0 auto;
overflow: visible; // 允许卡片超出显示
}
.modules-section {
margin-bottom: 40px;
overflow: visible; // 允许卡片超出显示
}
// 问题输入界面样式 - 简洁聊天风格
.question-input-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
padding: 40px 80px;
.question-container {
width: 60%;
max-width: 1400px;
display: flex;
flex-direction: column;
align-items: center;
}
.question-title {
font-size: 2rem;
font-weight: 500;
background: linear-gradient(135deg, #00D4FF 0%, #1890FF 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
margin-bottom: 40px;
text-align: center;
}
.chat-input-container {
display: flex;
align-items: flex-start;
width: 100%;
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 24px;
padding: 20px 24px;
box-shadow: 0 2px 8px rgba(0, 212, 255, 0.1);
transition: all 0.3s ease;
backdrop-filter: blur(10px);
&:hover {
border-color: rgba(0, 212, 255, 0.5);
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.2);
}
&:focus-within {
border-color: #00D4FF;
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.3);
}
.chat-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
background: rgba(0, 212, 255, 0.15);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 50%;
margin-right: 16px;
margin-top: 4px;
flex-shrink: 0;
svg {
width: 22px;
height: 22px;
color: #00D4FF;
}
}
.input-wrapper {
flex: 1;
margin-right: 16px;
.chat-input {
width: 100%;
border: none;
outline: none;
background: transparent;
font-size: 16px;
color: #ffffff;
padding: 12px 0;
min-height: 40px;
max-height: 200px;
resize: none;
overflow-y: auto;
line-height: 1.5;
font-family: inherit;
&::placeholder {
color: #94A3B8;
}
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 212, 255, 0.1);
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 212, 255, 0.3);
border-radius: 2px;
&:hover {
background: rgba(0, 212, 255, 0.5);
}
}
}
}
.input-actions {
display: flex;
gap: 10px;
align-items: flex-start;
margin-top: 4px;
.action-btn {
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(0, 212, 255, 0.1);
border: 1px solid rgba(0, 212, 255, 0.3);
color: #94A3B8;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 20px;
height: 20px;
}
&:hover {
background: rgba(0, 212, 255, 0.2);
color: #00D4FF;
border-color: rgba(0, 212, 255, 0.5);
}
&.voice-btn:hover {
background: rgba(255, 77, 79, 0.15);
color: #ff4d4f;
border-color: rgba(255, 77, 79, 0.3);
}
&.send-btn {
background: #00D4FF;
color: white;
border-color: #00D4FF;
&:hover {
background: #1890FF;
border-color: #1890FF;
}
&:active {
background: #0066CC;
border-color: #0066CC;
}
}
}
}
}
}
// 思考过程样式
.thinking-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
padding: 40px;
.thinking-spinner {
width: 60px;
height: 60px;
border: 3px solid rgba(0, 212, 255, 0.2);
border-top: 3px solid #00d4ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 30px;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
.thinking-text {
font-size: 1.5rem;
color: #ffffff;
margin-bottom: 10px;
background: linear-gradient(135deg, #00d4ff 0%, #1890ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.thinking-dots {
font-size: 1.5rem;
color: #00d4ff;
.dot {
animation: thinkingDots 1.5s ease-in-out infinite;
&:nth-child(1) { animation-delay: 0s; }
&:nth-child(2) { animation-delay: 0.3s; }
&:nth-child(3) { animation-delay: 0.6s; }
}
}
}
.modules-container {
position: relative;
width: 100%;
min-height: calc(100vh - 70px); // 减去顶部导航栏高度
max-width: 1200px;
margin: 0 auto;
overflow: visible; // 允许卡片超出容器显示
}
/* 可拖拽模块卡片样式 */
.draggable-module-card {
cursor: move;
user-select: none;
transition: all 0.3s ease;
}
.draggable-module-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
/* 移动端响应式设计 */
@media (max-width: 768px) {
.modules-container {
min-height: 100vh;
}
}
/* 平板端响应式设计 */
@media (min-width: 769px) and (max-width: 1024px) {
.modules-container {
min-height: 100vh;
}
}
.overview-section {
margin-top: 40px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
max-width: 800px;
margin: 0 auto;
.chart-title {
color: #00a2ff;
font-size: 18px;
font-weight: 600;
margin: 20px 0 10px 0;
text-align: center;
grid-column: 1 / -1;
&:first-of-type {
margin-top: 40px;
}
}
}
.overview-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(30, 41, 59, 0.6) 100%);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
text-align: center;
&:hover {
border-color: rgba(0, 212, 255, 0.6);
transform: translateY(-2px);
}
h3 {
font-size: 28px;
font-weight: 700;
margin: 0;
color: #FFFFFF;
}
p {
font-size: 14px;
color: #94A3B8;
margin: 0;
}
}
.overview-icon {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background: linear-gradient(135deg, rgba(0, 212, 255, 0.2) 0%, rgba(24, 144, 255, 0.2) 100%);
border-radius: 12px;
color: #00D4FF;
&--success {
background: linear-gradient(135deg, rgba(82, 196, 26, 0.2) 0%, rgba(82, 196, 26, 0.2) 100%);
color: #52C41A;
}
&--warning {
background: linear-gradient(135deg, rgba(250, 173, 20, 0.2) 0%, rgba(250, 173, 20, 0.2) 100%);
color: #FAAD14;
}
&--info {
background: linear-gradient(135deg, rgba(19, 194, 194, 0.2) 0%, rgba(19, 194, 194, 0.2) 100%);
color: #13C2C2;
}
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(10, 22, 40, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
z-index: 9999;
backdrop-filter: blur(5px);
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(0, 212, 255, 0.3);
border-top: 3px solid #00D4FF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
p {
color: #94A3B8;
font-size: 16px;
margin: 0;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes thinkingDots {
0%, 20% { opacity: 0.3; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
80%, 100% { opacity: 0.3; transform: scale(1); }
}
@keyframes particleFloat {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-10px) rotate(120deg); }
66% { transform: translateY(5px) rotate(240deg); }
}
@keyframes borderGlow {
0%, 100% {
background-size: 200% 200%;
background-position: 0% 50%;
}
50% {
background-size: 200% 200%;
background-position: 100% 50%;
}
}
@keyframes iconPulse {
0%, 100% {
opacity: 0.7;
transform: translateY(-50%) scale(1);
}
50% {
opacity: 1;
transform: translateY(-50%) scale(1.1);
}
}
@keyframes buttonGlow {
0%, 100% { opacity: 0; }
50% { opacity: 0.8; }
}
@keyframes cardPopIn {
0% {
box-shadow: 0 0 0 rgba(0, 212, 255, 0);
}
30% {
box-shadow:
0 0 30px rgba(0, 212, 255, 0.4),
0 0 60px rgba(0, 255, 255, 0.2),
0 8px 32px rgba(0, 212, 255, 0.15);
}
60% {
box-shadow:
0 0 20px rgba(0, 212, 255, 0.3),
0 0 40px rgba(0, 255, 255, 0.15),
0 8px 32px rgba(0, 212, 255, 0.15);
}
100% {
box-shadow: 0 8px 32px rgba(0, 212, 255, 0.15);
}
}
// 大屏适配 (4K及以上)
@media (min-width: 2560px) {
.dashboard-main {
padding: 20px;
max-width: 2400px;
}
.modules-container {
max-width: none;
min-height: 800px;
}
.question-input-section {
padding: 40px 120px;
.question-container {
max-width: 1800px;
}
}
.overview-section {
grid-template-columns: repeat(4, 1fr);
gap: 32px;
max-width: none;
.chart-title {
font-size: 20px;
}
}
}
// 超宽屏适配 (1920px-2560px)
@media (min-width: 1920px) and (max-width: 2559px) {
.dashboard-main {
padding: 20px;
max-width: 1800px;
}
.modules-container {
max-width: none;
min-height: 800px;
}
.question-input-section {
padding: 40px 100px;
.question-container {
max-width: 1600px;
}
}
.overview-section {
grid-template-columns: repeat(4, 1fr);
gap: 28px;
max-width: none;
}
}
// 标准大屏适配 (1440px-1920px)
@media (min-width: 1440px) and (max-width: 1919px) {
.dashboard-main {
padding: 20px;
max-width: 1600px;
}
.modules-container {
max-width: none;
min-height: 800px;
}
}
// 中等屏幕适配 (1025px-1439px)
@media (min-width: 1025px) and (max-width: 1439px) {
.modules-container {
max-width: 1200px;
min-height: 800px;
}
}
@media (max-width: 1024px) {
.dashboard-main {
padding: 20px;
}
.overview-section {
max-width: 800px;
}
// 中等屏幕聊天输入框调整
.question-input-section {
padding: 40px 40px;
.question-container {
max-width: 1200px;
}
}
.chat-input-container {
.chat-avatar {
width: 34px;
height: 34px;
svg {
width: 19px;
height: 19px;
}
}
.input-actions {
.action-btn {
width: 34px;
height: 34px;
svg {
width: 17px;
height: 17px;
}
}
}
}
}
@media (max-width: 768px) {
.overview-section {
grid-template-columns: repeat(2, 1fr);
}
// 聊天输入框响应式设计
.question-input-section {
padding: 30px 20px;
.question-container {
max-width: 100%;
}
}
.chat-input-container {
padding: 10px 12px;
.chat-avatar {
width: 32px;
height: 32px;
margin-right: 10px;
svg {
width: 18px;
height: 18px;
}
}
.input-wrapper {
margin-right: 10px;
.chat-input {
font-size: 15px;
padding: 6px 0;
min-height: 60px;
}
}
.input-actions {
gap: 6px;
.action-btn {
width: 32px;
height: 32px;
svg {
width: 16px;
height: 16px;
}
}
}
}
}
@media (max-width: 480px) {
.dashboard-main {
padding: 12px;
}
// 小屏幕聊天输入框优化
.question-input-section {
padding: 20px 16px;
.question-title {
font-size: 1.5rem;
}
}
.chat-input-container {
padding: 8px 10px;
border-radius: 20px;
.chat-avatar {
width: 28px;
height: 28px;
margin-right: 8px;
svg {
width: 16px;
height: 16px;
}
}
.input-wrapper {
margin-right: 8px;
.chat-input {
font-size: 14px;
padding: 4px 0;
min-height: 48px;
}
}
.input-actions {
gap: 4px;
.action-btn {
width: 28px;
height: 28px;
svg {
width: 14px;
height: 14px;
}
}
}
}
.overview-section {
grid-template-columns: 1fr;
gap: 12px;
.chart-title {
font-size: 14px;
}
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
// 通知组件样式
.notification {
position: fixed;
top: 80px;
right: 20px;
z-index: 9999;
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid;
transition: all 0.3s ease;
&--success {
background: rgba(82, 196, 26, 0.1);
border-color: rgba(82, 196, 26, 0.3);
color: #52C41A;
}
&--error {
background: rgba(255, 77, 79, 0.1);
border-color: rgba(255, 77, 79, 0.3);
color: #FF4D4F;
}
&--info {
background: rgba(0, 162, 255, 0.1);
border-color: rgba(0, 162, 255, 0.3);
color: #00A2FF;
}
}
/* 规章制度 PDF 预览弹窗样式(页面级) */
.rule-pdf-dialog {
:deep(.el-dialog__header) {
background: linear-gradient(135deg, #13c2c2 0%, #08979c 100%);
color: #fff;
border-radius: 12px 12px 0 0;
}
:deep(.el-dialog__title) {
color: #fff;
font-weight: 600;
}
}
.preview-content {
min-height: 750px;
display: flex;
align-items: center;
justify-content: center;
background: #0b1424;
border-radius: 0 0 12px 12px;
}
.pdf-preview {
width: 100%;
height: 750px;
border-radius: 8px;
overflow: hidden;
background: #ffffff;
}
</style>