mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-07 12:50:03 +08:00
✨ 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
d9e65057cf
commit
7472cabd48
@@ -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 (
|
||||
|
||||
@@ -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(" ", " ") 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user