1969 lines
49 KiB
Vue
1969 lines
49 KiB
Vue
<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>
|