You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
795 lines
23 KiB
795 lines
23 KiB
|
|
<template>
|
|
<div style="display: flex;
|
|
height: 100vh;">
|
|
<Menu
|
|
:initial-active="2"
|
|
/>
|
|
<div class="medical-qa-container">
|
|
<!-- 标题 -->
|
|
<div style="display: flex;align-items: center;">
|
|
<div class="title-before">
|
|
</div>
|
|
<h2 class="title">医疗问答</h2>
|
|
</div>
|
|
|
|
<!-- 搜索框 -->
|
|
<div class="search-box">
|
|
<el-input
|
|
v-model="query"
|
|
placeholder="请输入问题..."
|
|
clearable
|
|
@keydown.enter="handleSearch"
|
|
/>
|
|
<el-button type="primary" @click="handleSearch" style=" background: #114FE6;">查询</el-button>
|
|
</div>
|
|
|
|
<!-- 主内容区 -->
|
|
<div class="content-wrapper">
|
|
<!-- 左侧:问答结果 -->
|
|
<div class="answer-list">
|
|
<div v-if="queryRecord">
|
|
<h2 class="section-title" style="margin-bottom: 10px;margin-top: 0px">问答提问</h2>
|
|
<div class="answer-item" style=" background-color: #165DFF;
|
|
border-color: #0066cc;
|
|
color: #fff; height: auto;max-height: 3vw">
|
|
{{queryRecord}}
|
|
</div>
|
|
</div>
|
|
<h2 class="section-title">问答结果</h2>
|
|
<div v-if="isSending">
|
|
<div class="loading-dots">
|
|
<div class="dot dot-1"></div>
|
|
<div class="dot dot-2"></div>
|
|
<div class="dot dot-3"></div>
|
|
<div class="dot dot-4"></div>
|
|
</div>
|
|
</div>
|
|
<div v-if="answers.length === 0" class="empty-state">
|
|
请输入问题进行查询...
|
|
</div>
|
|
<div v-else class="answer-items">
|
|
<div
|
|
v-for="(item, index) in answers"
|
|
:key="index"
|
|
class="answer-item"
|
|
@click="selectGraph(index)"
|
|
:class="{ 'highlight': selected === index }"
|
|
>
|
|
{{ item.answer }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧:知识图谱 -->
|
|
<div class="knowledge-graph">
|
|
<GraphToolbar
|
|
v-if="_graph"
|
|
:graph="_graph"
|
|
ref="toolbarRef"
|
|
/>
|
|
|
|
<div ref="graphContainer" class="graph-container" id="container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import Menu from "@/components/Menu.vue";
|
|
import {qaAnalyze} from "@/api/qa";
|
|
import {Graph} from "@antv/g6";
|
|
import {getGraph} from "@/api/graph";
|
|
import GraphToolbar from '@/components/GraphToolbar.vue';
|
|
|
|
|
|
export default {
|
|
name: 'GraghQA',
|
|
components: {Menu,GraphToolbar},
|
|
data() {
|
|
return {
|
|
_graph: null,
|
|
query:"",
|
|
answers:[],
|
|
selected:0,
|
|
// 节点样式
|
|
nodeShowLabel: true,
|
|
nodeFontSize: 12,
|
|
nodeFontColor: '#fff',
|
|
nodeShape: 'circle',
|
|
nodeSize: 50,
|
|
nodeFill: '#9FD5FF',
|
|
nodeStroke: '#5B8FF9',
|
|
nodeLineWidth: 2,
|
|
nodeFontFamily: 'Microsoft YaHei, sans-serif',
|
|
|
|
// 边样式
|
|
edgeShowLabel: true,
|
|
edgeFontSize: 10,
|
|
edgeFontColor: '#666666',
|
|
edgeType: 'quadratic',
|
|
edgeLineWidth: 2,
|
|
edgeStroke: '#b6b2b2',
|
|
edgeEndArrow: true,
|
|
edgeFontFamily: 'Microsoft YaHei, sans-serif',
|
|
|
|
|
|
queryRecord:"",
|
|
isSending:false
|
|
|
|
};
|
|
},
|
|
// =============== 👇【新增】组件内路由守卫:离开当前路由时触发 ===============
|
|
beforeRouteLeave(to, from, next) {
|
|
this.saveDataToLocalStorage();
|
|
next(); // 允许导航
|
|
},
|
|
// =======================================================================
|
|
|
|
mounted() {
|
|
// =============== 👇【新增】页面加载时从 localStorage 恢复数据 ===============
|
|
this.restoreDataFromLocalStorage();
|
|
// =======================================================================
|
|
// this.answers=[]
|
|
// 如果有初始数据,可以初始化图谱(可选)
|
|
if (this.answers.length > 0) {
|
|
this.initGraph(this.answers[0].result);
|
|
// console.log(this.answers[0].result)
|
|
}
|
|
},
|
|
beforeUnmount() {
|
|
// =============== 👇【新增】组件销毁前也保存一次(兼容非路由跳转场景)==============
|
|
this.saveDataToLocalStorage();
|
|
// 移除页面卸载监听(如果用了 beforeunload)
|
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
|
// =======================================================================
|
|
},
|
|
|
|
created() {
|
|
// =============== 👇【新增】监听页面刷新/关闭事件 ===============
|
|
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
|
// =======================================================================
|
|
},
|
|
methods: {
|
|
buildNodeLabelMap(nodes) {
|
|
this._nodeLabelMap = new Map();
|
|
nodes.forEach(node => {
|
|
this._nodeLabelMap.set(node.id, node.data?.type || 'default');
|
|
});
|
|
},
|
|
// =============== 👇【新增】统一的数据保存方法 ===============
|
|
saveDataToLocalStorage() {
|
|
try {
|
|
localStorage.setItem('graphQA_queryRecord', this.queryRecord);
|
|
localStorage.setItem('graphQA_answers', JSON.stringify(this.answers));
|
|
console.log('✅ 数据已保存到 localStorage');
|
|
} catch (e) {
|
|
console.warn('⚠️ 无法保存到 localStorage:', e);
|
|
}
|
|
},
|
|
// =======================================================================
|
|
|
|
// =============== 👇【新增】统一的数据恢复方法 ===============
|
|
restoreDataFromLocalStorage() {
|
|
try {
|
|
const savedQuery = localStorage.getItem('graphQA_queryRecord');
|
|
const savedAnswers = localStorage.getItem('graphQA_answers');
|
|
|
|
if (savedQuery !== null) {
|
|
this.queryRecord = savedQuery;
|
|
}
|
|
if (savedAnswers !== null) {
|
|
this.answers = JSON.parse(savedAnswers);
|
|
// 确保 selected 不越界
|
|
if (this.answers.length > 0) {
|
|
this.selected = Math.min(this.selected, this.answers.length - 1);
|
|
}
|
|
}
|
|
console.log('✅ 数据已从 localStorage 恢复');
|
|
} catch (e) {
|
|
console.warn('⚠️ 无法从 localStorage 恢复数据:', e);
|
|
// 出错时清空(避免脏数据)
|
|
localStorage.removeItem('graphQA_queryRecord');
|
|
localStorage.removeItem('graphQA_answers');
|
|
}
|
|
},
|
|
// =======================================================================
|
|
|
|
// =============== 👇【新增】处理页面关闭/刷新的兜底保存 ===============
|
|
handleBeforeUnload(event) {
|
|
this.saveDataToLocalStorage();
|
|
// 注意:现代浏览器通常不显示自定义消息
|
|
event.preventDefault();
|
|
event.returnValue = ''; // 必须设置才能触发提示(但实际可能不显示)
|
|
},
|
|
selectGraph(index){
|
|
this.selected=index
|
|
if(this.answers.length>0){
|
|
this.formatData(this.answers[index].result)
|
|
}
|
|
|
|
},
|
|
handleSearch() {
|
|
alert('方法触发成功!');
|
|
console.log('--- 1. 发起搜索,参数为:', this.query);
|
|
this.isSending = true;
|
|
this.answers = [];
|
|
|
|
if (this._graph) {
|
|
this._graph.clear();
|
|
}
|
|
|
|
let data = { text: this.query };
|
|
this.queryRecord = this.query;
|
|
this.query = "";
|
|
|
|
// 1. 调用接口
|
|
qaAnalyze(data).then(res => {
|
|
console.log('--- 2. 接口响应成功 ---');
|
|
this.answers = res;
|
|
if (this.answers && this.answers.length > 0) {
|
|
this.initGraph(this.answers[0].result);
|
|
}
|
|
this.isSending = false;
|
|
}).catch(err => {
|
|
console.error('--- 2. 接口失败,启动保底方案 ---', err);
|
|
const mockData = {
|
|
nodes: [
|
|
{ id: "node1", label: "霍乱", data: { type: "疾病" } },
|
|
{ id: "node2", label: "腹泻", data: { type: "症状" } },
|
|
{ id: "node3", label: "脱水", data: { type: "疾病" } },
|
|
{ id: "node4", label: "呕吐", data: { type: "疾病" } },
|
|
{ id: "node5", label: "霍乱弧菌", data: { type: "病因" } },
|
|
{ id: "node6", label: "复方磺胺", data: { type: "药品" } },
|
|
|
|
],
|
|
edges: [
|
|
{ id: "e1", source: "node1", target: "node2", data: { label: "典型症状" } },
|
|
{ id: "e2", source: "node1", target: "node3", data: { label: "并发症" } },
|
|
{ id: "e3", source: "node1", target: "node4", data: { label: "并发症" } },
|
|
{ id: "e4", source: "node1", target: "node5", data: { label: "致病菌" } },
|
|
{ id: "e5", source: "node1", target: "node6", data: { label: "推荐用药" } },
|
|
]
|
|
};
|
|
|
|
// 伪造一个和接口返回格式一样的数组
|
|
this.answers = [{
|
|
answer: "后端接口连接失败,当前显示的是预览版图谱。",
|
|
result: mockData
|
|
}];
|
|
|
|
// 强制执行初始化,让工具栏出来
|
|
this.initGraph(this.answers[0].result);
|
|
|
|
this.isSending = false;
|
|
this.$message.warning("已切换至离线预览模式");
|
|
});
|
|
},
|
|
formatData(data) {
|
|
const typeMap = {
|
|
'疾病': 'Disease',
|
|
'药品': 'Drug',
|
|
'药物': 'Drug',
|
|
'症状': 'Symptom',
|
|
'检查': 'Check'
|
|
};
|
|
|
|
const updatedEdges = data.edges.map(edge => ({
|
|
...edge,
|
|
type: this.edgeType,
|
|
data: {
|
|
...edge.data,
|
|
label: edge.data?.label || ""
|
|
},
|
|
style: {
|
|
endArrow: this.edgeEndArrow,
|
|
stroke: this.edgeStroke,
|
|
lineWidth: this.edgeLineWidth,
|
|
label: this.edgeShowLabel,
|
|
labelFontSize: this.edgeFontSize,
|
|
labelFontFamily: this.edgeFontFamily,
|
|
labelFill: this.edgeFontColor,
|
|
},
|
|
}));
|
|
|
|
// 【关键修正点】
|
|
const updatedNodes = data.nodes.map(node => {
|
|
// 1. 这里使用标准大括号,去掉之前的圆括号
|
|
const englishLabel = typeMap[node.data?.type] || 'Other';
|
|
|
|
// 2. 必须手动 return
|
|
return {
|
|
...node,
|
|
type: this.nodeShape,
|
|
data: {
|
|
...node.data,
|
|
label: englishLabel, // 适配组件颜色逻辑 (GraphToolbar 里的 colorMap)
|
|
name: node.label // 适配组件文字逻辑 (GraphToolbar 里的 node.data.name)
|
|
},
|
|
style: {
|
|
...node.style, // 必须保留原始 style 里的 x, y 坐标,否则导出时节点会重叠
|
|
size: this.nodeSize,
|
|
lineWidth: this.nodeLineWidth,
|
|
label: this.nodeShowLabel,
|
|
labelFontSize: this.nodeFontSize,
|
|
labelFontFamily: this.nodeFontFamily,
|
|
labelFill: this.nodeFontColor,
|
|
opacity: 1,
|
|
}
|
|
};
|
|
});
|
|
|
|
const updatedData = {
|
|
nodes: updatedNodes,
|
|
edges: updatedEdges
|
|
};
|
|
|
|
this.updateGraph(updatedData);
|
|
},
|
|
updateGraph(data) {
|
|
if (!this._graph) return
|
|
|
|
this._graph.setData(data)
|
|
this._graph.render()
|
|
},
|
|
localResetGraph() {
|
|
if (!this._graph) return;
|
|
|
|
// 1. 获取当前选中的数据快照
|
|
const currentResult = this.answers[this.selected]?.result;
|
|
if (!currentResult) return;
|
|
|
|
// 2. 彻底销毁当前图谱实例(这是清理 EventBoundary 报错的终极手段)
|
|
this._graph.destroy();
|
|
this._graph = null;
|
|
|
|
// 3. 重新调用 initGraph,相当于重新加载页面时的纯净状态
|
|
// 这种方式不会闪烁,因为 initGraph 内部包含了一套完整的 render 流程
|
|
this.$nextTick(() => {
|
|
this.initGraph(currentResult);
|
|
this.$message.success("图谱已重置");
|
|
});
|
|
},
|
|
|
|
initGraph(data) {
|
|
console.log('--- 触发了 initGraph ---', data);
|
|
if (this._graph!=null){
|
|
this._graph.destroy()
|
|
this._graph = null;
|
|
}
|
|
|
|
const updatedEdges = data.edges.map(edge => ({
|
|
...edge,
|
|
type: this.edgeType,
|
|
data: {
|
|
...edge.data,
|
|
label: edge.data?.label || ""
|
|
},
|
|
style: {
|
|
endArrow: this.edgeEndArrow,
|
|
stroke: this.edgeStroke,
|
|
lineWidth: this.edgeLineWidth,
|
|
label: this.edgeShowLabel,
|
|
labelFontSize: this.edgeFontSize,
|
|
labelFontFamily: this.edgeFontFamily,
|
|
labelFill: this.edgeFontColor,
|
|
|
|
},
|
|
}))
|
|
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check' };
|
|
const updatedNodes = data.nodes.map(node => {
|
|
const englishLabel = typeMap[node.data?.type] || 'Other';
|
|
return {
|
|
...node,
|
|
type: this.nodeShape,
|
|
data: {
|
|
...node.data,
|
|
label: englishLabel, // 适配组件颜色逻辑
|
|
name: node.label // 适配组件导出文字逻辑
|
|
},
|
|
style: {
|
|
...node.style,
|
|
size: this.nodeSize,
|
|
lineWidth: this.nodeLineWidth,
|
|
label: this.nodeShowLabel,
|
|
labelFontSize: this.nodeFontSize,
|
|
labelFontFamily: this.nodeFontFamily,
|
|
labelFill: this.nodeFontColor,
|
|
opacity: 1,
|
|
}
|
|
};
|
|
});
|
|
const updatedData = {
|
|
nodes: updatedNodes,
|
|
edges: updatedEdges
|
|
}
|
|
this.buildNodeLabelMap(updatedNodes);
|
|
const container = this.$refs.graphContainer;
|
|
console.log(container)
|
|
if (container!=null){
|
|
const width = container.clientWidth || 800;
|
|
const height = container.clientHeight || 600;
|
|
console.log(width)
|
|
console.log(height)
|
|
const graph = new Graph({
|
|
container,
|
|
width,
|
|
height,
|
|
plugins: [
|
|
{
|
|
type: 'toolbar',
|
|
key: 'g6-toolbar',
|
|
onClick: (id) => {
|
|
if (id === 'reset') {
|
|
this.localResetGraph(); // 如果你有重置方法
|
|
} else if (this.$refs.toolbarRef) {
|
|
// 调用 GraphToolbar.vue 组件内部的方法执行 放大/缩小/导出
|
|
this.$refs.toolbarRef.handleToolbarAction(id);
|
|
}
|
|
},
|
|
getItems: () => {
|
|
return [
|
|
{ id: 'zoom-in', value: 'zoom-in', title: '放大' },
|
|
{ id: 'zoom-out', value: 'zoom-out', title: '缩小' },
|
|
{ id: 'undo', value: 'undo', title: '撤销' },
|
|
{ id: 'redo', value: 'redo', title: '重做' },
|
|
{ id: 'auto-fit', value: 'auto-fit', title: '聚焦' },
|
|
{ id: 'reset', value: 'reset', title: '重置' },
|
|
{ id: 'export', value: 'export', title: '导出图谱' },
|
|
];
|
|
},
|
|
},
|
|
{ type: 'history', key: 'history' }, // 撤销重做必须
|
|
],
|
|
layout: {
|
|
type: 'force', // 力导向布局
|
|
gravity: 0.3, // 重力系数,控制节点聚集程度
|
|
repulsion: 500, // 排斥力
|
|
attraction: 20, // 吸引力
|
|
preventOverlap: true // 防止节点重叠
|
|
|
|
|
|
},
|
|
behaviors: [ 'zoom-canvas', 'drag-element',
|
|
'click-select','focus-element', {
|
|
type: 'hover-activate',
|
|
degree: 1,
|
|
enable: (e) =>
|
|
{
|
|
return e.target && e.target.id && e.action !== 'drag';
|
|
}
|
|
},
|
|
{
|
|
type: 'drag-canvas',
|
|
enable: (event) => event.shiftKey === false,
|
|
},
|
|
{
|
|
type: 'brush-select',
|
|
},
|
|
],
|
|
|
|
node: {
|
|
style: {
|
|
fill: (d) => {
|
|
|
|
const label = d.data?.type;
|
|
if (label === '疾病') return '#EF4444'; // 红
|
|
if (label === '药品'||label === '药物') return '#91cc75'; // 绿
|
|
if (label === '症状') return '#fac858'; // 橙
|
|
if (label === '检查') return '#336eee'; // 橙
|
|
return '#59d1d4'; // 默认灰蓝
|
|
},
|
|
stroke: (d) => {
|
|
const label = d.data?.type;
|
|
if (label === '疾病') return '#B91C1C';
|
|
if (label === '药品'||label === '药物') return '#047857';
|
|
if (label === '检查') return '#1D4ED8'; // 橙
|
|
if (label === '症状') return '#B45309';
|
|
return '#40999b';
|
|
},
|
|
labelText: (d) => d.label,
|
|
labelPlacement: 'center',
|
|
labelWordWrap: true,
|
|
labelMaxWidth: '150%',
|
|
labelMaxLines: 3,
|
|
labelTextOverflow: 'ellipsis',
|
|
labelTextAlign: 'center',
|
|
opacity: 1
|
|
},
|
|
state: {
|
|
active: {
|
|
lineWidth: 2,
|
|
shadowColor: '#ffffff',
|
|
shadowBlur: 10,
|
|
opacity: 1
|
|
},
|
|
inactive: {
|
|
opacity: 0.3
|
|
},
|
|
normal:{
|
|
opacity: 1
|
|
}
|
|
|
|
|
|
},
|
|
},
|
|
|
|
edge: {
|
|
style: {
|
|
labelText: (d) => {return d.data.label},
|
|
stroke: (d) => {
|
|
const targetLabel = this._nodeLabelMap.get(d.source); // d.target 是目标节点 ID
|
|
if (targetLabel === '疾病') return 'rgba(239,68,68,0.5)';
|
|
if (targetLabel === '药品'||targetLabel === '药物') return 'rgba(145,204,117,0.5)';
|
|
if (targetLabel === '症状') return 'rgba(250,200,88,0.5)';
|
|
if (targetLabel === '检查') return 'rgba(51,110,238,0.5)'; // 橙
|
|
return 'rgba(89,209,212,0.5)'; // default
|
|
},
|
|
// labelFill: (d) => {
|
|
// // 获取 target 节点的 label
|
|
// const targetLabel = this._nodeLabelMap.get(d.target); // d.target 是目标节点 ID
|
|
// // 根据 target 节点类型返回对应浅色
|
|
//
|
|
// if (targetLabel === 'Disease') return '#ff4444';
|
|
// if (targetLabel === 'Drug') return '#2f9b70';
|
|
// if (targetLabel === 'Symptom') return '#f89775';
|
|
// return '#6b91ff'; // default
|
|
// }
|
|
|
|
},
|
|
state: {
|
|
selected: {
|
|
stroke: '#1890FF',
|
|
lineWidth: 2,
|
|
},
|
|
highlight: {
|
|
halo: true,
|
|
haloStroke: '#1890FF',
|
|
haloLineWidth: 6,
|
|
haloStrokeOpacity: 0.3,
|
|
lineWidth: 3,
|
|
opacity: 1
|
|
},
|
|
inactive: {
|
|
opacity: 0.3
|
|
},
|
|
normal:{
|
|
opacity: 1
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
data:updatedData,
|
|
|
|
|
|
});
|
|
|
|
graph.render();
|
|
this._graph = graph
|
|
this._graph?.fitView()
|
|
}
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
</script>
|
|
<style scoped>
|
|
.medical-qa-container {
|
|
padding: 25px;
|
|
background-color: #F6F9FF;
|
|
flex: 1;
|
|
overflow: auto;
|
|
height: 100vh;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.title {
|
|
font-size: 22px;
|
|
margin-bottom: 20px;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
color: #165DFF;
|
|
margin-left: 11px;
|
|
}
|
|
|
|
.title-before {
|
|
content: '';
|
|
width: 8px;
|
|
height: 22px;
|
|
background-color: #165DFF;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.search-box {
|
|
display: flex;
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
.search-box .el-input {
|
|
flex: 1;
|
|
max-width: 94%;
|
|
margin-right: 5px;
|
|
}
|
|
|
|
.content-wrapper {
|
|
display: flex;
|
|
gap: 20px;
|
|
height: 37vw;
|
|
}
|
|
|
|
.answer-list {
|
|
flex: 1;
|
|
background-color: white;
|
|
border-radius: 33px;
|
|
padding: 25px;
|
|
box-shadow: 3px 1px 12px 7px #ECF2FF;
|
|
max-width: 32%;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 16px;
|
|
color: #165DFF;
|
|
margin-bottom: 15px;
|
|
padding-left: 12px;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.section-title::before {
|
|
content: '';
|
|
width: 6px;
|
|
height: 16px;
|
|
background-color: #165DFF;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.answer-items {
|
|
max-height: 25vw;
|
|
overflow-y: auto;
|
|
padding-right: 10px;
|
|
cursor: pointer;
|
|
|
|
}
|
|
.answer-items::-webkit-scrollbar {
|
|
width: 6px; /* 滚动条宽度 */
|
|
}
|
|
|
|
.answer-items::-webkit-scrollbar-track {
|
|
background: #fff; /* 轨道背景 */
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.answer-items::-webkit-scrollbar-thumb {
|
|
background: #B8C2D9; /* 滑块颜色 */
|
|
border-radius: 4px;
|
|
transition: background 0.2s ease;
|
|
|
|
}
|
|
|
|
.answer-items::-webkit-scrollbar-thumb:hover {
|
|
background: #B8C2D9; /* 悬停时滑块颜色 */
|
|
}
|
|
.answer-item {
|
|
padding: 12px;
|
|
border: 1px solid #165DFF;
|
|
border-radius: 15px;
|
|
background-color: #f9f9f9;
|
|
line-height: 1.5;
|
|
word-wrap: break-word;
|
|
color: #a2a5a7;
|
|
font-weight: 400;
|
|
font-size: 13px;
|
|
text-align: left;
|
|
height: 4.5vw;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.answer-item.highlight {
|
|
background-color: #165DFF;
|
|
border-color: #0066cc;
|
|
color: #fff;
|
|
}
|
|
.answer-item:hover {
|
|
background-color: #165DFF;
|
|
border-color: #0066cc;
|
|
color: #fff;
|
|
}
|
|
.empty-state {
|
|
text-align: center;
|
|
color: #fff;
|
|
padding: 40px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.knowledge-graph {
|
|
flex: 1;
|
|
background-color: white;
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
|
}
|
|
/deep/.el-input__wrapper{
|
|
background-color: #EFF4FF;
|
|
border: 1px solid #165DFF;
|
|
border-radius: 8px;
|
|
}
|
|
/* 加载动画容器 */
|
|
.loading-dots {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 10px; /* 圆点之间的间距 */
|
|
}
|
|
|
|
/* 每个圆点的基础样式 */
|
|
.dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background-color: #a6d2ff; /* 默认浅蓝 */
|
|
animation: bounce 1.2s infinite ease-in-out both;
|
|
}
|
|
|
|
/* 使用不同的蓝色调 */
|
|
.dot-1 { background-color: #4096ff; } /* 较深的蓝色 */
|
|
.dot-2 { background-color: #6fbfff; } /* 中等蓝色 */
|
|
.dot-3 { background-color: #8ed1ff; } /* 浅一些的蓝色 */
|
|
.dot-4 { background-color: #a6d2ff; } /* 更浅的蓝色 */
|
|
|
|
/* 定义弹跳动画 */
|
|
@keyframes bounce {
|
|
0%, 80%, 100% {
|
|
transform: scale(0);
|
|
}
|
|
40% {
|
|
transform: scale(1.0);
|
|
}
|
|
}
|
|
|
|
/* 让每个圆点的动画延迟出现,形成连续效果 */
|
|
.dot-1 { animation-delay: -0.4s; }
|
|
.dot-2 { animation-delay: -0.2s; }
|
|
.dot-3 { animation-delay: 0s; }
|
|
.dot-4 { animation-delay: 0.2s; }
|
|
|
|
.knowledge-graph {
|
|
position: relative; /* 必须!否则工具栏会飘走 */
|
|
flex: 1;
|
|
height: 100%;
|
|
background-color: white;
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
|
overflow: hidden; /* 保证工具栏不超出边界 */
|
|
}
|
|
|
|
/* 确保图谱容器铺满 */
|
|
.graph-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
:deep(.custom-graph-toolbar) {
|
|
position: absolute !important; /* 强制绝对定位 */
|
|
top: 30px !important;
|
|
right: 30px !important;
|
|
z-index: 9999 !important; /* 确保层级最高 */
|
|
display: inline-flex !important;
|
|
}
|
|
</style>
|