Browse Source

all

hanyuqing
hanyuqing 3 months ago
parent
commit
91572f3d10
  1. 9
      controller/GraphStyleController.py
  2. 46
      service/GraphStyleService.py
  3. 7
      vue/src/api/style.js
  4. 213
      vue/src/system/GraphDemo.vue
  5. 78
      vue/src/system/GraphStyle.vue

9
controller/GraphStyleController.py

@ -50,6 +50,15 @@ async def get_grouped_style_list(request):
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"}) return create_response(200, {"code": 200, "data": data, "msg": "查询成功"})
except Exception as e: except Exception as e:
return create_response(200, {"code": 500, "msg": f"查询异常: {str(e)}"}) return create_response(200, {"code": 500, "msg": f"查询异常: {str(e)}"})
@app.get("/api/graph/style/active")
async def get_active_style(request):
"""获取【分组嵌套】格式的配置列表(用于右侧折叠面板)"""
try:
# 调用 Service 的嵌套聚合方法,现在内部已包含 is_active/is_default 逻辑
data = GraphStyleService.get_active_configs()
return create_response(200, {"code": 200, "data": data, "msg": "查询成功"})
except Exception as e:
return create_response(200, {"code": 500, "msg": f"查询异常: {str(e)}"})
@app.post("/api/graph/style/group/apply") @app.post("/api/graph/style/group/apply")

46
service/GraphStyleService.py

@ -87,6 +87,52 @@ class GraphStyleService:
return result return result
@staticmethod @staticmethod
def get_active_configs() -> list:
"""
获取 is_active = 1 的方案组及其配置项
返回长度为 0 1 的列表因为通常只有一个激活组
"""
# 1. 查询 is_active = 1 的组(按 id 排序,取第一个以防有多个)
group_sql = """
SELECT id, group_name, is_active, is_default
FROM graph_style_groups
WHERE is_active = 1
ORDER BY id ASC
LIMIT 1
"""
groups = mysql_client.execute_query(group_sql) or []
if not groups:
return [] # 没有激活的组
group = groups[0]
group_id = group['id']
# 2. 查询该组下的所有配置
configs_sql = """
SELECT id, group_id, canvas_name, current_label, config_json, create_time
FROM graph_configs
WHERE group_id = %s
"""
configs = mysql_client.execute_query(configs_sql, (group_id,)) or []
# 3. 处理配置项
for conf in configs:
if conf.get('config_json'):
try:
conf['styles'] = json.loads(conf['config_json'])
except (TypeError, ValueError):
conf['styles'] = {}
del conf['config_json']
if conf.get('create_time') and not isinstance(conf['create_time'], str):
conf['create_time'] = conf['create_time'].strftime('%Y-%m-%d %H:%M:%S')
# 4. 组装结果
group['configs'] = configs
return [group] # 返回单元素列表,保持接口兼容性
@staticmethod
def apply_group_all(group_id: int) -> bool: def apply_group_all(group_id: int) -> bool:
""" """
核心新增应用全案 核心新增应用全案

7
vue/src/api/style.js

@ -72,6 +72,13 @@ export function getGraphStyleGroups() {
}); });
} }
export function getGraphStyleActive() {
return request({
url: '/api/graph/style/active',
method: 'get'
});
}
/** /**
* 获取所有图谱样式配置列表 * 获取所有图谱样式配置列表
* 保留此接口用于兼容旧版逻辑或后台管理 * 保留此接口用于兼容旧版逻辑或后台管理

213
vue/src/system/GraphDemo.vue

@ -157,6 +157,7 @@ import Menu from "@/components/Menu.vue";
import {a} from "vue-router/dist/devtools-EWN81iOl.mjs"; import {a} from "vue-router/dist/devtools-EWN81iOl.mjs";
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import {getGraphStyleActive} from "@/api/style";
export default { export default {
name: 'Display', name: 'Display',
components: {Menu}, components: {Menu},
@ -232,7 +233,16 @@ export default {
{ value: 'Disease', label: '疾病' }, { value: 'Disease', label: '疾病' },
{ value: 'Drug', label: '药品' }, { value: 'Drug', label: '药品' },
{ value: 'Check', label: '检查' } { value: 'Check', label: '检查' }
] ],
configs:[],
parsedStyles:{},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
Check: '检查',
Symptom: '症状',
Other: '其他'
}
} }
}, },
computed: { computed: {
@ -307,39 +317,64 @@ export default {
this.treeData=this.diseaseICD10Tree this.treeData=this.diseaseICD10Tree
await this.$nextTick(); await this.$nextTick();
try { try {
await this.getDefault()
const response = await getTestGraphData(); // Promise const response = await getTestGraphData(); // Promise
const updatedNodes = response.nodes.map(node => ({ // === 1. nodeId label ===
const nodeIdToEnLabel = {};
response.nodes.forEach(node => {
nodeIdToEnLabel[node.id] = node.data.label; // e.g. "Disease"
});
// === 2. label ===
const updatedNodes = response.nodes.map(node => {
const enLabel = node.data.label;
const zhLabel = this.enToZhLabelMap[enLabel] || '其他'; // 退
const styleConf = this.parsedStyles[zhLabel] || {};
return {
...node, ...node,
type: this.nodeShape, type: styleConf.nodeShape || this.nodeShape,
style: { style: {
size: this.nodeSize, size: styleConf.nodeSize || this.nodeSize,
fill: this.nodeFill, fill: styleConf.nodeFill || this.nodeFill,
stroke: this.nodeStroke, stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: this.nodeLineWidth, lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: this.nodeShowLabel, label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontSize: this.nodeFontSize, labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFontFamily: this.nodeFontFamily, labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: this.nodeFontColor, labelFill: styleConf.nodeFontColor || this.nodeFontColor
} }
})) };
const updatedEdges = response.edges.map(edge => ({ });
// === 3. source label ===
const updatedEdges = response.edges.map(edge => {
console.log(edge)
const sourceEnLabel = nodeIdToEnLabel[edge.source]; // e.g. "Disease"
const sourceZhLabel = this.enToZhLabelMap[sourceEnLabel] || '其他';
const styleConf = this.parsedStyles[sourceZhLabel] || {};
return {
...edge, ...edge,
id: edge.data.relationship.id, id: edge.data?.relationship?.id || edge.id,
type: this.edgeType, type: styleConf.edgeType ||this.edgeType,
style: { style: {
endArrow: this.edgeEndArrow, endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: this.edgeStroke, stroke: styleConf.edgeStroke || this.edgeStroke,
lineWidth: this.edgeLineWidth, lineWidth: styleConf.edgeLineWidth || this.edgeLineWidth,
label: this.edgeShowLabel, label: styleConf.edgeShowLabel !== undefined ? styleConf.edgeShowLabel : false,
labelFontSize: this.edgeFontSize, labelFontSize: styleConf.edgeFontSize || this.edgeFontSize,
labelFontFamily: this.edgeFontFamily, labelFontFamily: styleConf.edgeFontFamily || this.edgeFontFamily,
labelFill: this.edgeFontColor, labelFill: styleConf.edgeFontColor || this.edgeFontColor
}, }
})) };
const updatedData = { });
// === 4. ===
let updatedData = {
nodes: updatedNodes, nodes: updatedNodes,
edges: updatedEdges edges: updatedEdges
} };
console.log(updatedData)
this.defaultData = updatedData this.defaultData = updatedData
setTimeout(() => { setTimeout(() => {
this.initGraph(); this.initGraph();
@ -349,6 +384,50 @@ export default {
} catch (error) { } catch (error) {
console.error('加载图谱数据失败:', error); console.error('加载图谱数据失败:', error);
} }
// try {
// await this.getDefault()
// const response = await getTestGraphData(); // Promise
// const updatedNodes = response.nodes.map(node => ({
// ...node,
// type: this.nodeShape,
// style:{
// size: this.nodeSize,
// fill: this.nodeFill,
// stroke: this.nodeStroke,
// lineWidth: this.nodeLineWidth,
// label: this.nodeShowLabel,
// labelFontSize: this.nodeFontSize,
// labelFontFamily: this.nodeFontFamily,
// labelFill: this.nodeFontColor,
// }
// }))
// const updatedEdges = response.edges.map(edge => ({
// ...edge,
// id: edge.data.relationship.id,
// type: this.edgeType,
// style: {
// endArrow: this.edgeEndArrow,
// stroke: this.edgeStroke,
// lineWidth: this.edgeLineWidth,
// label: this.edgeShowLabel,
// labelFontSize: this.edgeFontSize,
// labelFontFamily: this.edgeFontFamily,
// labelFill: this.edgeFontColor,
// },
// }))
// const updatedData = {
// nodes: updatedNodes,
// edges: updatedEdges
// }
// this.defaultData = updatedData
// setTimeout(() => {
// this.initGraph();
// this.buildCategoryIndex();
// window.addEventListener('resize', this.handleResize);
// }, 1000);
// } catch (error) {
// console.error(':', error);
// }
}, },
@ -384,6 +463,36 @@ export default {
edgeFontFamily: 'updateAllEdges', edgeFontFamily: 'updateAllEdges',
}, },
methods: { methods: {
safeParseStyles(stylesStr) {
try {
return JSON.parse(stylesStr || '{}');
} catch (e) {
console.warn('Failed to parse styles:', stylesStr);
return {};
}
},
async getDefault(){
const response = await getGraphStyleActive();
const data = response.data;
if (!Array.isArray(data) || data.length === 0) {
this.configs = [];
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)
},
// //
toggleDropdown() { toggleDropdown() {
this.isDropdownOpen = !this.isDropdownOpen; this.isDropdownOpen = !this.isDropdownOpen;
@ -871,22 +980,22 @@ export default {
node: { node: {
style: { style: {
fill: (d) => { // fill: (d) => {
const label = d.data?.label; // const label = d.data?.label;
if (label === 'Disease') return '#EF4444'; // // if (label === 'Disease') return '#EF4444'; //
if (label === 'Drug') return '#91cc75'; // 绿 // if (label === 'Drug') return '#91cc75'; // 绿
if (label === 'Symptom') return '#fac858'; // // if (label === 'Symptom') return '#fac858'; //
if (label === 'Check') return '#336eee'; // // if (label === 'Check') return '#336eee'; //
return '#59d1d4'; // // return '#59d1d4'; //
}, // },
stroke: (d) => { // stroke: (d) => {
const label = d.data?.label; // const label = d.data?.label;
if (label === 'Disease') return '#B91C1C'; // if (label === 'Disease') return '#B91C1C';
if (label === 'Drug') return '#047857'; // if (label === 'Drug') return '#047857';
if (label === 'Check') return '#1D4ED8'; // // if (label === 'Check') return '#1D4ED8'; //
if (label === 'Symptom') return '#B45309'; // if (label === 'Symptom') return '#B45309';
return '#40999b'; // return '#40999b';
}, // },
labelText: (d) => d.data.name, labelText: (d) => d.data.name,
labelPlacement: 'center', labelPlacement: 'center',
labelWordWrap: true, labelWordWrap: true,
@ -921,16 +1030,16 @@ export default {
edge: { edge: {
style: { style: {
labelText: (d) => d.data.relationship.properties.label, labelText: (d) => d.data.relationship.properties.label,
stroke: (d) => { // stroke: (d) => {
// target label // // target label
const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID // const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
// target // // target
if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)'; // if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)'; // if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)'; // if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
if (targetLabel === 'Check') return 'rgba(51,110,238,0.5)'; // // if (targetLabel === 'Check') return 'rgba(51,110,238,0.5)'; //
return 'rgba(89,209,212,0.5)'; // default // return 'rgba(89,209,212,0.5)'; // default
}, // },
// labelFill: (d) => { // labelFill: (d) => {
// // target label // // target label
// const targetLabel = this._nodeLabelMap.get(d.target); // d.target ID // const targetLabel = this._nodeLabelMap.get(d.target); // d.target ID

78
vue/src/system/GraphStyle.vue

@ -32,7 +32,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体名称:</label> <label>字体名称</label>
<select v-model="nodeFontFamily"> <select v-model="nodeFontFamily">
<option value="Microsoft YaHei, sans-serif">微软雅黑</option> <option value="Microsoft YaHei, sans-serif">微软雅黑</option>
<option value="SimSun, serif">宋体SimSun</option> <option value="SimSun, serif">宋体SimSun</option>
@ -42,7 +42,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体大小:</label> <label>字体大小</label>
<div class="slider-wrapper"> <div class="slider-wrapper">
<input v-model.number="nodeFontSize" type="range" min="10" max="24" step="1" class="theme-slider"/> <input v-model.number="nodeFontSize" type="range" min="10" max="24" step="1" class="theme-slider"/>
<span class="val-text-black">{{ nodeFontSize }}px</span> <span class="val-text-black">{{ nodeFontSize }}px</span>
@ -50,14 +50,14 @@
</div> </div>
<div class="color-picker-item"> <div class="color-picker-item">
<label>字体颜色:</label> <label>字体颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="nodeFontColor" type="color" class="square-picker"/> <input v-model="nodeFontColor" type="color" class="square-picker"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>图形:</label> <label>图形</label>
<select v-model="nodeShape"> <select v-model="nodeShape">
<option value="circle">圆形</option> <option value="circle">圆形</option>
<option value="diamond">菱形</option> <option value="diamond">菱形</option>
@ -67,7 +67,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>尺寸:</label> <label>尺寸</label>
<input <input
:value="nodeSize" :value="nodeSize"
type="number" type="number"
@ -78,21 +78,21 @@
</div> </div>
<div class="color-picker-item"> <div class="color-picker-item">
<label>填充颜色:</label> <label>填充颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="nodeFill" type="color" class="square-picker"/> <input v-model="nodeFill" type="color" class="square-picker"/>
</div> </div>
</div> </div>
<div class="color-picker-item"> <div class="color-picker-item">
<label>边框颜色:</label> <label>边框颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="nodeStroke" type="color" class="square-picker"/> <input v-model="nodeStroke" type="color" class="square-picker"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>边框尺寸:</label> <label>边框尺寸</label>
<input <input
:value="nodeLineWidth" :value="nodeLineWidth"
type="number" type="number"
@ -116,7 +116,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体名称:</label> <label>字体名称</label>
<select v-model="edgeFontFamily"> <select v-model="edgeFontFamily">
<option value="Microsoft YaHei, sans-serif">微软雅黑</option> <option value="Microsoft YaHei, sans-serif">微软雅黑</option>
<option value="SimSun, serif">宋体</option> <option value="SimSun, serif">宋体</option>
@ -126,7 +126,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>字体大小:</label> <label>字体大小</label>
<div class="slider-wrapper"> <div class="slider-wrapper">
<input v-model.number="edgeFontSize" type="range" min="8" max="16" step="1" class="theme-slider"/> <input v-model.number="edgeFontSize" type="range" min="8" max="16" step="1" class="theme-slider"/>
<span class="val-text-black">{{ edgeFontSize }}px</span> <span class="val-text-black">{{ edgeFontSize }}px</span>
@ -134,14 +134,14 @@
</div> </div>
<div class="color-picker-item"> <div class="color-picker-item">
<label>字体颜色:</label> <label>字体颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="edgeFontColor" type="color" class="square-picker"/> <input v-model="edgeFontColor" type="color" class="square-picker"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>连边类型:</label> <label>连边类型</label>
<select v-model="edgeType"> <select v-model="edgeType">
<option value="line">直线</option> <option value="line">直线</option>
<option value="polyline">折线</option> <option value="polyline">折线</option>
@ -151,7 +151,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>线粗细:</label> <label>线粗细</label>
<input <input
:value="edgeLineWidth" :value="edgeLineWidth"
type="number" type="number"
@ -162,7 +162,7 @@
</div> </div>
<div class="color-picker-item"> <div class="color-picker-item">
<label>线条颜色:</label> <label>线条颜色</label>
<div class="color-picker-border"> <div class="color-picker-border">
<input v-model="edgeStroke" type="color" class="square-picker"/> <input v-model="edgeStroke" type="color" class="square-picker"/>
</div> </div>
@ -304,7 +304,7 @@ import {
deleteGraphStyle, deleteGraphStyle,
batchDeleteGraphStyle, batchDeleteGraphStyle,
deleteGraphStyleGroup, deleteGraphStyleGroup,
applyGraphStyleGroup applyGraphStyleGroup, getGraphStyleActive
} from '@/api/style'; } from '@/api/style';
import {ElMessageBox, ElMessage} from 'element-plus'; import {ElMessageBox, ElMessage} from 'element-plus';
import {markRaw} from 'vue'; import {markRaw} from 'vue';
@ -882,7 +882,7 @@ export default {
} }
.control-panel { .control-panel {
width: 260px; width: 250px;
background: #ffffff; background: #ffffff;
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.08); box-shadow: 4px 0 12px rgba(0, 0, 0, 0.08);
padding: 10px; padding: 10px;
@ -894,7 +894,7 @@ export default {
} }
.config-list-panel { .config-list-panel {
width: 320px; width: 250px;
background: #ffffff; background: #ffffff;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.08); box-shadow: -4px 0 12px rgba(0, 0, 0, 0.08);
padding: 18px; padding: 18px;
@ -906,14 +906,15 @@ export default {
} }
.panel-header-container { .panel-header-container {
margin-bottom: 15px; margin-bottom: 10px;
} }
.panel-header { .panel-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 8px; padding-bottom: 10px;
border-bottom: 1px solid #E2E6F3;
} }
/*.header-line { /*.header-line {
@ -939,7 +940,7 @@ export default {
.tag-pill { .tag-pill {
flex-shrink: 0; flex-shrink: 0;
padding: 0 10px; padding: 1px 10px;
border-radius: 20px; border-radius: 20px;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
@ -958,14 +959,11 @@ export default {
background-color: #4a68db; background-color: #4a68db;
} }
.section {
margin-bottom: 25px;
}
.section-title { .section-title {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 16px; font-size: 14px;
font-weight: bold; font-weight: bold;
color: #334155; color: #334155;
margin-bottom: 12px; margin-bottom: 12px;
@ -979,7 +977,7 @@ export default {
position: absolute; position: absolute;
left: 0; left: 0;
width: 4px; width: 4px;
height: 16px; height: 15px;
background-color: #1559f3; background-color: #1559f3;
border-radius: 2px; border-radius: 2px;
} }
@ -988,15 +986,16 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
font-size: 14px; font-size: 13px;
margin-bottom: 12px; margin-bottom: 10px;
color: #475569; color: #475569;
} }
.checkbox-label { .checkbox-label {
width: 150px; width: 80px;
flex-shrink: 0; flex-shrink: 0;
text-align: left; text-align: left;
margin-right: 8px;
} }
.theme-checkbox { .theme-checkbox {
@ -1009,13 +1008,15 @@ export default {
.form-group, .color-picker-item { .form-group, .color-picker-item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 10px;
font-size: 14px; font-size: 13px;
} }
.form-group label, .color-picker-item label { .form-group label, .color-picker-item label {
width: 80px; width: 80px;
flex-shrink: 0; flex-shrink: 0;
text-align: right;
margin-right: 8px;
} }
.form-group select, .form-group input[type="number"] { .form-group select, .form-group input[type="number"] {
@ -1023,6 +1024,7 @@ export default {
padding: 5px; padding: 5px;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 4px; border-radius: 4px;
width: 100px;
} }
.slider-wrapper { .slider-wrapper {
@ -1075,9 +1077,6 @@ export default {
} }
.button-footer { .button-footer {
display: flex;
gap: 10px;
padding-top: 10px;
} }
.btn-confirm-save { .btn-confirm-save {
@ -1085,10 +1084,12 @@ export default {
color: #fff; color: #fff;
border: none; border: none;
flex: 1; flex: 1;
padding: 10px; padding: 5px 14px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-weight: bold; width: 100px;
font-size: 12px;
margin-right: 15px;
} }
.btn-reset-style { .btn-reset-style {
@ -1096,9 +1097,11 @@ export default {
color: #1559f3; color: #1559f3;
border: 1px solid #1559f3; border: 1px solid #1559f3;
flex: 1; flex: 1;
padding: 10px; padding: 5px 14px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
width: 100px;
font-size: 12px;
} }
.graph-container { .graph-container {
@ -1142,11 +1145,10 @@ export default {
.card-using { .card-using {
background-color: #eff6ff !important; background-color: #eff6ff !important;
outline: 1.5px solid #1559f3;
} }
.card-checked { .card-checked {
border-left: 4px solid #ef4444 !important; border-left: 4px solid rgb(239, 68, 68) !important;
} }

Loading…
Cancel
Save