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. 225
      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": "查询成功"})
except Exception as 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")

46
service/GraphStyleService.py

@ -87,6 +87,52 @@ class GraphStyleService:
return result
@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:
"""
核心新增应用全案

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'
});
}
/**
* 获取所有图谱样式配置列表
* 保留此接口用于兼容旧版逻辑或后台管理

225
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 Fuse from 'fuse.js';
import {getGraphStyleActive} from "@/api/style";
export default {
name: 'Display',
components: {Menu},
@ -232,7 +233,16 @@ export default {
{ value: 'Disease', label: '疾病' },
{ value: 'Drug', label: '药品' },
{ value: 'Check', label: '检查' }
]
],
configs:[],
parsedStyles:{},
enToZhLabelMap: {
Disease: '疾病',
Drug: '药品',
Check: '检查',
Symptom: '症状',
Other: '其他'
}
}
},
computed: {
@ -307,41 +317,66 @@ export default {
this.treeData=this.diseaseICD10Tree
await this.$nextTick();
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 = {
// === 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,
type: styleConf.nodeShape || this.nodeShape,
style: {
size: styleConf.nodeSize || this.nodeSize,
fill: styleConf.nodeFill || this.nodeFill,
stroke: styleConf.nodeStroke || this.nodeStroke,
lineWidth: styleConf.nodeLineWidth || this.nodeLineWidth,
label: styleConf.nodeShowLabel !== undefined ? styleConf.nodeShowLabel : true,
labelFontSize: styleConf.nodeFontSize || this.nodeFontSize,
labelFontFamily: styleConf.nodeFontFamily || this.nodeFontFamily,
labelFill: styleConf.nodeFontColor || this.nodeFontColor
}
};
});
// === 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,
id: edge.data?.relationship?.id || edge.id,
type: styleConf.edgeType ||this.edgeType,
style: {
endArrow: styleConf.edgeEndArrow !== undefined ? styleConf.edgeEndArrow : true,
stroke: styleConf.edgeStroke || this.edgeStroke,
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
}
};
});
// === 4. ===
let updatedData = {
nodes: updatedNodes,
edges: updatedEdges
}
};
console.log(updatedData)
this.defaultData = updatedData
setTimeout(() => {
setTimeout(() => {
this.initGraph();
this.buildCategoryIndex();
window.addEventListener('resize', this.handleResize);
@ -349,6 +384,50 @@ export default {
} catch (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',
},
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() {
this.isDropdownOpen = !this.isDropdownOpen;
@ -871,22 +980,22 @@ export default {
node: {
style: {
fill: (d) => {
const label = d.data?.label;
if (label === 'Disease') return '#EF4444'; //
if (label === 'Drug') return '#91cc75'; // 绿
if (label === 'Symptom') return '#fac858'; //
if (label === 'Check') return '#336eee'; //
return '#59d1d4'; //
},
stroke: (d) => {
const label = d.data?.label;
if (label === 'Disease') return '#B91C1C';
if (label === 'Drug') return '#047857';
if (label === 'Check') return '#1D4ED8'; //
if (label === 'Symptom') return '#B45309';
return '#40999b';
},
// fill: (d) => {
// const label = d.data?.label;
// if (label === 'Disease') return '#EF4444'; //
// if (label === 'Drug') return '#91cc75'; // 绿
// if (label === 'Symptom') return '#fac858'; //
// if (label === 'Check') return '#336eee'; //
// return '#59d1d4'; //
// },
// stroke: (d) => {
// const label = d.data?.label;
// if (label === 'Disease') return '#B91C1C';
// if (label === 'Drug') return '#047857';
// if (label === 'Check') return '#1D4ED8'; //
// if (label === 'Symptom') return '#B45309';
// return '#40999b';
// },
labelText: (d) => d.data.name,
labelPlacement: 'center',
labelWordWrap: true,
@ -921,16 +1030,16 @@ export default {
edge: {
style: {
labelText: (d) => d.data.relationship.properties.label,
stroke: (d) => {
// target label
const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
// target
if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
if (targetLabel === 'Check') return 'rgba(51,110,238,0.5)'; //
return 'rgba(89,209,212,0.5)'; // default
},
// stroke: (d) => {
// // target label
// const targetLabel = this._nodeLabelMap.get(d.source); // d.target ID
// // target
// if (targetLabel === 'Disease') return 'rgba(239,68,68,0.5)';
// if (targetLabel === 'Drug') return 'rgba(145,204,117,0.5)';
// if (targetLabel === 'Symptom') return 'rgba(250,200,88,0.5)';
// if (targetLabel === 'Check') 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

78
vue/src/system/GraphStyle.vue

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

Loading…
Cancel
Save