Browse Source

Merge branch 'mh' of http://124.70.32.114:3100/hanyuqing/KGPython into hanyuqing

# Conflicts:
#	vue/src/system/GraphDemo.vue
#	vue/src/system/GraphQA.vue
#	vue/src/system/KGData.vue
hanyuqing
hanyuqing 3 months ago
parent
commit
2bb1329352
  1. 111
      vue/src/system/GraphDemo.vue
  2. 482
      vue/src/system/GraphQA.vue
  3. 664
      vue/src/system/KGData.vue

111
vue/src/system/GraphDemo.vue

@ -134,6 +134,13 @@
</div>
</div>
<div class="graph-viewport">
<GraphToolbar
ref="toolbarRef"
v-if="_graph"
:graph="_graph"
class="toolbar-position"
style="position: absolute; top: 40px; left: 750px; z-index: 1000; width: auto;"
/>
<div ref="graphContainer" class="graph-container" id="container"></div>
</div>
</section>
@ -155,14 +162,17 @@ import {
import {Graph, Tooltip} from '@antv/g6';
import Menu from "@/components/Menu.vue";
import {a} from "vue-router/dist/devtools-EWN81iOl.mjs";
import GraphToolbar from "@/components/GraphToolbar.vue";
import Fuse from 'fuse.js';
import {ElMessage} from "element-plus";
import {getGraphStyleActive} from "@/api/style";
export default {
name: 'Display',
components: {Menu},
components: {Menu,GraphToolbar},
data() {
return {
_graph: null,
G6: null, //
//
nodeShowLabel: true,
@ -1164,18 +1174,18 @@ export default {
label:node.name,
type:node.label
}
getGraph(data).then(response=>{
getGraph(data).then(response => {
console.log(response)
this.formatData(response)
}); // Promise
});
graph.once('afterlayout', () => {
if (!graph.destroyed) {
graph.fitCenter({ padding: 40, duration: 1000 });
graph.fitCenter({padding: 40, duration: 1000});
}
});
this._graph = graph
this._graph.setPlugins([ {
this._graph.setPlugins([{
type: 'tooltip',
// tooltip
enable: (e) => e.targetType === 'edge',
@ -1184,7 +1194,7 @@ export default {
console.log(e)
const edge = items[0]; //
if (!edge) return '';
const data=items[0].data
const data = items[0].data
const sourceId = edge.source;
const targetId = edge.target;
@ -1201,10 +1211,72 @@ export default {
${sourceName} ${rel} > ${targetName}
</div>`;
},
},])
},
{
type: 'toolbar',
onClick: (id) =>
{
if (id === 'reset') {
// 1.
this.localResetGraph();
}
else if (this.$refs.toolbarRef) {
// 2.
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: '导出图谱' },
];
},
},
// history
{ type: 'history', key: 'history' }])
}
},
localResetGraph() {
// 1.
if (!this._graph) return;
if (!this.defaultData || !this.defaultData.nodes) {
this.$message.warning("未找到可重置的数据源");
return;
}
try {
// 2.
const canvas = this._graph.getCanvas();
if (canvas && typeof canvas.setCursor === 'function') {
canvas.setCursor('default');
}
// 3.
// DOM EventBoundary
this._graph.destroy();
this._graph = null;
// 4.
this.$nextTick(() => {
// initGraph this.defaultData
this.initGraph();
// 5. destroy
this.buildCategoryIndex();
this.$message.success("图谱已重置");
});
} catch (err) {
console.error('重置图谱失败:', err);
//
// location.reload();
}
},
updateGraph(data) {
@ -1241,7 +1313,7 @@ export default {
if (!this._graph) return
const updatedEdges = this.defaultData.edges.map(edge => ({
...edge,
type:this.edgeType,
type: this.edgeType,
style: {
endArrow: this.edgeEndArrow,
stroke: this.edgeStroke,
@ -1524,12 +1596,14 @@ button:hover {
padding: 0 2px 0px 15px;
justify-content: space-between;
}
.d-title{
.d-title {
display: flex;
align-items: center;
font-size: 13px;
}
.d-count{
.d-count {
font-size: 9px;
background-color: #5989F0;
border-radius: 7px;
@ -1576,11 +1650,13 @@ button:hover {
text-align: left;
font-size: 12px;
}
.disease-body{
.disease-body {
width: 360px;
overflow: scroll;
height: 74vh;
}
/* 隐藏滚动条,但允许滚动 */
.disease-body {
/* Firefox */
@ -1606,12 +1682,12 @@ button:hover {
/deep/ .radio-drug .el-radio__input.is-checked .el-radio__inner {
background: #52c41a; /* 检查的颜色 */
border-color:#52c41a;
border-color: #52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked .el-radio__inner {
background: #1890ff; /* 药品的颜色 */
border-color:#1890ff;
border-color: #1890ff;
}
@ -1624,25 +1700,26 @@ button:hover {
/deep/ .radio-drug .el-radio__input.is-checked .el-radio__inner:hover {
background: #52c41a; /* 检查的颜色 */
border-color:#52c41a;
border-color: #52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked .el-radio__inner:hover {
background: #1890ff; /* 药品的颜色 */
border-color:#1890ff;
border-color: #1890ff;
}
/* 自定义选中后的样式 */
/deep/ .radio-disease .el-radio__input.is-checked+.el-radio__label {
/deep/ .radio-disease .el-radio__input.is-checked + .el-radio__label {
color: rgb(153, 10, 0);
}
/deep/ .radio-drug .el-radio__input.is-checked+.el-radio__label {
/deep/ .radio-drug .el-radio__input.is-checked + .el-radio__label {
color: #52c41a;
}
/deep/ .radio-check .el-radio__input.is-checked+.el-radio__label {
/deep/ .radio-check .el-radio__input.is-checked + .el-radio__label {
color: #1890ff;
}
/* 自定义下拉样式 */

482
vue/src/system/GraphQA.vue

@ -63,6 +63,12 @@
<!-- 右侧知识图谱 -->
<div class="knowledge-graph">
<GraphToolbar
v-if="_graph"
:graph="_graph"
ref="toolbarRef"
/>
<div ref="graphContainer" class="graph-container" id="container"></div>
</div>
</div>
@ -73,21 +79,21 @@
<script>
import Menu from "@/components/Menu.vue";
import {qaAnalyze} from "@/api/qa";
import {Graph} from "@antv/g6";
import {getGraph} from "@/api/graph";
import {getGraphStyleActive} from "@/api/style";
import { qaAnalyze } from "@/api/qa";
import { Graph } from "@antv/g6";
import { getGraphStyleActive } from "@/api/style";
import GraphToolbar from '@/components/GraphToolbar.vue';
export default {
name: 'GraghQA',
components: {Menu},
components: { Menu, GraphToolbar },
data() {
return {
query:"",
answers:[],
selected:0,
//
_graph: null,
query: "",
answers: [],
selected: 0,
//
nodeShowLabel: true,
nodeFontSize: 12,
nodeFontColor: '#fff',
@ -98,7 +104,7 @@ export default {
nodeLineWidth: 2,
nodeFontFamily: 'Microsoft YaHei, sans-serif',
//
//
edgeShowLabel: true,
edgeFontSize: 10,
edgeFontColor: '#666666',
@ -108,11 +114,10 @@ export default {
edgeEndArrow: true,
edgeFontFamily: 'Microsoft YaHei, sans-serif',
queryRecord:"",
isSending:false,
configs:[],
parsedStyles:{},
queryRecord: "",
isSending: false,
configs: [],
parsedStyles: {},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
@ -120,41 +125,31 @@ export default {
Symptom: '症状',
Other: '其他'
}
};
},
// =============== 👇 ===============
beforeRouteLeave(to, from, next) {
this.saveDataToLocalStorage();
next(); //
next();
},
// =======================================================================
async mounted() {
await this.getDefault()
// =============== 👇 localStorage ===============
await this.getDefault();
this.restoreDataFromLocalStorage();
// =======================================================================
// this.answers=[]
//
if (this.answers.length > 0) {
this.initGraph(this.answers[0].result);
// console.log(this.answers[0].result)
this.initGraph(this.answers[this.selected].result);
}
},
beforeUnmount() {
// =============== 👇==============
this.saveDataToLocalStorage();
// beforeunload
window.removeEventListener('beforeunload', this.handleBeforeUnload);
// =======================================================================
},
created() {
// =============== 👇/ ===============
window.addEventListener('beforeunload', this.handleBeforeUnload);
// =======================================================================
},
methods: {
safeParseStyles(stylesStr) {
try {
@ -164,7 +159,8 @@ export default {
return {};
}
},
async getDefault(){
async getDefault() {
const response = await getGraphStyleActive();
const data = response.data;
if (!Array.isArray(data) || data.length === 0) {
@ -172,27 +168,23 @@ export default {
this.parsedStyles = {};
return;
}
// is_active=1
const activeGroup = data[0];
this.configs = Array.isArray(activeGroup.configs) ? activeGroup.configs : [];
// label -> style
const styleMap = {};
this.configs.forEach(config => {
const label = config.current_label;
styleMap[label] = this.safeParseStyles(config.styles);
});
this.parsedStyles = styleMap;
console.log(this.parsedStyles)
},
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);
@ -202,89 +194,110 @@ export default {
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 (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 = ''; //
event.returnValue = '';
},
selectGraph(index){
this.selected=index
if(this.answers.length>0){
this.formatData(this.answers[index].result)
}
},
handleSearch(){
this.isSending=true
this.answers=[]
if (this._graph){
this._graph.clear()
}
let data={
text:this.query
selectGraph(index) {
this.selected = index;
if (this.answers.length > 0) {
this.formatData(this.answers[index].result);
}
this.queryRecord=this.query
this.query=""
// this.answers=[{"answer":"尿",
// "result":{"nodes":[{"id":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","label":"尿","type":""},{"id":"89f9498b-7a83-4361-889b-f96dfdfb802c","label":"","type":""},{"id":"03201620-ba9f-4957-b7d3-f89c5a115e37","label":"","type":""},{"id":"b0bace0a-eedc-485c-90d3-0a5378dc5556","label":"","type":""},{"id":"ffc12d7b-60e5-4ffa-a945-a0769f6e1047","label":"","type":""},{"id":"a7e94ee7-072b-456f-bc0c-354545851c38","label":"","type":""}],"edges":[{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"03201620-ba9f-4957-b7d3-f89c5a115e37","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"b0bace0a-eedc-485c-90d3-0a5378dc5556","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"ffc12d7b-60e5-4ffa-a945-a0769f6e1047","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"a7e94ee7-072b-456f-bc0c-354545851c38","label":""},{"source":"e0a8410b-c5ee-47d4-acbe-b7ae5f111fd4","target":"89f9498b-7a83-4361-889b-f96dfdfb802c","label":""}]}}]
// this.initGraph(this.answers[0].result)
// this.formatData(this.answers[0].result)
qaAnalyze(data).then(res=>{
this.answers=res
if(this.answers.length>0){
this.initGraph(this.answers[0].result)
},
handleSearch() {
this.isSending = true;
this.answers = [];
if (this._graph) this._graph.clear();
let data = { text: this.query };
this.queryRecord = this.query;
this.query = "";
qaAnalyze(data).then(res => {
this.answers = res;
if (this.answers && this.answers.length > 0) {
this.initGraph(this.answers[0].result);
}
this.isSending=false
})
},
formatData(data){
// this._graph.stopLayout();
// this.clearGraphState();
// === 1. nodeId label ===
const nodeIdToEnLabel = {};
this.isSending = false;
}).catch(err => {
console.error('接口失败,启动保底方案', 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;
});
},
formatData(data) {
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' };
const getStandardLabel = (rawType) => typeMap[rawType] || rawType;
const nodeIdToData = {};
data.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease"
nodeIdToData[node.id] = {
enLabel: getStandardLabel(node.data.type),
rawType: node.data.type
};
});
// === 2. label ===
const updatedNodes = data.nodes.map(node => {
const enLabel = node.data.type;
const enLabel = getStandardLabel(node.data.type);
const styleConf = this.parsedStyles[enLabel] || {};
// 💡 styleConf
let fColor = styleConf.nodeFill;
if (!fColor) {
if (node.data.type === '疾病') fColor = '#EF4444';
else if (node.data.type === '药品' || node.data.type === '药物') fColor = '#91cc75';
else if (node.data.type === '症状') fColor = '#fac858';
else if (node.data.type === '检查') fColor = '#336eee';
else fColor = this.nodeFill;
}
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
data: { ...node.data, label: enLabel, name: node.label },
style: {
...node.style,
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
fill: fColor,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
@ -295,64 +308,97 @@ export default {
};
});
// === 3. source label ===
const updatedEdges = data.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const styleConf = this.parsedStyles[sourceEnLabel] || {};
const sourceInfo = nodeIdToData[edge.source];
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {};
// 💡
let eStroke = styleConf.edgeStroke;
if (!eStroke) {
if (sourceInfo.rawType === '疾病') eStroke = 'rgba(239, 68, 68, 0.4)';
else if (sourceInfo.rawType === '药品' || sourceInfo.rawType === '药物') eStroke = 'rgba(145, 204, 117, 0.4)';
else if (sourceInfo.rawType === '症状') eStroke = 'rgba(250, 200, 88, 0.4)';
else eStroke = this.edgeStroke;
}
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType ||this.edgeType,
type: styleConf.edgeType || this.edgeType,
style: {
...edge.style,
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
stroke: eStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: styleConf.edgeFontColor || this.edgeFontColor
}
},
data: { ...edge.data, label: edge.data?.label || "" },
};
});
// === 4. ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
};
this.updateGraph(updatedData)
const pureData = JSON.parse(JSON.stringify({ nodes: updatedNodes, edges: updatedEdges }));
this.updateGraph(pureData);
},
updateGraph(data) {
if (!this._graph) return
if (!this._graph) return;
this._graph.setData(data);
this._graph.render();
},
this._graph.setData(data)
this._graph.render()
localResetGraph() {
if (!this._graph) return;
const currentResult = this.answers[this.selected]?.result;
if (!currentResult) return;
this._graph.destroy();
this._graph = null;
this.$nextTick(() => {
this.initGraph(currentResult);
this.$message.success("图谱已重置");
});
},
initGraph(data) {
if (this._graph!=null){
this._graph.destroy()
const typeMap = { '疾病': 'Disease', '药品': 'Drug', '药物': 'Drug', '症状': 'Symptom', '检查': 'Check', '病因': 'Cause' };
const getStandardLabel = (rawType) => typeMap[rawType] || rawType;
if (this._graph != null) {
this._graph.destroy();
this._graph = null;
}
console.log(data)
// === 1. nodeId label ===
const nodeIdToEnLabel = {};
const nodeIdToData = {};
data.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.type; // e.g. "Disease"
nodeIdToData[node.id] = {
enLabel: getStandardLabel(node.data.type),
rawType: node.data.type
};
});
console.log(nodeIdToEnLabel)
// === 2. label ===
const updatedNodes = data.nodes.map(node => {
const enLabel = node.data.type;
const enLabel = getStandardLabel(node.data.type);
const styleConf = this.parsedStyles[enLabel] || {};
let fColor = styleConf.nodeFill;
if (!fColor) {
if (node.data.type === '疾病') fColor = '#EF4444';
else if (node.data.type === '药品' || node.data.type === '药物') fColor = '#91cc75';
else if (node.data.type === '症状') fColor = '#fac858';
else if (node.data.type === '检查') fColor = '#336eee';
else fColor = this.nodeFill;
}
return {
...node,
type: styleConf.nodeShape || this.nodeShape,
data: { ...node.data, label: enLabel, name: node.label },
style: {
...node.style,
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
fill: fColor,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
@ -363,19 +409,27 @@ export default {
};
});
// === 3. source label ===
const updatedEdges = data.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const styleConf = this.parsedStyles[sourceEnLabel] || {};
const sourceInfo = nodeIdToData[edge.source];
const styleConf = this.parsedStyles[sourceInfo.enLabel] || {};
let eStroke = styleConf.edgeStroke;
if (!eStroke) {
if (sourceInfo.rawType === '疾病') eStroke = 'rgba(239, 68, 68, 0.4)';
else if (sourceInfo.rawType === '药品' || sourceInfo.rawType === '药物') eStroke = 'rgba(145, 204, 117, 0.4)';
else if (sourceInfo.rawType === '症状') eStroke = 'rgba(250, 200, 88, 0.4)';
else eStroke = this.edgeStroke;
}
return {
...edge,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType ||this.edgeType,
type: styleConf.edgeType || this.edgeType,
data: { ...edge.data, label: edge.data?.label || "" },
style: {
...edge.style,
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
stroke: eStroke,
lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
@ -385,65 +439,68 @@ export default {
};
});
// === 4. ===
let updatedData = {
const finalData = JSON.parse(JSON.stringify({
nodes: updatedNodes,
edges: updatedEdges
};
this.buildNodeLabelMap(updatedNodes);
}));
this.buildNodeLabelMap(finalData.nodes);
const container = this.$refs.graphContainer;
console.log(container)
if (container!=null){
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,
layout: {
type: 'force', //
gravity: 0.3, //
repulsion: 500, //
attraction: 20, //
preventOverlap: true //
plugins: [
{
type: 'toolbar',
key: 'g6-toolbar',
onClick: (id) => {
if (id === 'reset') this.localResetGraph();
else if (this.$refs.toolbarRef) this.$refs.toolbarRef.handleToolbarAction(id);
},
getItems: () => [
{ 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: '导出图谱' },
],
},
behaviors: [ 'zoom-canvas', 'drag-element',
'click-select','focus-element', {
{ 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) => e.target && e.target.id && e.action !== 'drag'
},
{
type: 'drag-canvas',
enable: (event) => event.shiftKey === false,
},
{
type: 'brush-select',
},
{ 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';
// },
fill: (d) => d.style?.fill,
stroke: (d) => d.style?.stroke,
size: (d) => d.style?.size,
lineWidth: (d) => d.style?.lineWidth,
labelText: (d) => d.label,
labelPlacement: 'center',
labelWordWrap: true,
@ -451,55 +508,37 @@ export default {
labelMaxLines: 3,
labelTextOverflow: 'ellipsis',
labelTextAlign: 'center',
labelFill: (d) => d.style?.labelFill,
labelFontSize: (d) => d.style?.labelFontSize,
labelFontFamily: (d) => d.style?.labelFontFamily,
opacity: 1
},
state: {
active: {
lineWidth: 2,
shadowColor: '#ffffff',
shadowBlur: 10,
fill: (d) => d.style?.fill,
stroke: (d) => d.style?.stroke,
lineWidth: 3,
opacity: 1
},
inactive: {
opacity: 0.3
},
normal:{
inactive: { opacity: 0.3 },
normal: {
fill: (d) => d.style?.fill,
stroke: (d) => d.style?.stroke,
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
// }
stroke: (d) => d.style?.stroke,
lineWidth: (d) => d.style?.lineWidth,
endArrow: (d) => d.style?.endArrow,
labelText: (d) => d.data?.label,
labelFill: (d) => d.style?.labelFill,
labelFontSize: (d) => d.style?.labelFontSize,
},
state: {
selected: {
stroke: '#1890FF',
lineWidth: 2,
},
selected: { stroke: '#1890FF', lineWidth: 2 },
highlight: {
halo: true,
haloStroke: '#1890FF',
@ -508,36 +547,22 @@ export default {
lineWidth: 3,
opacity: 1
},
inactive: {
opacity: 0.3
inactive: { opacity: 0.3 },
normal: { opacity: 1 }
},
normal:{
opacity: 1
}
},
},
data:updatedData,
data: finalData,
});
graph.render();
this._graph = graph
this._graph?.fitView()
this._graph = graph;
this._graph?.fitView();
}
},
},
};
</script>
<style scoped>
.medical-qa-container {
padding: 25px;
background-color: #F6F9FF;
@ -724,4 +749,29 @@ export default {
.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>

664
vue/src/system/KGData.vue

@ -84,8 +84,8 @@
</div>
<div class="filter-btns">
<el-button type="primary" class="btn-search-ref" @click="handleNodeSearch">搜索</el-button>
<el-button type="primary" class="btn-import" @click="">导入</el-button>
<el-button type="primary" class="btn-export" @click="">导出</el-button>
<el-button type="primary" class="btn-import" @click="triggerImport('node')">导入</el-button>
<el-button type="warning" class="btn-export" @click="handleExport('node')">导出</el-button>
<el-button class="btn-orange" @click="openNodeDialog(null)">新增节点</el-button>
</div>
</div>
@ -178,6 +178,8 @@
</div>
<div class="filter-btns">
<el-button type="primary" class="btn-search-ref" @click="handleRelSearch">查询</el-button>
<el-button type="primary" class="btn-import" @click="triggerImport('rel')">导入</el-button>
<el-button type="warning" class="btn-export" @click="handleExport('rel')">导出</el-button>
<el-button class="btn-orange" @click="openRelDialog(null)">新增关系</el-button>
</div>
</div>
@ -222,7 +224,90 @@
</div>
</main>
<el-dialog v-model="detailVisible" title="数据详情" style=" border-radius: 7px;" width="550px" destroy-on-close header-class="bold-header">
<input type="file" ref="fileInput" style="display: none" accept=".json" @change="handleFileChange"/>
<el-dialog v-model="importDialogVisible" title="批量导入预检报告" width="550px" class="custom-dialog"
:close-on-click-modal="false" header-class="bold-header">
<div style="display: flex; gap: 16px; padding: 10px 5px;">
<div style="flex-shrink: 0; padding-top: 2px;">
<el-icon v-if="importStep === 'report'" :size="28"
:color="importReport.conflicts.length > 0 ? '#e6a23c' : '#67c23a'">
<WarningFilled v-if="importReport.conflicts.length > 0"/>
<SuccessFilled v-else/>
</el-icon>
<el-icon v-else class="is-loading" :size="28" color="#165dff">
<Loading/>
</el-icon>
</div>
<div style="flex: 1;">
<div v-if="importStep === 'checking'">
<p style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">正在分析数据冲突...</p>
<p style="margin: 8px 0 0; font-size: 14px; color: #606266;">系统正在检查 JSON 文件中的重复数据</p>
</div>
<div v-else-if="importStep === 'report'">
<p style="margin: 0; font-size: 16px; font-weight: bold; color: #303133;">
<span v-if="importReport.invalid.length > 0" style="color: #f56c6c">检测到非法数据</span>
<span v-else-if="importReport.conflicts.length > 0" style="color: #e6a23c">检测到数据冲突</span>
<span v-else style="color: #67c23a;">预检通过</span>
</p>
<p style="margin: 8px 0 15px; font-size: 14px; color: #606266; line-height: 1.6;">
本次导入包含 {{ importReport.summary.total }} 条数据
<span v-if="importReport.invalid.length > 0">
其中 <b style="color: #f56c6c">{{ importReport.invalid.length }}</b> 条数据由于节点不存在无法导入
</span>
<span v-if="importReport.conflicts.length > 0">
另有 <b style="color: #e6a23c">{{ importReport.conflicts.length }}</b> 条已存在
</span>
<span v-if="importReport.summary.valid > 0" style="color: #67c23a;">
可导入 <b>{{ importReport.summary.valid }}</b>
</span>
</p>
<div style="background: #f8f9fb; padding: 10px; border-radius: 6px; margin-bottom: 15px;">
<span style="font-size: 13px; font-weight: bold; color: #606266; margin-right: 10px;">处理策略:</span>
<el-radio-group v-model="importMode" size="small">
<el-radio label="skip">跳过重复</el-radio>
<el-radio label="update">覆盖更新</el-radio>
</el-radio-group>
</div>
<el-table :data="importReport.conflicts.slice(0, 5)" border size="small"
style="width: 100%; font-size: 12px;">
<el-table-column :prop="importType === 'node' ? 'nodeId' : 'source'"
:label="importType === 'node' ? 'ID' : '起点'" show-overflow-tooltip/>
<el-table-column :prop="importType === 'node' ? 'name' : 'target'"
:label="importType === 'node' ? '名称' : '终点'" show-overflow-tooltip/>
<el-table-column
v-if="importReport.invalid.length > 0"
prop="reason"
label="异常原因"
width="180"
style="color: #f56c6c"
/>
</el-table>
</div>
<div v-else-if="importStep === 'uploading'">
<p style="margin: 0 0 10px; font-weight: bold;">正在写入数据库...</p>
<el-progress :percentage="importProgress" :stroke-width="15" striped striped-flow/>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer-wrap" v-if="importStep === 'report'">
<el-button class="btn-cancel" @click="importDialogVisible = false">取消导入</el-button>
<el-button class="btn-confirm" type="primary" @click="startBatchImport"
:disabled="importReport.summary.valid === 0" >{{ importReport.summary.valid === 0 ? '无有效数据' : '开始导入' }}</el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="detailVisible" title="数据详情" style="border-radius: 7px;" width="550px" destroy-on-close
header-class="bold-header">
<el-descriptions :column="1" border>
<el-descriptions-item label="系统 ID (ElementId)">{{ currentDetail.id }}</el-descriptions-item>
<template v-if="detailType === 'node'">
@ -248,7 +333,8 @@
<el-input v-model="nodeForm.name" placeholder="请输入实体名称" clearable/>
</el-form-item>
<el-form-item label="标签" required>
<el-select class="label-select" v-model="nodeForm.label" filterable placeholder="请选择标签" style="width: 100%">
<el-select class="label-select" v-model="nodeForm.label" filterable placeholder="请选择标签"
style="width: 100%">
<el-option v-for="item in dynamicLabels" :key="item" :label="translateToChinese(item)" :value="item"/>
</el-select>
</el-form-item>
@ -310,66 +396,44 @@
<script setup>
import {ref, onMounted, reactive, computed} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {ElMessage, ElMessageBox, ElLoading} from 'element-plus'
import Menu from '@/components/Menu.vue'
import {
getKgStats, getLabels, getRelationshipTypes, getNodeSuggestions, getNodesList, getRelationshipsList,
addNode, updateNode, addRelationship, updateRelationship, deleteNode, deleteRelationship,
fixNodeIds
fixNodeIds,
exportNodes, exportRelationships,
precheckNodes, executeImportNodes,
precheckRelationships, executeImportRelationships
} from '@/api/data'
import {preload} from "@/api/graph";
// --- () ---
// --- ---
const CHINESE_TO_ENGLISH_LABEL = {
"疾病": "Disease",
"症状": "Symptom",
"检查项目": "AuxiliaryExamination",
"药物": "Drug",
"手术": "Operation",
"解剖部位": "CheckSubject",
"并发症": "Complication",
"诊断": "Diagnosis",
"治疗": "Treatment",
"辅助治疗": "AdjuvantTherapy",
"不良反应": "adverseReactions",
"检查": "Check",
"科室": "Department",
"疾病部位": "DiseaseSite",
"相关疾病": "RelatedDisease",
"相关症状": "RelatedSymptom",
"传播途径": "SpreadWay",
"阶段": "Stage",
"药物分类": "Subject",
"症状与体征": "SymptomAndSign",
"治疗方案": "TreatmentPrograms",
"类型": "Type",
"原因": "Cause",
"属性": "Attribute",
"指示/适应症": "Indications",
"成分": "Ingredients",
"病原学": "Pathogenesis",
"病理类型": "PathologicalType",
"发病机制": "Pathophysiology",
"注意事项": "Precautions",
"预后": "Prognosis",
"预后生存时间": "PrognosticSurvivalTime",
"疾病比率": "DiseaseRatio",
"药物治疗": "DrugTherapy",
"感染性": "Infectious",
"关联实体":"RelatedTo",
"人群分组":"MultipleGroups",
"发病率":"DiseaseRate"
"疾病": "Disease", "症状": "Symptom", "检查项目": "AuxiliaryExamination", "药物": "Drug", "手术": "Operation",
"解剖部位": "CheckSubject", "并发症": "Complication", "诊断": "Diagnosis", "治疗": "Treatment",
"辅助治疗": "AdjuvantTherapy", "不良反应": "adverseReactions", "检查": "Check", "部门": "Department",
"疾病部位": "DiseaseSite", "相关疾病": "RelatedDisease", "相关症状": "RelatedSymptom", "传播途径": "SpreadWay",
"阶段": "Stage", "药物分类": "Subject", "症状与体征": "SymptomAndSign", "治疗方案": "TreatmentPrograms",
"类型": "Type", "原因": "Cause", "属性": "Attribute", "指示/适应症": "Indications", "成分": "Ingredients",
"病原学": "Pathogenesis", "病理类型": "PathologicalType", "发病机制": "Pathophysiology", "注意事项": "Precautions",
"预后": "Prognosis", "预后生存时间": "PrognosticSurvivalTime", "疾病比率": "DiseaseRatio", "药物治疗": "DrugTherapy",
"感染性": "Infectious", "关联实体": "RelatedTo", "人群分组": "MultipleGroups", "发病率": "DiseaseRate"
};
//
const dynamicRelTypes = ref([]);
const dynamicLabels = ref([]);
//
const ENGLISH_TO_CHINESE = computed(() => {
const map = {};
// 1.
for (const [chi, eng] of Object.entries(CHINESE_TO_ENGLISH_LABEL)) {
map[eng] = chi;
map[eng.toLowerCase()] = chi;
}
// 2. ()
dynamicRelTypes.value.forEach(item => {
map[item.type] = item.label;
});
@ -399,6 +463,7 @@ const relTotal = ref(0);
const relPage = ref(1);
const relSearch = reactive({source: '', target: '', type: ''});
// --- ---
const nodeDialogVisible = ref(false);
const relDialogVisible = ref(false);
const detailVisible = ref(false);
@ -406,10 +471,25 @@ const detailType = ref('node');
const currentDetail = ref({});
const isEdit = ref(false);
// --- ---
const nodeForm = reactive({id: '', name: '', label: ''});
const relForm = reactive({id: '', source: '', target: '', type: '', label: ''});
// --- ---
// --- ---
const fileInput = ref(null);
const importDialogVisible = ref(false);
const importType = ref('node');
const importStep = ref('checking');
const importProgress = ref(0);
const importMode = ref('skip');
const importRawData = ref([]);
const importReport = reactive({
conflicts: [],
invalid: [],
summary: {total: 0, valid: 0, conflict: 0}
});
//
const handleNodeSizeChange = (val) => {
pageSize.value = val;
nodePage.value = 1;
@ -424,9 +504,11 @@ const handleRelSizeChange = (val) => {
// --- ---
const fetchAllMetadata = async () => {
//
getLabels().then(res => {
if (res?.code === 200) dynamicLabels.value = res.data;
});
//
getRelationshipTypes().then(res => {
if (res?.code === 200) dynamicRelTypes.value = res.data;
});
@ -460,8 +542,8 @@ const fetchNodes = async () => {
label: nodeSearch.label || null
});
if (res?.code === 200) {
nodeData.value = res.data.items;
nodeTotal.value = res.data.total;
nodeData.value = res.data.items || res.data || [];
nodeTotal.value = res.count || res.data.total || 0;
}
} catch (e) {
ElMessage.error('加载节点失败');
@ -470,31 +552,52 @@ const fetchNodes = async () => {
}
};
/**
* 关系列表解析逻辑
*/
const fetchRels = async () => {
loading.value = true;
try {
const res = await getRelationshipsList({
page: relPage.value,
pageSize: pageSize.value,
source: relSearch.source?.trim() || null,
target: relSearch.target?.trim() || null,
type: relSearch.type || null // type
page: relPage.value, pageSize: pageSize.value,
source: relSearch.source?.trim() || null, target: relSearch.target?.trim() || null,
type: relSearch.type || null
});
if (res?.code === 200) {
relData.value = res.data.items;
relTotal.value = res.data.total;
const rawList = Array.isArray(res.data) ? res.data : (res.data.items || []);
relData.value = rawList.map(pathItem => {
const segment = (pathItem.segments && pathItem.segments.length > 0) ? pathItem.segments[0] : null;
const relObj = segment ? segment.relationship : null;
if (relObj) {
return {
id: relObj.elementId || relObj.identity,
elementId: relObj.elementId,
source: pathItem.start?.properties?.name || pathItem.start?.name || '未知',
target: pathItem.end?.properties?.name || pathItem.end?.name || '未知',
type: relObj.type,
label: relObj.properties?.label || relObj.label || relObj.type,
properties: relObj.properties || {}
};
}
return pathItem;
});
relTotal.value = res.count !== undefined ? res.count : (res.data.total || 0);
}
} catch (e) {
console.error("关系解析异常:", e);
ElMessage.error('加载关系失败');
} finally {
loading.value = false;
}
};
//
const openNodeDialog = (row = null) => {
isEdit.value = !!row;
if (row) {
Object.assign(nodeForm, {id: row.id, name: row.name, label: row.labels?.[0] || ''});
Object.assign(nodeForm, {id: row.elementId || row.id, name: row.name, label: row.labels?.[0] || ''});
} else {
Object.assign(nodeForm, {id: '', name: '', label: ''});
}
@ -515,6 +618,8 @@ const submitNode = async () => {
fetchAllMetadata(); //
preload();
fetchAllMetadata();
fetchAllMetadata();
if (typeof preload === 'function') preload();
} else {
ElMessage.error(res?.msg || '操作失败');
}
@ -529,11 +634,11 @@ const openRelDialog = (row = null) => {
isEdit.value = !!row;
if (row) {
Object.assign(relForm, {
id: row.id,
id: row.elementId || row.id,
source: row.source,
target: row.target,
type: row.type,
label: row.label || ''
label: row.label || row.type
});
} else {
Object.assign(relForm, {id: '', source: '', target: '', type: '', label: ''});
@ -565,14 +670,13 @@ const submitRel = async () => {
const handleDelete = (row, type) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
customClass: 'my-custom-btns', //
confirmButtonText: '确认',
cancelButtonText: '取消'
confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
const res = type === 'node' ? await deleteNode(row.id) : await deleteRelationship(row.id);
const targetId = row.elementId || row.id;
const res = type === 'node' ? await deleteNode(targetId) : await deleteRelationship(targetId);
if (res?.code === 200) {
ElMessage.success('删除成功');
preload();
if (typeof preload === 'function') preload();
type === 'node' ? fetchNodes() : fetchRels();
fetchStats();
fetchAllMetadata();
@ -604,18 +708,12 @@ const handleRelSearch = () => {
* 重点将当前 nodeSearch.label 传给后端
*/
const queryNodeSearch = async (queryString, cb) => {
//
if (!nodeSearch.label && !queryString?.trim()) {
return cb([]);
}
const currentLabel = activeName.value === 'first' ? nodeSearch.label : null;
if (!currentLabel && !queryString?.trim()) return cb([]);
try {
// nodeSearch.label
const res = await getNodeSuggestions(queryString || "", nodeSearch.label || null);
const res = await getNodeSuggestions(queryString || "", currentLabel);
if (res?.code === 200) {
//
const results = (res.data || []).map(n => ({value: n}));
cb(results);
cb((res.data || []).map(n => ({value: n})));
} else {
cb([]);
}
@ -625,10 +723,157 @@ const queryNodeSearch = async (queryString, cb) => {
}
};
const handleView = (row, type) => {
detailType.value = type;
currentDetail.value = {...row};
detailVisible.value = true;
/**
* 导出功能 - 增加了二次确认弹窗
*/
const handleExport = async (type) => {
const typeText = type === 'node' ? '节点' : '关系';
// 1.
ElMessageBox.confirm(
`确定要导出当前的${typeText}数据吗?若数据量较大可能需要一定处理时间。`,
'导出确认',
{
confirmButtonText: '立即导出',
cancelButtonText: '取消',
type: 'info',
}
).then(async () => {
// 2.
const loadingInstance = ElLoading.service({
text: `正在导出${typeText}数据...`,
background: 'rgba(0, 0, 0, 0.7)'
});
try {
let res;
let fileName = '';
if (type === 'node') {
res = await exportNodes({
name: nodeSearch.name?.trim() || null,
label: nodeSearch.label || null
});
fileName = `KG_Nodes_${Date.now()}.json`;
} else {
res = await exportRelationships({
source: relSearch.source?.trim() || null,
target: relSearch.target?.trim() || null,
type: relSearch.type || null
});
fileName = `KG_Rels_${Date.now()}.json`;
}
if (res?.code === 200 && res.data) {
const blob = new Blob([JSON.stringify(res.data, null, 2)], {type: "application/json"});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link); // DOM
link.click();
document.body.removeChild(link); //
URL.revokeObjectURL(url);
ElMessage.success(`${typeText}导出完成`);
} else {
ElMessage.error(res?.msg || '导出失败');
}
} catch (error) {
console.error('导出异常:', error);
ElMessage.error('导出异常');
} finally {
loadingInstance.close();
}
}).catch(() => {
//
});
};
//
const triggerImport = (type) => {
importType.value = type;
if (fileInput.value) fileInput.value.click();
};
const handleFileChange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
try {
const json = JSON.parse(event.target.result);
let dataList = [];
if (importType.value === 'node') {
dataList = Array.isArray(json) ? json : (json.nodes || []);
dataList = dataList.map(
item =>
{
// label
const
mappedLabel = CHINESE_TO_ENGLISH_LABEL[item.label] || item.label;
return {
...item,
label: mappedLabel
};
});
} else {
dataList = Array.isArray(json) ? json : (json.relationships || json.rels || []);
}
if (dataList.length === 0) return ElMessage.error("数据为空");
importRawData.value = dataList;
importDialogVisible.value = true;
importStep.value = 'checking';
const res = importType.value === 'node' ? await precheckNodes(dataList) : await precheckRelationships(dataList);
if (res?.code === 200) {
Object.assign(importReport, {
conflicts: res.data.conflicts || [],
invalid: res.data.invalid || [],
summary: res.data.summary || {total: dataList.length}
});
importStep.value = 'report';
} else {
ElMessage.error(res?.msg || "检查失败");
importDialogVisible.value = false;
}
} catch (err) {
ElMessage.error("文件解析失败");
} finally {
e.target.value = '';
}
};
reader.readAsText(file);
};
const startBatchImport = async () => {
if (importReport.summary.valid === 0) {
ElMessage.error("无有效数据可导入,请检查节点是否存在或数据格式。");
return;
}
importStep.value = 'uploading';
importProgress.value = 0;
const total = importRawData.value.length;
const chunkSize = 1000;
try {
for (let i = 0; i < total; i += chunkSize) {
const chunk = importRawData.value.slice(i, i + chunkSize);
const res = importType.value === 'node' ? await executeImportNodes(chunk, importMode.value) : await executeImportRelationships(chunk, importMode.value);
if (res?.code === 200) {
importProgress.value = Math.min(Math.round(((i + chunkSize) / total) * 100), 100);
} else {
throw new Error(res?.msg || "执行失败");
}
}
ElMessage.success(`导入成功`);
importDialogVisible.value = false;
fetchStats();
fetchAllMetadata();
if (typeof preload === 'function') preload();
activeName.value === 'first' ? fetchNodes() : fetchRels();
} catch (err) {
ElMessageBox.alert(`错误:${err.message}`);
importDialogVisible.value = false;
}
};
onMounted(() => {
@ -665,68 +910,233 @@ onMounted(() => {
.input-group-inline :deep(.el-select__wrapper){
box-shadow: none !important;
}
:deep(.input-group-inline .el-autocomplete .el-input__wrapper){
:deep(.input-group-inline .el-autocomplete .el-input__wrapper) {
box-shadow: none !important;
}
:deep(.label-select .el-autocomplete .el-input__wrapper.is-focus){
:deep(.label-select .el-autocomplete .el-input__wrapper.is-focus) {
box-shadow: none !important;
}
:deep(.label-select .el-autocomplete .el-input__wrapper:hover){
:deep(.label-select .el-autocomplete .el-input__wrapper:hover) {
box-shadow: none !important;
}
:deep(.el-form-item__content){box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px; }
.label-select {box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px; }
.btn-search-ref { background: #165dff !important; border-radius: 8px; height: 38px; }
.btn-import { background: rgb(145, 204, 117) !important; border-radius: 8px; height: 38px;border: none }
.btn-export { background: rgb(89, 209, 212) !important; border-radius: 8px; height: 38px;border: none }
.btn-orange { background: #ffb142 !important; color: white !important; border-radius: 8px; height: 38px; border: none !important; }
.table-compact { box-shadow: 0 4px 20px rgba(22, 93, 255, 0.08); overflow: hidden; }
.ref-table :deep(.el-table__header) th { background-color: #e8f0ff !important; color: #2869ff; font-weight: 700; }
.op-group { display: flex; gap: 8px; justify-content: center; }
.ref-op-btn { border: none !important; color: white !important; padding: 6px 14px !important; border-radius: 8px !important; }
.ref-op-btn.edit { background-color: #4379ff !important; }
.ref-op-btn.delete { background-color: #ff6060 !important; }
.ref-op-btn.view { background-color: #ffb142 !important; }
.pagination-footer { margin-top: 20px; display: flex; justify-content: flex-end; }
:deep(.bold-header) { margin-right: 0 !important; display: flex !important; justify-content: flex-start !important; }
:deep(.bold-header .el-dialog__title) { color: #303133 !important;font-weight: 900 !important;padding:5px;margin-bottom: 10px; font-size: 19px !important;}
.custom-form :deep(.el-form-item__label) { color: #606266 !important;font-size: 16px !important; }
.custom-form :deep(.el-input) {box-shadow: 0 0 0 2px #EBF0FF;border: none;border-radius: 5px;}
.custom-form :deep(.el-select__wrapper){
:deep(.el-form-item__content) {
box-shadow: 0 0 0 2px #EBF0FF;
border: none;
border-radius: 5px;
}
.label-select {
box-shadow: 0 0 0 2px #EBF0FF;
border: none;
border-radius: 5px;
}
.btn-search-ref {
background: #165dff !important;
border-radius: 8px;
height: 38px;
}
.btn-import {
background: rgb(145, 204, 117) !important;
border-radius: 8px;
height: 38px;
border: none
}
.btn-export {
background: rgb(89, 209, 212) !important;
border-radius: 8px;
height: 38px;
border: none
}
.btn-orange {
background: #ffb142 !important;
color: white !important;
border-radius: 8px;
height: 38px;
border: none !important;
}
.table-compact {
box-shadow: 0 4px 20px rgba(22, 93, 255, 0.08);
overflow: hidden;
}
.ref-table :deep(.el-table__header) th {
background-color: #e8f0ff !important;
color: #2869ff;
font-weight: 700;
}
.op-group {
display: flex;
gap: 8px;
justify-content: center;
}
.ref-op-btn {
border: none !important;
color: white !important;
padding: 6px 14px !important;
border-radius: 8px !important;
}
.ref-op-btn.edit {
background-color: #4379ff !important;
}
.ref-op-btn.delete {
background-color: #ff6060 !important;
}
.ref-op-btn.view {
background-color: #ffb142 !important;
}
.pagination-footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.bold-header) {
margin-right: 0 !important;
display: flex !important;
justify-content: flex-start !important;
}
:deep(.bold-header .el-dialog__title) {
color: #303133 !important;
font-weight: 900 !important;
padding: 5px;
margin-bottom: 10px;
font-size: 19px !important;
}
.custom-form :deep(.el-form-item__label) {
color: #606266 !important;
font-size: 16px !important;
}
.custom-form :deep(.el-input) {
box-shadow: 0 0 0 2px #EBF0FF;
border: none;
border-radius: 5px;
}
.custom-form :deep(.el-select__wrapper) {
box-shadow: none !important;
}
.custom-form :deep(.el-input__wrapper){
.custom-form :deep(.el-input__wrapper) {
box-shadow: none !important;
}
:deep(.custom-form .el-input__wrapper.is-focus){
:deep(.custom-form .el-input__wrapper.is-focus) {
box-shadow: none !important;
}
:deep(.custom-form .el-input__wrapper:hover){
:deep(.custom-form .el-input__wrapper:hover) {
box-shadow: none !important;
}
.dialog-footer-wrap { display: flex; justify-content: flex-end; gap: 15px; }
.btn-cancel { background-color: #e6e6e6 !important; border: none !important; color: #444 !important; padding: 18px 20px !important; font-weight: 500; }
.btn-confirm { background-color: #165dff !important; border: none !important; padding: 18px 20px !important; font-weight: 500; }
.animate-fade { animation: fadeIn 0.4s ease-out; }
.pagination-custom-text{color: #86909c}
:deep(.el-select__placeholder){color: #606266}
:deep(.el-pagination__sizes .el-select__wrapper){box-shadow: 0 0 0 2px #EBF0FF;}
:deep(.el-pagination.is-background .el-pager li.is-active){background-color: #165DFF;}
.dialog-footer-wrap {
display: flex;
justify-content: flex-end;
gap: 15px;
}
.btn-cancel {
background-color: #e6e6e6 !important;
border: none !important;
color: #444 !important;
padding: 18px 20px !important;
font-weight: 500;
}
.btn-confirm {
background-color: #165dff !important;
border: none !important;
padding: 18px 20px !important;
font-weight: 500;
}
.animate-fade {
animation: fadeIn 0.4s ease-out;
}
.pagination-custom-text {
color: #86909c
}
:deep(.el-select__placeholder) {
color: #606266
}
:deep(.el-pagination__sizes .el-select__wrapper) {
box-shadow: 0 0 0 2px #EBF0FF;
}
:deep(.el-pagination.is-background .el-pager li.is-active) {
background-color: #165DFF;
}
:deep(.el-pager li.is-active) {
color: #fff !important;
}
:deep(.el-dialog){border-radius: 7px}
:deep(.el-input__inner) {color:#606266}
:deep(.el-pagination .btn-next:hover){color: #165DFF !important;}
:deep(.el-pagination .btn-prev:hover){color: #165DFF !important;}
.pagination-footer :deep(.el-pagination .el-input__inner){box-shadow: 0 0 0 2px #EBF0FF;border-radius: 5px; }
.pagination-footer :deep(.el-input__wrapper){ box-shadow: none !important;}
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
:deep(.el-dialog) {
border-radius: 7px
}
:deep(.el-input__inner) {
color: #606266
}
:deep(.el-pagination .btn-next:hover) {
color: #165DFF !important;
}
:deep(.el-pagination .btn-prev:hover) {
color: #165DFF !important;
}
:deep(.custom-dialog .el-dialog__footer) {
padding: 15px 25px 20px !important;
}
:deep(.custom-dialog .el-dialog__body) {
padding: 10px 25px 15px !important;
}
.pagination-footer :deep(.el-pagination .el-input__inner) {
box-shadow: 0 0 0 2px #EBF0FF;
border-radius: 5px;
}
.pagination-footer :deep(.el-input__wrapper) {
box-shadow: none !important;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
<style>
.el-popper .el-select-dropdown__item.is-selected{
.el-popper .el-select-dropdown__item.is-selected {
color: #165DFF !important;
font-weight: bold;
color: #165dff;
@ -803,8 +1213,20 @@ onMounted(() => {
border-radius: 8px 8px 0 0;
}
.my-custom-btns .el-button:not(.el-button--primary){ background-color: #e6e6e6 !important; border: none !important; color: #444 !important; padding: 18px 20px !important; font-weight: 500; }
.my-custom-btns .el-button--primary { background-color: #165dff !important; border: none !important; padding: 18px 20px !important; font-weight: 500; }
.my-custom-btns .el-button:not(.el-button--primary) {
background-color: #e6e6e6 !important;
border: none !important;
color: #444 !important;
padding: 18px 20px !important;
font-weight: 500;
}
.my-custom-btns .el-button--primary {
background-color: #165dff !important;
border: none !important;
padding: 18px 20px !important;
font-weight: 500;
}
.data-card-container {
background: #ffffff;

Loading…
Cancel
Save