✨ feat!(ui): 重构图表组件架构,实现数据与样式分离 (#2035)

* ✨ feat!(ui): 重构图表组件架构,实现数据与样式分离

🏗️ **架构重构**
- 移除charts.py中所有硬编码样式参数(grid、tooltip、legend等)
- 将样式配置迁移至主题层style.json文件
- 统一图表模板消费样式文件的能力

📊 **图表组件优化**
- bar_chart: 移除grid和坐标轴show参数
- pie_chart: 移除tooltip、legend样式和series视觉参数
- line_chart: 移除tooltip、grid和坐标轴配置
- radar_chart: 移除tooltip硬编码

🎨 **主题系统增强**
- 新增pie_chart、line_chart、radar_chart的style.json配置
- 更新bar_chart/style.json,添加grid、xAxis、yAxis样式
- 所有图表模板支持deepMerge样式合并逻辑

🔧 **Breaking Changes**
- 图表工厂函数不再接受样式参数
- 主题开发者现可通过style.json完全定制图表外观
- 提升组件可维护性和主题灵活性

* 📦️ build(pyinstaller): 引入 resources.spec 并更新 .gitignore 规则

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Rumio
2025-08-28 09:20:15 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent d9e65057cf
commit 7472cabd48
61 changed files with 2272 additions and 648 deletions
+4 -1
View File
@@ -109,8 +109,11 @@ async def _(
)
if name.available:
help_style = Config.get_config("help", "HELP_STYLE")
variant = help_style if help_style != "default" else None
traditional_help_result = await get_plugin_help(
session.user.id, name.result, _is_superuser
session.user.id, name.result, _is_superuser, variant=variant
)
is_plugin_found = not (
+22 -18
View File
@@ -17,7 +17,6 @@ from zhenxun.services import (
)
from zhenxun.services.log import logger
from zhenxun.ui.builders import (
InfoCardBuilder,
NotebookBuilder,
PluginMenuBuilder,
)
@@ -25,7 +24,6 @@ from zhenxun.ui.models import PluginMenuCategory
from zhenxun.utils.common_utils import format_usage_for_markdown
from zhenxun.utils.enum import BlockType, PluginType
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.pydantic_compat import model_dump
from ._utils import classify_plugin
@@ -164,13 +162,16 @@ def split_text(text: str):
return [s.replace(" ", "&nbsp;") for s in split_text]
async def get_plugin_help(user_id: str, name: str, is_superuser: bool) -> str | bytes:
async def get_plugin_help(
user_id: str, name: str, is_superuser: bool, variant: str | None = None
) -> str | bytes:
"""获取功能的帮助信息
参数:
user_id: 用户id
name: 插件名称或id
is_superuser: 是否为超级用户
variant: 使用的皮肤/变体名称
"""
type_list = await get_user_allow_help(user_id)
if name.isdigit():
@@ -192,29 +193,32 @@ async def get_plugin_help(user_id: str, name: str, is_superuser: bool) -> str |
return "该功能没有超级用户帮助信息"
usage = extra_data.superuser_help
builder = InfoCardBuilder(title=_plugin.metadata.name)
builder.add_metadata_items(
[
("作者", extra_data.author or "未知"),
("版本", extra_data.version or "未知"),
("调用次数", call_count),
]
)
metadata_items = [
{"label": "作者", "value": extra_data.author or "未知"},
{"label": "版本", "value": extra_data.version or "未知"},
{"label": "调用次数", "value": call_count},
]
processed_description = format_usage_for_markdown(
_plugin.metadata.description.strip()
)
processed_usage = format_usage_for_markdown(usage.strip())
builder.add_section("简介", [processed_description])
builder.add_section("使用方法", [processed_usage])
sections = [
{"title": "简介", "content": [processed_description]},
{"title": "使用方法", "content": [processed_usage]},
]
style_name = Config.get_config("help", "HELP_STYLE", "default")
render_dict = model_dump(builder._data)
render_dict["style_name"] = style_name
page_data = {
"title": _plugin.metadata.name,
"metadata": metadata_items,
"sections": sections,
}
return await ui.render_template("pages/builtin/help", data=render_dict)
component = ui.template("pages/builtin/help", data=page_data)
if variant:
component.variant = variant
return await ui.render(component, use_cache=True, device_scale_factor=2)
return "糟糕! 该功能没有帮助喔..."
return "没有查找到这个功能噢..."
@@ -4,7 +4,7 @@ from zhenxun.services import renderer_service
from zhenxun.services.llm.core import KeyStatus
from zhenxun.services.llm.types import ModelModality
from zhenxun.ui.builders import MarkdownBuilder, TableBuilder
from zhenxun.ui.models.core.table import StatusBadgeCell, TextCell
from zhenxun.ui.models import StatusBadgeCell, TextCell
def _format_seconds(seconds: int) -> str:
@@ -39,20 +39,19 @@ class Presenters:
return await renderer_service.render(builder.build())
column_name = ["提供商", "模型名称", "API类型", "状态"]
data_list = []
rows_data = []
for model in models:
is_available = model.get("is_available", True)
status_cell = StatusBadgeCell(
text="可用" if is_available else "不可用",
status_type="ok" if is_available else "error",
)
embed_tag = " (Embed)" if model.get("is_embedding_model", False) else ""
data_list.append(
rows_data.append(
[
TextCell(content=model.get("provider_name", "N/A")),
TextCell(content=f"{model.get('model_name', 'N/A')}{embed_tag}"),
TextCell(content=model.get("api_type", "N/A")),
status_cell,
StatusBadgeCell(
text="可用" if is_available else "不可用",
status_type="ok" if is_available else "error",
),
]
)
@@ -60,7 +59,8 @@ class Presenters:
title=title, tip="使用 `llm info <Provider/ModelName>` 查看详情"
)
builder.set_headers(column_name)
builder.add_rows(data_list)
builder.set_column_alignments(["left", "left", "left", "center"])
builder.add_rows(rows_data)
return await renderer_service.render(builder.build(), use_cache=True)
@staticmethod
+28 -21
View File
@@ -7,11 +7,9 @@ import aiofiles
import nonebot
from nonebot.drivers import Driver
from nonebot_plugin_uninfo import Uninfo
import pytz
from zhenxun import ui
from zhenxun.configs.config import BotConfig, Config
from zhenxun.models.sign_log import SignLog
from zhenxun.models.sign_user import SignUser
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils
@@ -176,18 +174,17 @@ async def _generate_html_card(
impression = float(user.impression)
user_console = await user.user_console
uid_str = (
f"{user_console.uid:08}"
if user_console and user_console.uid is not None
else "XXXXXXXX"
)
uid_formatted = f"{uid_str[:4]} {uid_str[4:]}"
if user_console and user_console.uid is not None:
uid = f"{user_console.uid}".rjust(12, "0")
uid_formatted = f"{uid[:4]} {uid[4:8]} {uid[8:]}"
else:
uid_formatted = "XXXX XXXX XXXX"
level, next_impression, previous_impression = get_level_and_next_impression(
impression
)
attitude = level2attitude.get(str(level), "未知")
attitude = f"对你的态度: {level2attitude.get(str(level), '未知')}"
interpolation_val = max(0, next_impression - impression)
interpolation = f"{interpolation_val:.2f}"
@@ -200,16 +197,21 @@ async def _generate_html_card(
hour = now.hour
if 6 < hour < 10:
bot_message = random.choice(MORNING_MESSAGE)
message = random.choice(MORNING_MESSAGE)
elif 0 <= hour < 6:
bot_message = random.choice(LG_MESSAGE)
message = random.choice(LG_MESSAGE)
else:
bot_message = f"{BotConfig.self_nickname}希望你开心!"
message = f"{BotConfig.self_nickname}希望你开心!"
bot_message = f"{BotConfig.self_nickname}说: {message}"
temperature = random.randint(1, 40)
weather_icon_name = f"{random.randint(0, 11)}.png"
tag_icon_name = f"{random.randint(0, 5)}.png"
font_size = 45
if len(nickname) > 6:
font_size = 27
user_info = {
"nickname": nickname,
"uid_str": uid_formatted,
@@ -218,11 +220,17 @@ async def _generate_html_card(
)
or "",
"sign_count": user.sign_count,
"font_size": font_size,
}
favorability_info = {
"current": impression,
"level": level,
"level_text": f"{level} [{lik2relation.get(str(level), '未知')}]",
"attitude": f"对你的态度: {level2attitude.get(str(level), '未知')}",
"relation": lik2relation.get(str(level), "未知"),
"heart2": [1 for _ in range(level)],
"heart1": [1 for _ in range(len(lik2level) - level - 1)],
"next_level_at": next_impression,
"previous_level_at": previous_impression,
}
@@ -241,15 +249,14 @@ async def _generate_html_card(
rank = value_list.index(user.user_id) + 1 if user.user_id in value_list else 0
total_gold = user_console.gold if user_console else 0
last_log = (
await SignLog.filter(user_id=user.user_id).order_by("-create_time").first()
)
last_date = "从未"
if last_log:
last_date = str(
last_log.create_time.astimezone(pytz.timezone("Asia/Shanghai")).date()
)
last_sign_date_str = f"上次签到:{last_date}"
last_sign_date_str = ""
reward_info = {
"impression": f"好感度排名第 {rank} 位",
"gold": f"总金币:{total_gold}",
"gift": "",
"is_double": False,
}
else:
reward_info = {
@@ -1,8 +1,17 @@
from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_alconna import (
Alconna,
AlconnaMatch,
Args,
Arparma,
Match,
Subcommand,
on_alconna,
)
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services import renderer_service
from zhenxun.services.log import logger
@@ -14,7 +23,9 @@ __plugin_meta__ = PluginMetadata(
description="管理UI、主题和渲染服务的相关配置",
usage="""
指令:
重载UI主题
ui reload / 重载主题: 重新加载当前主题的配置和资源。
ui theme / 主题列表: 显示所有可用的主题,并高亮显示当前主题。
ui theme [主题名称] / 切换主题 [主题名称]: 将UI主题切换为指定主题。
""".strip(),
extra=PluginExtraData(
author="HibiKier",
@@ -37,22 +48,39 @@ __plugin_meta__ = PluginMetadata(
default_value=True,
type=bool,
),
RegisterConfig(
module="UI",
key="DEBUG_MODE",
value=False,
help="是否在日志中输出渲染组件的完整HTML源码,用于调试",
default_value=False,
type=bool,
),
],
).to_dict(),
)
_matcher = on_alconna(
Alconna("重载主题"),
ui_matcher = on_alconna(
Alconna(
"ui",
Subcommand("reload", help_text="重载当前主题"),
Subcommand("theme", Args["theme_name?", str], help_text="查看或切换主题"),
),
aliases={"主题管理"},
rule=to_me(),
permission=SUPERUSER,
priority=1,
block=True,
)
ui_matcher.shortcut("重载主题", command="ui reload")
ui_matcher.shortcut("主题列表", command="ui theme")
ui_matcher.shortcut("切换主题", command="ui theme", arguments=["{%0}"])
@_matcher.handle()
async def _(arparma: Arparma):
@ui_matcher.assign("reload")
async def handle_reload(arparma: Arparma):
theme_name = await renderer_service.reload_theme()
logger.info(
f"UI主题已重载为: {theme_name}", "UI管理器", session=arparma.header_result
@@ -60,3 +88,55 @@ async def _(arparma: Arparma):
await MessageUtils.build_message(f"UI主题已成功重载为 '{theme_name}'!").send(
reply_to=True
)
@ui_matcher.assign("theme")
async def handle_theme(
arparma: Arparma, theme_name_match: Match[str] = AlconnaMatch("theme_name")
):
if theme_name_match.available:
new_theme_name = theme_name_match.result
try:
await renderer_service.switch_theme(new_theme_name)
logger.info(
f"UI主题已切换为: {new_theme_name}",
"UI管理器",
session=arparma.header_result,
)
await MessageUtils.build_message(
f"🎨 主题已成功切换为 '{new_theme_name}'!"
).send(reply_to=True)
except FileNotFoundError as e:
logger.warning(
f"尝试切换到不存在的主题: {new_theme_name}",
"UI管理器",
session=arparma.header_result,
)
await MessageUtils.build_message(str(e)).send(reply_to=True)
except Exception as e:
logger.error(
f"切换主题时发生错误: {e}",
"UI管理器",
session=arparma.header_result,
e=e,
)
await MessageUtils.build_message(f"切换主题失败: {e}").send(reply_to=True)
else:
try:
available_themes = renderer_service.list_available_themes()
current_theme = Config.get_config("UI", "THEME", "default")
theme_list_str = "\n".join(
f" - {theme}{' <- 当前' if theme == current_theme else ''}"
for theme in sorted(available_themes)
)
response = f"🎨 可用主题列表:\n{theme_list_str}"
await MessageUtils.build_message(response).send(reply_to=True)
except Exception as e:
logger.error(
f"获取主题列表时发生错误: {e}",
"UI管理器",
session=arparma.header_result,
e=e,
)
await MessageUtils.build_message("获取主题列表失败。").send(reply_to=True)