Files
qhmes/.trae/skills/jimubi-dashboard/references/bi_utils.py

1158 lines
38 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
"""
JeecgBoot 大屏/仪表盘设计器 Python 工具库
用于通过 API 自动创建和管理大屏仪表盘页面及其组件
使用方式
from bi_utils import *
init_api('https://api3.boot.jeecg.com', 'your-token')
page_id = create_page('销售大屏', style='bigScreen')
add_number(page_id, '销售额', x=50, y=50, w=400, h=200, value=128560)
add_chart(page_id, 'JBar', '月度销售', x=50, y=280, w=860, h=380,
categories=['1月','2月','3月'], series=[{'name':'销售额','data':[820,932,901]}])
save_page(page_id)
"""
import json
import urllib.request
import urllib.parse
import time
import random
import uuid
# ============================================================
# 全局配置
# ============================================================
API_BASE = ''
TOKEN = ''
# 内存中缓存页面组件数据save_page 时一次性提交
_page_components = {} # {page_id: [component_dict, ...]}
_page_info = {} # {page_id: {name, style, theme, ...}}
# ============================================================
# 大屏 vs 仪表盘 模式预设(颜色、样式完全不同)
# ============================================================
# 大屏bigScreen- 深色背景,亮色文字
_BIGSCREEN = {
'bg': 'rgba(0,0,0,0)',
'border_color': '',
'title_color': '#ffffff',
'axis_color': '#ffffff',
'grid_color': 'rgba(255,255,255,0.1)',
'body_color': '#ffffff',
'suffix_color': '#ffffff',
'legend_color': '#ffffff',
'tooltip_color': '#ffffff',
'card': {'title': '', 'extra': '', 'rightHref': '', 'size': 'small'},
'number_font_size': 32,
# 表格
'table_header_bg': 'rgba(0,0,0,0.3)',
'table_header_color': '#ffffff',
'table_body_bg': 'rgba(0,0,0,0.1)',
'table_body_color': '#ffffff',
'table_body_font_size': 14,
'table_header_font_size': 14,
# 滚动表格
'scroll_odd_color': '#0a2732',
'scroll_even_color': '#003b51',
'scroll_header_bg': '#0a73ff',
'scroll_header_color': '#ffffff',
'scroll_body_color': '#ffffff',
'scroll_border_color': 'rgba(255,255,255,0.1)',
# 排行榜
'ranking_color': '#1370fb',
'ranking_text_color': '#fff',
}
# 仪表盘default- 亮色背景,深色文字,带卡片头
_DASHBOARD = {
'bg': '#FFFFFF',
'border_color': '#E8E8E8',
'title_color': '#464646',
'axis_color': '#909198',
'grid_color': '#F3F3F3',
'body_color': '#464646',
'suffix_color': '#909198',
'legend_color': '#464646',
'tooltip_color': '#464646',
'card': {
'title': '', # 由各函数填充
'extra': '', 'rightHref': '',
'size': 'default',
'headColor': '#FFFFFF',
'textStyle': {'color': '#464646', 'fontSize': 16, 'fontWeight': 'bold'},
},
'number_font_size': 32,
# 表格
'table_header_bg': '#FAFAFA',
'table_header_color': '#464646',
'table_body_bg': '#FFFFFF',
'table_body_color': '#666666',
'table_body_font_size': 13,
'table_header_font_size': 14,
# 滚动表格
'scroll_odd_color': '#FFFFFF',
'scroll_even_color': '#FAFAFA',
'scroll_header_bg': '#F0F0F0',
'scroll_header_color': '#464646',
'scroll_body_color': '#666666',
'scroll_border_color': '#E8E8E8',
# 排行榜
'ranking_color': '#1890FF',
'ranking_text_color': '#464646',
}
def _get_mode(page_id):
"""获取页面模式预设(大屏 or 仪表盘)"""
info = _page_info.get(page_id, {})
style = info.get('style', 'bigScreen')
if style == 'default':
return _DASHBOARD
return _BIGSCREEN
def _make_card(mode, title):
"""根据模式创建 card 配置。
大屏模式card.title 保持为空标题由 ECharts option.title 显示避免重复
仪表盘模式card.title 设置标题卡片头显示
"""
card = dict(mode['card'])
if 'textStyle' in mode['card']:
card['textStyle'] = dict(mode['card']['textStyle'])
# 大屏不用 card 标题头,仪表盘用
if mode is _DASHBOARD:
card['title'] = title
else:
card['title'] = ''
return card
def init_api(api_base, token):
"""初始化 API 地址和 Token"""
global API_BASE, TOKEN
API_BASE = api_base.rstrip('/')
TOKEN = token
print(f'[bi_utils] API: {API_BASE}')
# ============================================================
# HTTP 工具
# ============================================================
def _request(method, path, data=None, params=None):
"""发送 HTTP 请求"""
url = f'{API_BASE}{path}'
if params:
url += '?' + urllib.parse.urlencode(params)
headers = {
'Content-Type': 'application/json;charset=UTF-8',
'X-Access-Token': TOKEN,
}
body = None
if data is not None:
body = json.dumps(data, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read().decode('utf-8'))
return result
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else ''
print(f'[bi_utils] HTTP {e.code}: {error_body}')
raise
except Exception as e:
print(f'[bi_utils] Request error: {e}')
raise
def _gen_key():
"""生成唯一 key"""
return f'{int(time.time() * 1000)}_{random.randint(100000, 999999)}'
def _gen_uuid():
"""生成 32 位无横线 UUID"""
return uuid.uuid4().hex
# ============================================================
# 页面管理 API
# ============================================================
def create_page(name, style='bigScreen', theme=None, background_image=None,
type_id='0', design_type=100, protection_code=''):
"""
创建大屏或仪表盘页面
Args:
name: 页面名称
style: 'bigScreen'=大屏, 'default'=仪表盘
theme: 主题默认大屏=dark仪表盘=default
background_image: 背景图路径大屏默认 '/img/bg/bg4.png'
type_id: 分类 ID默认 '0'
design_type: 设计类型 100=PC, 30=手机, 80=平板
protection_code: 保护密码
Returns:
page_id: 页面 ID
"""
if theme is None:
theme = 'dark' if style == 'bigScreen' else 'default'
if background_image is None and style == 'bigScreen':
background_image = '/img/bg/bg4.png'
payload = {
'name': name,
'type': type_id,
'protectionCode': protection_code,
'theme': theme,
'style': style,
'backgroundImage': background_image or '',
'designType': design_type,
}
result = _request('POST', '/drag/page/add', data=payload)
if not result.get('success'):
raise Exception(f"创建页面失败: {result.get('message', json.dumps(result, ensure_ascii=False))}")
page_data = result.get('result', {})
page_id = page_data.get('id')
if not page_id:
raise Exception(f"创建页面成功但未返回 ID: {json.dumps(result, ensure_ascii=False)}")
# 缓存页面信息
_page_components[page_id] = []
_page_info[page_id] = {
'name': name,
'style': style,
'theme': theme,
'backgroundImage': background_image or '',
'designType': design_type,
'updateCount': page_data.get('updateCount', 1),
'path': page_data.get('path', ''),
}
print(f'[bi_utils] 页面创建成功: {name} (ID: {page_id})')
return page_id
def query_page(page_id):
"""查询页面详情(含组件配置)"""
result = _request('GET', '/drag/page/queryById', params={'id': page_id})
if not result.get('success'):
raise Exception(f"查询页面失败: {result.get('message')}")
page = result.get('result', {})
# 更新缓存
if page_id not in _page_info:
_page_info[page_id] = {}
_page_info[page_id]['updateCount'] = page.get('updateCount', 1)
_page_info[page_id]['name'] = page.get('name', '')
# 解析 template
template = page.get('template')
if template and isinstance(template, str):
try:
page['template'] = json.loads(template)
except:
pass
return page
def list_pages(style=None, page_no=1, page_size=50):
"""列表查询页面"""
params = {'pageNo': page_no, 'pageSize': page_size}
if style:
params['style'] = style
result = _request('GET', '/drag/page/list', params=params)
if not result.get('success'):
raise Exception(f"查询列表失败: {result.get('message')}")
return result.get('result', {})
def save_page(page_id):
"""
保存页面设计将所有缓存的组件一次性提交
通过 POST /drag/page/edit 提交后端会
1. 删除所有旧的 OnlDragPageComp 记录
2. template 中提取 config 创建新的 comp 记录
3. 更新 template移除 config注入 pageCompId
"""
components = _page_components.get(page_id, [])
info = _page_info.get(page_id, {})
# 始终查询最新页面信息,确保 updateCount 正确
try:
page = query_page(page_id)
info = _page_info.get(page_id, {})
# 如果本地没有新增组件,使用已有的
if not components:
existing_template = page.get('template', [])
if isinstance(existing_template, list):
components = existing_template
except Exception as e:
print(f'[bi_utils] 查询页面警告: {e},使用缓存信息')
# 构建 template JSON
template = json.dumps(components, ensure_ascii=False)
payload = {
'id': page_id,
'name': info.get('name', ''),
'template': template,
'updateCount': info.get('updateCount', 1),
'style': info.get('style', 'bigScreen'),
'theme': info.get('theme', 'dark'),
'backgroundImage': info.get('backgroundImage', ''),
'designType': info.get('designType', 100),
}
result = _request('POST', '/drag/page/edit', data=payload)
if not result.get('success'):
raise Exception(f"保存页面失败: {result.get('message')}")
# 更新 updateCount
new_count = result.get('result', {})
if isinstance(new_count, dict):
info['updateCount'] = new_count.get('updateCount', info.get('updateCount', 1) + 1)
else:
info['updateCount'] = info.get('updateCount', 1) + 1
print(f'[bi_utils] 页面保存成功: {info.get("name", page_id)} ({len(components)} 个组件)')
return True
def delete_page(page_id, physical=False):
"""
删除页面
Args:
page_id: 页面 ID
physical: True=物理删除彻底False=逻辑删除回收站
"""
if physical:
result = _request('DELETE', '/drag/page/physicalDelete', params={'id': page_id})
else:
result = _request('DELETE', '/drag/page/delete', params={'id': page_id})
if not result.get('success'):
raise Exception(f"删除页面失败: {result.get('message')}")
# 清理缓存
_page_components.pop(page_id, None)
_page_info.pop(page_id, None)
print(f'[bi_utils] 页面删除成功: {page_id} ({"物理删除" if physical else "逻辑删除"})')
return True
def recover_page(page_id):
"""恢复回收站中的页面"""
result = _request('POST', '/drag/page/recoveryDelete', data={'id': page_id})
if not result.get('success'):
raise Exception(f"恢复页面失败: {result.get('message')}")
print(f'[bi_utils] 页面恢复成功: {page_id}')
return True
def copy_page(page_id):
"""复制页面"""
result = _request('GET', '/drag/page/copyPage', params={'id': page_id})
if not result.get('success'):
raise Exception(f"复制页面失败: {result.get('message')}")
new_page = result.get('result', {})
new_id = new_page.get('id')
print(f'[bi_utils] 页面复制成功: {page_id}{new_id}')
return new_id
# ============================================================
# 组件添加函数
# ============================================================
def add_component(page_id, component, title, x, y, w, h, config=None):
"""
添加通用组件到页面
Args:
page_id: 页面 ID
component: 组件类型 'JBar', 'JNumber', 'JTable'
title: 组件标题
x, y: 位置大屏=像素仪表盘=栅格
w, h: 尺寸大屏=像素仪表盘=栅格
config: 组件配置 dict可选会与默认配置合并
Returns:
component dict已加入缓存
"""
if page_id not in _page_components:
_page_components[page_id] = []
key = _gen_key()
# 栅格单位转换为像素(仪表盘模式)
info = _page_info.get(page_id, {})
style = info.get('style', 'bigScreen')
if style == 'default':
px_w = w * 75
px_h = h * 11
else:
px_w = w
px_h = h
# 基础配置
default_config = {
'dataType': 1,
'timeOut': 0,
'size': {'width': px_w, 'height': px_h},
'chart': {
'subclass': component,
'category': _get_category(component),
},
'option': {},
'chartData': [],
'linkageConfig': [],
'turnConfig': {'url': '', 'type': '_blank'},
'linkType': 'url',
}
# 合并用户配置
if config:
_deep_merge(default_config, config)
comp = {
'component': component,
'i': key,
'x': x,
'y': y,
'w': w,
'h': h,
'pcX': x,
'pcY': y,
'pcW': w,
'orderNum': len(_page_components[page_id]),
'config': json.dumps(default_config, ensure_ascii=False),
}
_page_components[page_id].append(comp)
return comp
def add_number(page_id, title, x, y, w, h, value=0, prefix='', suffix='',
font_size=None, color=None, bg_color=None):
"""
添加数字指标组件JNumber
自动根据页面模式大屏/仪表盘应用不同默认样式
Args:
value: 显示的数值
prefix: 前缀 '¥'
suffix: 后缀 '', '%'
font_size: 字体大小默认根据模式自动设置
color: 字体颜色默认根据模式自动设置
"""
mode = _get_mode(page_id)
if font_size is None:
font_size = mode['number_font_size']
if color is None:
color = mode['body_color']
config = {
'dataType': 1,
'chartData': json.dumps({'value': value}, ensure_ascii=False),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 14}},
'body': {
'text': '',
'color': color,
'fontSize': font_size,
'fontWeight': 'bold',
'marginLeft': 0,
'marginTop': 0,
},
'card': _make_card(mode, title),
'prefix': prefix,
'prefixColor': mode['suffix_color'],
'prefixFontSize': 14,
'suffix': suffix,
'suffixColor': mode['suffix_color'],
'suffixFontSize': 14,
'isCompare': False,
'trendType': '1',
},
'analysis': {
'isCompare': False,
'compareType': '',
'trendType': '1',
},
}
return add_component(page_id, 'JNumber', title, x, y, w, h, config)
def add_chart(page_id, chart_type, title, x, y, w, h,
categories=None, series=None, pie_data=None):
"""
添加图表组件
支持的 chart_type: JBar, JLine, JSmoothLine, JHorizontalBar, JStackBar,
JMixLineBar, DoubleLineBar, JPie, JRing, JRose, JFunnel, JRadar, JScatter,
JGauge, JLiquid, JProgress, JWordCloud, JAreaMap, JFlyLineMap, 等等
Args:
chart_type: 组件类型
categories: X轴类目数据 ['一月','二月',...]轴类图表用
series: 系列数据 [{'name':'系列1','data':[1,2,3]}]轴类图表用
pie_data: 饼图数据 [{'name':'A','value':10}]//玫瑰图用
"""
# 确定图表 ECharts 类型
echart_type_map = {
'JBar': 'bar', 'JHorizontalBar': 'bar', 'JBackgroundBar': 'bar',
'JMultipleBar': 'bar', 'JNegativeBar': 'bar', 'JStackBar': 'bar',
'JLine': 'line', 'JSmoothLine': 'line', 'JStepLine': 'line',
'JMultipleLine': 'line',
'JMixLineBar': 'bar', # 混合类型
'JPie': 'pie', 'JRing': 'pie', 'JRose': 'pie',
'JFunnel': 'funnel',
'JRadar': 'radar',
'JScatter': 'scatter', 'JBubble': 'scatter',
'JGauge': 'gauge',
}
echart_type = echart_type_map.get(chart_type, 'bar')
# 构建 chartData
chart_data = []
if pie_data:
chart_data = pie_data
elif categories and series:
if len(series) == 1:
# 单系列:简单 name/value
for i, cat in enumerate(categories):
chart_data.append({
'name': cat,
'value': series[0]['data'][i] if i < len(series[0]['data']) else 0,
})
else:
# 多系列:需要 type 字段区分
for s in series:
for i, cat in enumerate(categories):
chart_data.append({
'name': cat,
'value': s['data'][i] if i < len(s['data']) else 0,
'type': s.get('name', ''),
})
mode = _get_mode(page_id)
# 构建 ECharts option根据模式应用不同颜色
option = {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 16},
'subtextStyle': {'color': mode['axis_color']}},
'tooltip': {'show': True, 'textStyle': {'color': mode['tooltip_color']}},
'legend': {'show': len(series or []) > 1 or bool(pie_data),
'textStyle': {'color': mode['legend_color'], 'fontSize': 12}},
'grid': {'left': 60, 'right': 30, 'top': 70, 'bottom': 40, 'show': False},
'card': _make_card(mode, title),
}
if pie_data:
# 饼图系列
radius = '55%'
if chart_type == 'JRing':
radius = ['40%', '55%']
elif chart_type == 'JRose':
radius = ['20%', '55%']
option['tooltip']['trigger'] = 'item'
if chart_type in ('JPie', 'JRing', 'JRose'):
option['legend']['orient'] = 'vertical'
option['series'] = [{
'name': title,
'type': 'pie',
'radius': radius,
'data': pie_data,
'emphasis': {'itemStyle': {'shadowBlur': 10, 'shadowOffsetX': 0,
'shadowColor': 'rgba(0,0,0,0.5)'}},
}]
elif categories and series:
# 轴类图表
option['xAxis'] = {
'type': 'category',
'show': True,
'data': categories,
'axisLabel': {'color': mode['axis_color']},
'axisLine': {'lineStyle': {'color': mode['grid_color']}},
}
option['yAxis'] = {
'type': 'value',
'show': True,
'axisLabel': {'color': mode['axis_color']},
'splitLine': {'lineStyle': {'color': mode['grid_color']}},
}
if chart_type == 'JHorizontalBar':
option['xAxis'], option['yAxis'] = option['yAxis'], option['xAxis']
option['yAxis']['data'] = categories
option['yAxis']['type'] = 'category'
option['xAxis'] = {
'type': 'value', 'show': True,
'axisLabel': {'color': mode['axis_color']},
'axisLine': {'lineStyle': {'color': mode['grid_color']}},
'splitLine': {'lineStyle': {'color': mode['grid_color']}},
}
option['series'] = []
for s in series:
series_item = {
'name': s.get('name', ''),
'type': echart_type,
'data': s.get('data', []),
}
if chart_type == 'JSmoothLine':
series_item['smooth'] = True
if chart_type == 'JStepLine':
series_item['step'] = 'middle'
if chart_type == 'JStackBar':
series_item['stack'] = 'total'
option['series'].append(series_item)
# 仪表盘模式card.title 留空,仅用 option.title 显示标题(避免重复)
option['card'] = _make_card(mode, '')
config = {
'dataType': 1,
'chartData': json.dumps(chart_data, ensure_ascii=False),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': option,
}
return add_component(page_id, chart_type, title, x, y, w, h, config)
def add_table(page_id, title, x, y, w, h, columns=None, data=None):
"""
添加数据表格组件JTable
Args:
columns: 列名列表 ['姓名', '年龄', '地址']
data: 数据行列表 [{'姓名':'张三','年龄':'28','地址':'北京'}]
"""
columns = columns or []
data = data or []
chart_data = []
for col in columns:
field_name = col.lower().replace(' ', '_')
chart_data.append({
'fieldTxt': col,
'fieldName': field_name,
'type': 'field',
'isShow': 'Y',
'isTotal': 'N',
})
mode = _get_mode(page_id)
config = {
'dataType': 1,
'chartData': json.dumps(chart_data, ensure_ascii=False),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
'bordered': True,
'size': 'small',
'headerBgColor': mode['table_header_bg'],
'headerColor': mode['table_header_color'],
'headerFontSize': mode['table_header_font_size'],
'bodyBgColor': mode['table_body_bg'],
'bodyColor': mode['table_body_color'],
'bodyFontSize': mode['table_body_font_size'],
'card': _make_card(mode, title),
'data': data,
},
}
return add_component(page_id, 'JTable', title, x, y, w, h, config)
def add_scroll_table(page_id, title, x, y, w, h, columns=None, data=None):
"""
添加自动滚动表格组件JScrollTable
Args:
columns: 列名列表
data: 数据行列表二维数组格式
"""
columns = columns or []
data = data or []
# JScrollTable expects chartData as array of objects
# columns maps to option.fieldMapping
field_mapping = []
for col in columns:
field_name = col.lower().replace(' ', '_')
field_mapping.append({'name': col, 'key': field_name, 'width': 0})
# Convert data rows: if data is list of lists, convert to list of dicts
chart_data = []
if data and isinstance(data[0], (list, tuple)):
for row in data:
item = {}
for j, col in enumerate(columns):
field_name = col.lower().replace(' ', '_')
item[field_name] = row[j] if j < len(row) else ''
chart_data.append(item)
elif data and isinstance(data[0], dict):
chart_data = data
else:
chart_data = data or []
mode = _get_mode(page_id)
config = {
'dataType': 1,
'chartData': json.dumps(chart_data, ensure_ascii=False),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
'ranking': False,
'textPosition': 'center',
'lineHeight': 50,
'fontSize': 20,
'bodyFontSize': 18,
'scrollTime': 50,
'scroll': True,
'showBorder': True,
'borderWidth': 1,
'borderColor': mode['scroll_border_color'],
'borderStyle': 'solid',
'showHead': True,
'bodyFontColor': mode['scroll_body_color'],
'oddColor': mode['scroll_odd_color'],
'evenColor': mode['scroll_even_color'],
'headerBgColor': mode['scroll_header_bg'],
'headerFontColor': mode['scroll_header_color'],
'fieldMapping': field_mapping,
'card': _make_card(mode, title),
},
}
return add_component(page_id, 'JScrollTable', title, x, y, w, h, config)
def add_ranking(page_id, title, x, y, w, h, data=None):
"""
添加排行榜组件JScrollRankingBoard
Args:
data: 排行数据 [{'name':'项目A','value':100}, ...]
"""
data = data or []
mode = _get_mode(page_id)
config = {
'dataType': 1,
'chartData': json.dumps(data, ensure_ascii=False),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
'waitTime': 2000,
'rowNum': 5,
'carousel': 'single',
'sort': True,
'fontSize': 13,
'color': mode['ranking_color'],
'textColor': mode['ranking_text_color'],
'card': _make_card(mode, title),
},
}
return add_component(page_id, 'JScrollRankingBoard', title, x, y, w, h, config)
def add_text(page_id, title, x, y, w, h, content='', font_size=16, color=None,
font_weight='normal', text_align='left', letter_spacing=0):
"""
添加文本组件JText
使用与真实模板一致的 option.body 结构 + chartData: {"value": "..."} 格式
Args:
content: 文本内容
font_size: 字体大小
color: 字体颜色默认根据模式自动设置
font_weight: 字体粗细 'normal'/'bold'
text_align: 对齐方式 'left'/'center'/'right'
letter_spacing: 字间距大屏标题建议 5-8
"""
mode = _get_mode(page_id)
if color is None:
color = mode['title_color']
text_value = content or title
config = {
'dataType': 1,
'chartData': {'value': text_value},
'option': {
'body': {
'color': color,
'fontSize': font_size,
'fontWeight': font_weight,
'letterSpacing': letter_spacing,
'text': '',
'marginTop': 0,
'marginLeft': 0,
},
'textAlign': text_align,
'card': {'title': '', 'extra': '', 'rightHref': '', 'size': 'default'},
'openUrl': '',
'isLink': False,
'openType': '_blank',
},
}
return add_component(page_id, 'JText', title, x, y, w, h, config)
def add_image(page_id, title, x, y, w, h, src=''):
"""
添加图片组件JImg
Args:
src: 图片 URL
"""
config = {
'dataType': 1,
'chartData': src,
'option': {
'objectFit': 'cover',
},
}
return add_component(page_id, 'JImg', title, x, y, w, h, config)
def add_gauge(page_id, title, x, y, w, h, value=0, max_val=100,
unit='%', color='#00BAFF'):
"""
添加仪表盘组件JGauge
Args:
value: 当前值
max_val: 最大值
unit: 单位
color: 指针颜色
"""
mode = _get_mode(page_id)
tail_color = '#333' if mode is _BIGSCREEN else '#E8E8E8'
config = {
'dataType': 1,
'chartData': json.dumps([{'name': title, 'value': value}], ensure_ascii=False),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
'card': _make_card(mode, title),
'series': [{
'type': 'gauge',
'max': max_val,
'detail': {'formatter': f'{{value}}{unit}'},
'data': [{'value': value, 'name': title}],
'axisLine': {
'lineStyle': {
'color': [[value / max_val, color], [1, tail_color]],
}
},
}],
},
}
return add_component(page_id, 'JGauge', title, x, y, w, h, config)
def add_liquid(page_id, title, x, y, w, h, value=50, color='#00BAFF'):
"""
添加水球图组件JLiquid
Args:
value: 0~100 的百分比值 97.3 表示 97.3%
color: 颜色
"""
mode = _get_mode(page_id)
config = {
'dataType': 1,
'chartData': json.dumps([{'value': value}], ensure_ascii=False),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
'liquidType': 'circle',
'color': color,
'borderWidth': 2,
'distance': 1,
'borderColor': color,
'strokeOpacity': 0,
'count': 4,
'length': 128,
'textColor': mode['title_color'],
'textFontSize': 30,
'card': _make_card(mode, title),
},
}
return add_component(page_id, 'JLiquid', title, x, y, w, h, config)
def add_countdown(page_id, title, x, y, w, h, value=0, font_size=48, color='#00BAFF'):
"""
添加数字翻牌器组件JCountTo
Args:
value: 目标数值
font_size: 字体大小
color: 字体颜色
"""
mode = _get_mode(page_id)
if color == '#00BAFF' and mode is _DASHBOARD:
color = mode['body_color']
config = {
'dataType': 1,
'chartData': str(value),
'background': mode['bg'],
'borderColor': mode['border_color'],
'option': {
'title': {'text': title, 'show': True,
'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
'endVal': value,
'fontSize': font_size,
'color': color,
'duration': 2000,
'card': _make_card(mode, title),
},
}
return add_component(page_id, 'JCountTo', title, x, y, w, h, config)
def add_border(page_id, x, y, w, h, border_type=1, color='#00BAFF'):
"""
添加装饰边框组件JDragBorder
Args:
border_type: 边框样式 1~13
color: 边框颜色
"""
config = {
'dataType': 1,
'option': {
'borderType': border_type,
'color': [color],
},
}
return add_component(page_id, 'JDragBorder', f'边框{border_type}', x, y, w, h, config)
def add_decoration(page_id, x, y, w, h, deco_type=1, color='#00BAFF'):
"""
添加装饰条组件JDragDecoration
Args:
deco_type: 装饰样式 1~12
color: 颜色
"""
config = {
'dataType': 1,
'option': {
'decorationType': deco_type,
'color': [color],
},
}
return add_component(page_id, 'JDragDecoration', f'装饰{deco_type}', x, y, w, h, config)
# ============================================================
# 编辑已有页面
# ============================================================
def update_page(page_id, new_components=None):
"""
更新已有页面的组件
Args:
page_id: 页面 ID
new_components: 新的组件列表完全替换
"""
# 查询当前页面信息
page = query_page(page_id)
if new_components is not None:
_page_components[page_id] = new_components
elif page_id not in _page_components:
# 从已有页面加载组件
template = page.get('template', [])
if isinstance(template, str):
try:
template = json.loads(template)
except:
template = []
_page_components[page_id] = template
return save_page(page_id)
def add_to_existing(page_id, component_func, *args, **kwargs):
"""
向已有页面追加组件先加载已有组件再添加新组件最后保存
用法
add_to_existing(page_id, add_number, '新指标', x=500, y=0, w=400, h=200, value=999)
"""
if page_id not in _page_components or not _page_components[page_id]:
# 先加载已有组件
page = query_page(page_id)
template = page.get('template', [])
if isinstance(template, str):
try:
template = json.loads(template)
except:
template = []
_page_components[page_id] = template
# 调用组件添加函数
return component_func(page_id, *args, **kwargs)
# ============================================================
# 菜单 SQL 生成
# ============================================================
ROLE_ID = 'f6817f48af4fb3af11b9e8bf182f618b'
def gen_menu_sql(parent_name, children, icon='ant-design:appstore-outlined', role_id=None):
"""
生成菜单 SQL + 角色授权 SQL
Args:
parent_name: 父菜单名称
children: [(名称, page_id, sort_no), ...]
icon: 父菜单图标
role_id: 角色 ID默认使用 ROLE_ID
Returns:
SQL 字符串
"""
rid = role_id or ROLE_ID
parent_id = _gen_uuid()
lines = []
# 父菜单
lines.append(f"-- 父菜单: {parent_name}")
lines.append(
f"INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, "
f"redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, "
f"is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, "
f"create_by, create_time, update_by, update_time, internal_or_external) "
f"VALUES ('{parent_id}', NULL, '{parent_name}', '/{parent_id}', "
f"'layouts/RouteView', NULL, NULL, 0, NULL, '1', 1.00, 0, '{icon}', "
f"1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', now(), NULL, NULL, 0);"
)
rp_id = _gen_uuid()
lines.append(
f"INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, "
f"operate_date, operate_ip) VALUES ('{rp_id}', '{rid}', '{parent_id}', NULL, "
f"now(), '127.0.0.1');"
)
lines.append('')
# 子菜单
for name, page_id, sort_no in children:
menu_id = _gen_uuid()
lines.append(f"-- 子菜单: {name}")
lines.append(
f"INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, "
f"redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, "
f"is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, "
f"create_by, create_time, update_by, update_time, internal_or_external) "
f"VALUES ('{menu_id}', '{parent_id}', '{name}', "
f"'/drag/page/view/{page_id}', "
f"'super/drag/page/dashboardPreview', 'dashboardPreview', "
f"NULL, 0, NULL, '1', {sort_no}.00, 0, NULL, 0, 1, 0, 0, 0, NULL, '1', "
f"0, 0, 'admin', now(), NULL, NULL, 0);"
)
rp_id2 = _gen_uuid()
lines.append(
f"INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, "
f"operate_date, operate_ip) VALUES ('{rp_id2}', '{rid}', '{menu_id}', NULL, "
f"now(), '127.0.0.1');"
)
lines.append('')
return '\n'.join(lines)
# ============================================================
# 辅助函数
# ============================================================
def _get_category(component):
"""根据组件类型获取分类"""
category_map = {
'JBar': 'Bar', 'JHorizontalBar': 'Bar', 'JBackgroundBar': 'Bar',
'JMultipleBar': 'Bar', 'JNegativeBar': 'Bar', 'JStackBar': 'Bar',
'JDynamicBar': 'Bar', 'JCapsuleChart': 'Bar',
'JLine': 'Line', 'JSmoothLine': 'Line', 'JStepLine': 'Line',
'JMultipleLine': 'Line',
'JMixLineBar': 'MixLineBar', 'DoubleLineBar': 'DoubleLineBar',
'JPie': 'Pie', 'JRing': 'Ring', 'JRose': 'Rose',
'JGauge': 'Gauge', 'JColorGauge': 'Gauge', 'JSemiGauge': 'Gauge',
'JProgress': 'Progress', 'JCustomProgress': 'Progress',
'JLiquid': 'Liquid', 'JRadialBar': 'RadialBar',
'JFunnel': 'Funnel', 'JPyramidFunnel': 'Funnel',
'JRadar': 'Radar', 'JCircleRadar': 'Radar',
'JScatter': 'Scatter', 'JBubble': 'Bubble',
'JWordCloud': 'WordCloud',
'JAreaMap': 'Map', 'JBubbleMap': 'Map', 'JFlyLineMap': 'Map',
'JHeatMap': 'Map', 'JBarMap': 'Map',
'JBar3d': '3D', 'JBarGroup3d': '3D',
'JNumber': 'Number', 'JCountTo': 'CountTo',
'JTable': 'Table', 'JScrollTable': 'ScrollTable',
'JPivotTable': 'PivotTable',
'JScrollRankingBoard': 'Ranking',
'JText': 'Text', 'JImg': 'Image',
'JCarousel': 'Carousel', 'JVideoPlay': 'Video',
'JCustomButton': 'Button', 'JTabs': 'Tabs',
'JDragBorder': 'Border', 'JDragDecoration': 'Decoration',
'JIframe': 'Iframe', 'JCurrentTime': 'Time',
}
return category_map.get(component, 'Common')
def _deep_merge(base, override):
"""深度合并字典override 覆盖 base"""
for k, v in override.items():
if k in base and isinstance(base[k], dict) and isinstance(v, dict):
_deep_merge(base[k], v)
else:
base[k] = v
return base