mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-14 21:52:56 +08:00
✨ feat(ui): 添加富文本单元格并迁移UI表格渲染
- 【新功能】 - 添加 `RichTextCell` 模型,支持在表格单元格中显示多个带样式的文本片段。 - `TableCell` 类型别名更新以包含 `RichTextCell`。 - 【迁移】 - 将`ShopManage`、`SignManage` 和 `SchedulerManager` 中所有基于 `ImageTemplate.table_page` 的表格图片生成逻辑迁移至新的 `TableBuilder` 和 `ui.render` 系统。 - 移除旧的 `ImageTemplate` 导入和 `RowStyle` 函数。 - 将 `ThemeManager` 中的资源解析逻辑提取到独立的 `ResourceResolver` 类中,增强模块化和可维护性。 - 优化 `ThemeManager.load_theme` 中 `ChoiceLoader` 的处理逻辑。 - 优化签到卡片数据结构,移除 `last_sign_date_str` 字段,并调整 `reward_info` 在卡片视图下的结构。 - 移除 `_generate_html_card` 中 `favorability_info` 的 `attitude` 和 `relation` 字段。
This commit is contained in:
parent
b505307f2f
commit
7bf568c114
@ -1,9 +1,11 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.models.scheduled_job import ScheduledJob
|
||||
from zhenxun.services.scheduler import scheduler_manager
|
||||
from zhenxun.utils._image_template import ImageTemplate, RowStyle
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
from zhenxun.ui.models import StatusBadgeCell, TextCell
|
||||
from zhenxun.utils.pydantic_compat import model_json_schema
|
||||
|
||||
|
||||
@ -118,19 +120,6 @@ def format_update_success(schedule_info: ScheduledJob) -> str:
|
||||
return _format_operation_result_card("🔄️ 成功更新定时任务配置!", schedule_info)
|
||||
|
||||
|
||||
def _status_row_style(column: str, text: str) -> RowStyle:
|
||||
"""为状态列设置颜色"""
|
||||
style = RowStyle()
|
||||
if column == "状态":
|
||||
if text == "启用":
|
||||
style.font_color = "#67C23A"
|
||||
elif text == "暂停":
|
||||
style.font_color = "#F56C6C"
|
||||
elif text == "运行中":
|
||||
style.font_color = "#409EFF"
|
||||
return style
|
||||
|
||||
|
||||
def _format_params(schedule_status: dict) -> str:
|
||||
"""将任务参数格式化为人类可读的字符串"""
|
||||
if kwargs := schedule_status.get("job_kwargs"):
|
||||
@ -157,36 +146,47 @@ async def format_schedule_list_as_image(
|
||||
]
|
||||
all_statuses = await asyncio.gather(*status_tasks)
|
||||
|
||||
def get_status_text(status_value):
|
||||
if isinstance(status_value, bool):
|
||||
return "启用" if status_value else "暂停"
|
||||
return str(status_value)
|
||||
data_list = []
|
||||
for s in all_statuses:
|
||||
if not s:
|
||||
continue
|
||||
|
||||
data_list = [
|
||||
[
|
||||
s["id"],
|
||||
s["plugin_name"],
|
||||
s.get("bot_id") or "N/A",
|
||||
s["group_id"] or "全局",
|
||||
s["next_run_time"],
|
||||
_format_trigger_info(s),
|
||||
_format_params(s),
|
||||
get_status_text(s["is_enabled"]),
|
||||
]
|
||||
for s in all_statuses
|
||||
if s
|
||||
]
|
||||
status_value = s["is_enabled"]
|
||||
if status_value == "运行中":
|
||||
status_cell = StatusBadgeCell(text="运行中", status_type="info")
|
||||
else:
|
||||
is_enabled = status_value == "启用"
|
||||
status_cell = StatusBadgeCell(
|
||||
text="启用" if is_enabled else "暂停",
|
||||
status_type="ok" if is_enabled else "error",
|
||||
)
|
||||
|
||||
data_list.append(
|
||||
[
|
||||
TextCell(content=str(s["id"])),
|
||||
TextCell(content=s["plugin_name"]),
|
||||
TextCell(content=s.get("bot_id") or "N/A"),
|
||||
TextCell(content=s["group_id"] or "全局"),
|
||||
TextCell(content=s["next_run_time"]),
|
||||
TextCell(content=_format_trigger_info(s)),
|
||||
TextCell(content=_format_params(s)),
|
||||
status_cell,
|
||||
]
|
||||
)
|
||||
|
||||
if not data_list:
|
||||
return "没有找到任何相关的定时任务。"
|
||||
|
||||
return await ImageTemplate.table_page(
|
||||
head_text=title,
|
||||
tip_text=f"第 {current_page}/{total_pages} 页,共 {total_items} 条任务",
|
||||
column_name=["ID", "插件", "Bot", "目标", "下次运行", "规则", "参数", "状态"],
|
||||
data_list=data_list,
|
||||
column_space=20,
|
||||
text_style=_status_row_style,
|
||||
builder = TableBuilder(
|
||||
title, f"第 {current_page}/{total_pages} 页,共 {total_items} 条任务"
|
||||
)
|
||||
builder.set_headers(
|
||||
["ID", "插件", "Bot", "目标", "下次运行", "规则", "参数", "状态"]
|
||||
).add_rows(data_list)
|
||||
return await ui.render(
|
||||
builder.build(),
|
||||
viewport={"width": 1400, "height": 10},
|
||||
device_scale_factor=2,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -153,7 +153,7 @@ async def _(session: Uninfo, arparma: Arparma, nickname: str = UserName()):
|
||||
nickname,
|
||||
PlatformUtils.get_platform(session),
|
||||
):
|
||||
await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True)
|
||||
await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True) # type: ignore
|
||||
return await MessageUtils.build_message("你的道具为空捏...").send(reply_to=True)
|
||||
|
||||
|
||||
|
||||
@ -22,8 +22,8 @@ from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.models.user_gold_log import UserGoldLog
|
||||
from zhenxun.models.user_props_log import UserPropsLog
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.ui.models import ImageCell, TextCell
|
||||
from zhenxun.utils.enum import GoldHandle, PropHandle
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.pydantic_compat import model_dump
|
||||
|
||||
@ -92,9 +92,7 @@ class ShopParam(BaseModel):
|
||||
return model_dump(self, **kwargs)
|
||||
|
||||
|
||||
async def gold_rank(
|
||||
session: Uninfo, group_id: str | None, num: int
|
||||
) -> BuildImage | str:
|
||||
async def gold_rank(session: Uninfo, group_id: str | None, num: int) -> bytes | str:
|
||||
query = UserConsole
|
||||
if group_id:
|
||||
uid_list = await GroupInfoUser.filter(group_id=group_id).values_list(
|
||||
@ -125,16 +123,18 @@ async def gold_rank(
|
||||
data_list = []
|
||||
platform = PlatformUtils.get_platform(session)
|
||||
for i, user in enumerate(user_list):
|
||||
ava_bytes = await PlatformUtils.get_user_avatar(
|
||||
user[0], platform, session.self_id
|
||||
)
|
||||
ava_url = PlatformUtils.get_user_avatar_url(user[0], platform, session.self_id)
|
||||
data_list.append(
|
||||
[
|
||||
f"{i + 1}",
|
||||
(ava_bytes, 30, 30) if platform == "qq" else "",
|
||||
uid2name.get(user[0]),
|
||||
user[1],
|
||||
(PLATFORM_PATH.get(platform), 30, 30),
|
||||
TextCell(content=f"{i + 1}"),
|
||||
ImageCell(src=ava_url or "", shape="circle")
|
||||
if platform == "qq"
|
||||
else TextCell(content=""),
|
||||
TextCell(content=uid2name.get(user[0]) or user[0]),
|
||||
TextCell(content=str(user[1]), bold=True),
|
||||
ImageCell(src=platform_path.resolve().as_uri())
|
||||
if (platform_path := PLATFORM_PATH.get(platform))
|
||||
else TextCell(content=""),
|
||||
]
|
||||
)
|
||||
if group_id:
|
||||
@ -143,7 +143,11 @@ async def gold_rank(
|
||||
else:
|
||||
title = "金币全局排行"
|
||||
tip = f"你的排名在全局第 {index} 位哦!"
|
||||
return await ImageTemplate.table_page(title, tip, column_name, data_list)
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
|
||||
builder = TableBuilder(title, tip)
|
||||
builder.set_headers(column_name).add_rows(data_list)
|
||||
return await ui.render(builder.build())
|
||||
|
||||
|
||||
class ShopManage:
|
||||
@ -493,7 +497,7 @@ class ShopManage:
|
||||
@classmethod
|
||||
async def my_props(
|
||||
cls, user_id: str, name: str, platform: str | None = None
|
||||
) -> BuildImage | None:
|
||||
) -> bytes | None:
|
||||
"""获取道具背包
|
||||
|
||||
参数:
|
||||
@ -544,12 +548,11 @@ class ShopManage:
|
||||
return None
|
||||
|
||||
column_name = ["-", "使用ID", "名称", "数量", "简介"]
|
||||
return await ImageTemplate.table_page(
|
||||
f"{name}的道具仓库",
|
||||
"通过 使用道具[ID/名称] 令道具生效",
|
||||
column_name,
|
||||
table_rows,
|
||||
)
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
|
||||
builder = TableBuilder(f"{name}的道具仓库", "通过 使用道具[ID/名称] 令道具生效")
|
||||
builder.set_headers(column_name).add_rows(table_rows)
|
||||
return await ui.render(builder.build())
|
||||
|
||||
@classmethod
|
||||
async def my_cost(cls, user_id: str, platform: str | None = None) -> int:
|
||||
|
||||
@ -6,6 +6,7 @@ import secrets
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
import pytz
|
||||
|
||||
from zhenxun import ui
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.models.friend_user import FriendUser
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
@ -13,7 +14,7 @@ from zhenxun.models.sign_log import SignLog
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
from zhenxun.ui.models import ImageCell, TextCell
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ._random_event import random_event
|
||||
@ -33,7 +34,7 @@ class SignManage:
|
||||
@classmethod
|
||||
async def rank(
|
||||
cls, session: Uninfo, num: int, group_id: str | None = None
|
||||
) -> BuildImage | str: # sourcery skip: avoid-builtin-shadow
|
||||
) -> bytes | str:
|
||||
"""好感度排行
|
||||
|
||||
参数:
|
||||
@ -42,7 +43,7 @@ class SignManage:
|
||||
group_id: 群组id
|
||||
|
||||
返回:
|
||||
BuildImage: 构造图片
|
||||
bytes: 构造图片
|
||||
"""
|
||||
query = SignUser
|
||||
if group_id:
|
||||
@ -78,17 +79,21 @@ class SignManage:
|
||||
data_list = []
|
||||
platform = PlatformUtils.get_platform(session)
|
||||
for i, user in enumerate(user_list):
|
||||
bytes = await PlatformUtils.get_user_avatar(
|
||||
ava_url = PlatformUtils.get_user_avatar_url(
|
||||
user[0], platform, session.self_id
|
||||
)
|
||||
data_list.append(
|
||||
[
|
||||
f"{i + 1}",
|
||||
(bytes, 30, 30) if user[3] == "qq" else "",
|
||||
uid2name.get(user[0]),
|
||||
user[1],
|
||||
user[2],
|
||||
(PLATFORM_PATH.get(user[3]), 30, 30),
|
||||
TextCell(content=f"{i + 1}"),
|
||||
ImageCell(src=ava_url or "", shape="circle")
|
||||
if user[3] == "qq"
|
||||
else TextCell(content=""),
|
||||
TextCell(content=uid2name.get(user[0]) or user[0]),
|
||||
TextCell(content=str(user[1]), bold=True),
|
||||
TextCell(content=str(user[2])),
|
||||
ImageCell(src=platform_path.resolve().as_uri())
|
||||
if (platform_path := PLATFORM_PATH.get(platform))
|
||||
else TextCell(content=""),
|
||||
]
|
||||
)
|
||||
if group_id:
|
||||
@ -97,7 +102,11 @@ class SignManage:
|
||||
else:
|
||||
title = "好感度全局排行"
|
||||
tip = f"你的排名在全局第 {index} 位哦!"
|
||||
return await ImageTemplate.table_page(title, tip, column_name, data_list)
|
||||
from zhenxun.ui.builders import TableBuilder
|
||||
|
||||
builder = TableBuilder(title, tip)
|
||||
builder.set_headers(column_name).add_rows(data_list)
|
||||
return await ui.render(builder.build())
|
||||
|
||||
@classmethod
|
||||
async def sign(
|
||||
|
||||
@ -227,8 +227,6 @@ async def _generate_html_card(
|
||||
"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,
|
||||
@ -238,7 +236,6 @@ async def _generate_html_card(
|
||||
reward_info = None
|
||||
rank = None
|
||||
total_gold = None
|
||||
last_sign_date_str = None
|
||||
|
||||
if is_card_view:
|
||||
value_list = (
|
||||
@ -249,12 +246,10 @@ 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_sign_date_str = ""
|
||||
|
||||
reward_info = {
|
||||
"impression": f"好感度排名第 {rank} 位",
|
||||
"gold": f"总金币:{total_gold}",
|
||||
"gift": "",
|
||||
"impression_added": 0,
|
||||
"gold_added": 0,
|
||||
"gift_received": "",
|
||||
"is_double": False,
|
||||
}
|
||||
|
||||
@ -285,7 +280,6 @@ async def _generate_html_card(
|
||||
"progress": progress,
|
||||
"rank": rank,
|
||||
"total_gold": total_gold,
|
||||
"last_sign_date_str": last_sign_date_str,
|
||||
}
|
||||
|
||||
image_bytes = await ui.render_template("pages/builtin/sign", data=card_data)
|
||||
|
||||
@ -56,6 +56,138 @@ class Theme(BaseModel):
|
||||
default_assets_dir: Path
|
||||
|
||||
|
||||
class ResourceResolver:
|
||||
"""
|
||||
一个独立的、用于解析组件和主题资源的类。
|
||||
封装了所有复杂的路径查找和回退逻辑。
|
||||
|
||||
资源解析遵循以下回退顺序,以支持强大的主题覆盖和组件化:
|
||||
|
||||
1. **相对路径 (`./`)**: 对于在模板中使用 `asset('./style.css')` 的情况,
|
||||
这是组件内部的资源。
|
||||
a. **皮肤资源**: 首先在当前组件的皮肤目录中查找
|
||||
(`.../skins/{variant_name}/assets/`)。
|
||||
这允许皮肤完全覆盖其组件的默认资源。
|
||||
b. **当前主题组件资源**: 接着在当前激活主题的组件根目录中查找
|
||||
(`.../{theme_name}/.../assets/`)。
|
||||
c. **默认主题组件资源**: 如果仍未找到,最后回退到 `default` 主题中
|
||||
对应的组件目录
|
||||
(`.../default/.../assets/`) 查找。这是核心的回退逻辑。
|
||||
|
||||
2. **全局路径**: 对于使用 `asset('js/script.js')` 的情况,这是主题的全局资源。
|
||||
a. **当前主题全局资源**: 在当前激活主题的根 `assets` 目录中查找
|
||||
(`themes/{theme_name}/assets/`)。
|
||||
b. **默认主题全局资源**: 如果找不到,则回退到 `default` 主题的根 `assets` 目录
|
||||
(`themes/default/assets/`)。
|
||||
"""
|
||||
|
||||
def __init__(self, theme_manager: "ThemeManager"):
|
||||
self.theme_manager = theme_manager
|
||||
|
||||
def _find_component_root(self, start_path: Path) -> Path:
|
||||
"""从给定路径向上查找,找到包含 manifest.json 的组件根目录。"""
|
||||
current_path = start_path.parent
|
||||
themes_root_parts = len(THEMES_PATH.parts)
|
||||
for _ in range(len(current_path.parts) - themes_root_parts):
|
||||
if (current_path / "manifest.json").exists():
|
||||
return current_path
|
||||
if current_path.parent == current_path:
|
||||
break
|
||||
current_path = current_path.parent
|
||||
return start_path.parent
|
||||
|
||||
def _search_paths_for_relative_asset(
|
||||
self, asset_path: str, parent_template_name: str
|
||||
) -> list[tuple[str, Path]]:
|
||||
"""为相对路径的资源生成所有可能的查找路径元组 (描述, 路径)。"""
|
||||
if not self.theme_manager.current_theme:
|
||||
return []
|
||||
|
||||
paths_to_check: list[tuple[str, Path]] = []
|
||||
current_theme_name = self.theme_manager.current_theme.name
|
||||
current_theme_root = self.theme_manager.current_theme.assets_dir.parent
|
||||
default_theme_root = self.theme_manager.current_theme.default_assets_dir.parent
|
||||
|
||||
if not self.theme_manager.jinja_env.loader:
|
||||
return []
|
||||
|
||||
source_info = self.theme_manager.jinja_env.loader.get_source(
|
||||
self.theme_manager.jinja_env, parent_template_name
|
||||
)
|
||||
if not source_info[1]:
|
||||
return []
|
||||
|
||||
parent_template_abs_path = Path(source_info[1])
|
||||
|
||||
component_logical_root = Path(parent_template_name).parent
|
||||
|
||||
if (
|
||||
"/skins/" in parent_template_abs_path.as_posix()
|
||||
or "\\skins\\" in parent_template_abs_path.as_posix()
|
||||
):
|
||||
skin_dir = parent_template_abs_path.parent
|
||||
paths_to_check.append(
|
||||
(
|
||||
f"'{current_theme_name}' 主题皮肤资源",
|
||||
skin_dir / "assets" / asset_path,
|
||||
)
|
||||
)
|
||||
|
||||
paths_to_check.append(
|
||||
(
|
||||
f"'{current_theme_name}' 主题组件资源",
|
||||
current_theme_root / component_logical_root / "assets" / asset_path,
|
||||
)
|
||||
)
|
||||
|
||||
if current_theme_name != "default":
|
||||
paths_to_check.append(
|
||||
(
|
||||
"'default' 主题组件资源 (回退)",
|
||||
default_theme_root / component_logical_root / "assets" / asset_path,
|
||||
)
|
||||
)
|
||||
return paths_to_check
|
||||
|
||||
def resolve_asset_uri(self, asset_path: str, current_template_name: str) -> str:
|
||||
"""解析资源路径,实现完整的回退逻辑,并返回可用的URI。"""
|
||||
if not self.theme_manager.current_theme:
|
||||
return ""
|
||||
|
||||
search_paths: list[tuple[str, Path]] = []
|
||||
if asset_path.startswith("./"):
|
||||
search_paths.extend(
|
||||
self._search_paths_for_relative_asset(
|
||||
asset_path[2:], current_template_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
search_paths.append(
|
||||
(
|
||||
f"'{self.theme_manager.current_theme.name}' 主题全局资源",
|
||||
self.theme_manager.current_theme.assets_dir / asset_path,
|
||||
)
|
||||
)
|
||||
if self.theme_manager.current_theme.name != "default":
|
||||
search_paths.append(
|
||||
(
|
||||
"'default' 主题全局资源 (回退)",
|
||||
self.theme_manager.current_theme.default_assets_dir
|
||||
/ asset_path,
|
||||
)
|
||||
)
|
||||
|
||||
for source_desc, path in search_paths:
|
||||
if path.exists():
|
||||
logger.debug(f"解析资源 '{asset_path}' -> 找到 {source_desc}: '{path}'")
|
||||
return path.absolute().as_uri()
|
||||
|
||||
logger.warning(
|
||||
f"资源文件未找到: '{asset_path}' (在模板 '{current_template_name}' 中引用)"
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
class ThemeManager:
|
||||
def __init__(self, env: Environment):
|
||||
"""
|
||||
@ -83,120 +215,31 @@ class ThemeManager:
|
||||
return []
|
||||
return [d.name for d in THEMES_PATH.iterdir() if d.is_dir()]
|
||||
|
||||
def _find_component_root(self, start_path: Path) -> Path:
|
||||
def _create_asset_loader(self) -> Callable[..., str]:
|
||||
"""
|
||||
从给定的起始路径向上查找,直到找到包含 manifest.json 的目录。
|
||||
这被认为是组件的根目录。如果找不到,则返回起始路径的目录。
|
||||
"""
|
||||
current_path = start_path.parent
|
||||
for _ in range(len(current_path.parts)):
|
||||
if (current_path / "manifest.json").exists():
|
||||
return current_path
|
||||
if current_path.parent == current_path:
|
||||
break
|
||||
current_path = current_path.parent
|
||||
return start_path.parent
|
||||
|
||||
def _create_asset_loader(
|
||||
self, local_base_path: Path | None = None
|
||||
) -> Callable[..., str]:
|
||||
"""
|
||||
创建并返回一个用于解析静态资源的闭包函数 (Jinja2中的 `asset()` 函数)。
|
||||
|
||||
该函数实现了强大的资源解析回退逻辑,查找顺序如下:
|
||||
1. **相对路径 (`./`)**: 优先查找相对于当前模板的 `assets` 目录。
|
||||
- 这支持组件皮肤 (`skins/`) 对其资源的覆盖。
|
||||
2. **当前主题**: 在当前激活主题的 `assets` 目录中查找。
|
||||
3. **默认主题**: 如果当前主题未找到,则回退到 `default` 主题的 `assets` 目录。
|
||||
|
||||
参数:
|
||||
local_base_path: (可选) 当渲染独立模板时,提供模板所在的目录。
|
||||
创建一个闭包函数 (Jinja2中的 `asset()` 函数),使用
|
||||
ResourceResolver 进行路径解析。
|
||||
"""
|
||||
resolver = ResourceResolver(self)
|
||||
|
||||
@pass_context
|
||||
def asset_loader(ctx, asset_path: str) -> str:
|
||||
if asset_path.startswith("./"):
|
||||
parent_template_name = ctx.environment.get_template(ctx.name).name
|
||||
parent_template_abs_path = Path(
|
||||
ctx.environment.loader.get_source(
|
||||
ctx.environment, parent_template_name
|
||||
)[1]
|
||||
)
|
||||
|
||||
if (
|
||||
"/skins/" in parent_template_abs_path.as_posix()
|
||||
or "\\skins\\" in parent_template_abs_path.as_posix()
|
||||
):
|
||||
skin_dir = parent_template_abs_path.parent
|
||||
skin_asset_path = skin_dir / "assets" / asset_path[2:]
|
||||
if skin_asset_path.exists():
|
||||
logger.debug(f"找到皮肤本地资源: '{skin_asset_path}'")
|
||||
return skin_asset_path.absolute().as_uri()
|
||||
logger.debug(
|
||||
f"皮肤本地资源未找到: '{skin_asset_path}',将回退到组件公共资源"
|
||||
)
|
||||
|
||||
component_root = self._find_component_root(parent_template_abs_path)
|
||||
|
||||
local_asset = component_root / "assets" / asset_path[2:]
|
||||
if local_asset.exists():
|
||||
logger.debug(f"找到组件公共资源: '{local_asset}'")
|
||||
return local_asset.absolute().as_uri()
|
||||
|
||||
logger.warning(
|
||||
f"组件相对资源未找到: '{asset_path}'。已在皮肤和组件根目录中查找。"
|
||||
)
|
||||
return ""
|
||||
|
||||
assert self.current_theme is not None
|
||||
current_theme_asset = self.current_theme.assets_dir / asset_path
|
||||
if current_theme_asset.exists():
|
||||
return current_theme_asset.absolute().as_uri()
|
||||
|
||||
default_theme_asset = self.current_theme.default_assets_dir / asset_path
|
||||
if default_theme_asset.exists():
|
||||
return default_theme_asset.absolute().as_uri()
|
||||
|
||||
logger.warning(
|
||||
f"资源文件在主题 '{self.current_theme.name}' 和 'default' 中均未找到: "
|
||||
f"{asset_path}"
|
||||
)
|
||||
return ""
|
||||
if not ctx.name:
|
||||
logger.warning("Jinja2 上下文缺少模板名称,无法进行资源解析。")
|
||||
return resolver.resolve_asset_uri(asset_path, "unknown_template")
|
||||
parent_template_name = ctx.name
|
||||
return resolver.resolve_asset_uri(asset_path, parent_template_name)
|
||||
|
||||
return asset_loader
|
||||
|
||||
def _create_standalone_asset_loader(
|
||||
self, local_base_path: Path
|
||||
) -> Callable[[str], str]:
|
||||
"""
|
||||
[新增] 为独立模板创建一个专用的、更简单的 asset loader。
|
||||
"""
|
||||
"""为独立模板创建一个专用的 asset loader。"""
|
||||
resolver = ResourceResolver(self)
|
||||
|
||||
def asset_loader(asset_path: str) -> str:
|
||||
if asset_path.startswith("./"):
|
||||
local_file = local_base_path / "assets" / asset_path[2:]
|
||||
if local_file.exists():
|
||||
return local_file.absolute().as_uri()
|
||||
logger.warning(
|
||||
f"独立模板本地资源 '{asset_path}' 在 "
|
||||
f"'{local_base_path / 'assets'}' 中未找到。"
|
||||
)
|
||||
return ""
|
||||
|
||||
assert self.current_theme is not None
|
||||
current_theme_asset = self.current_theme.assets_dir / asset_path
|
||||
if current_theme_asset.exists():
|
||||
return current_theme_asset.absolute().as_uri()
|
||||
|
||||
default_theme_asset = self.current_theme.default_assets_dir / asset_path
|
||||
if default_theme_asset.exists():
|
||||
return default_theme_asset.absolute().as_uri()
|
||||
|
||||
logger.warning(
|
||||
f"资源文件在主题 '{self.current_theme.name}' 和 'default' 中均未找到: "
|
||||
f"{asset_path}"
|
||||
)
|
||||
return ""
|
||||
return resolver.resolve_asset_uri(asset_path, str(local_base_path))
|
||||
|
||||
return asset_loader
|
||||
|
||||
@ -278,15 +321,7 @@ class ThemeManager:
|
||||
new_theme_loader = FileSystemLoader(
|
||||
[str(theme_dir), str(THEMES_PATH / "default")]
|
||||
)
|
||||
self.jinja_env.loader = ChoiceLoader([prefix_loader, new_theme_loader])
|
||||
else:
|
||||
self.jinja_env.loader = FileSystemLoader(
|
||||
[str(theme_dir), str(THEMES_PATH / "default")]
|
||||
)
|
||||
else:
|
||||
self.jinja_env.loader = FileSystemLoader(
|
||||
[str(theme_dir), str(THEMES_PATH / "default")]
|
||||
)
|
||||
self.jinja_env.loader.loaders = [prefix_loader, new_theme_loader]
|
||||
|
||||
palette_path = theme_dir / "palette.json"
|
||||
palette = (
|
||||
|
||||
@ -26,12 +26,14 @@ from .core import (
|
||||
QuoteElement,
|
||||
RawHtmlElement,
|
||||
RenderableComponent,
|
||||
RichTextCell,
|
||||
StatusBadgeCell,
|
||||
TableCell,
|
||||
TableData,
|
||||
TableElement,
|
||||
TextCell,
|
||||
TextElement,
|
||||
TextSpan,
|
||||
)
|
||||
from .presets import (
|
||||
HelpCategory,
|
||||
@ -71,11 +73,13 @@ __all__ = [
|
||||
"RawHtmlElement",
|
||||
"Rectangle",
|
||||
"RenderableComponent",
|
||||
"RichTextCell",
|
||||
"StatusBadgeCell",
|
||||
"TableCell",
|
||||
"TableData",
|
||||
"TableElement",
|
||||
"TextCell",
|
||||
"TextElement",
|
||||
"TextSpan",
|
||||
"UserInfoBlock",
|
||||
]
|
||||
|
||||
@ -22,7 +22,15 @@ from .markdown import (
|
||||
TextElement,
|
||||
)
|
||||
from .notebook import NotebookData, NotebookElement
|
||||
from .table import BaseCell, ImageCell, StatusBadgeCell, TableCell, TableData, TextCell
|
||||
from .table import (
|
||||
BaseCell,
|
||||
ImageCell,
|
||||
RichTextCell,
|
||||
StatusBadgeCell,
|
||||
TableCell,
|
||||
TableData,
|
||||
TextCell,
|
||||
)
|
||||
from .template import TemplateComponent
|
||||
from .text import TextData, TextSpan
|
||||
|
||||
@ -48,6 +56,7 @@ __all__ = [
|
||||
"QuoteElement",
|
||||
"RawHtmlElement",
|
||||
"RenderableComponent",
|
||||
"RichTextCell",
|
||||
"StatusBadgeCell",
|
||||
"TableCell",
|
||||
"TableData",
|
||||
|
||||
@ -4,11 +4,13 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from ...models.components.progress_bar import ProgressBar
|
||||
from .base import RenderableComponent
|
||||
from .text import TextSpan
|
||||
|
||||
__all__ = [
|
||||
"BaseCell",
|
||||
"ImageCell",
|
||||
"ProgressBarCell",
|
||||
"RichTextCell",
|
||||
"StatusBadgeCell",
|
||||
"TableCell",
|
||||
"TableData",
|
||||
@ -56,8 +58,25 @@ class ProgressBarCell(BaseCell, ProgressBar):
|
||||
type: Literal["progress_bar"] = "progress_bar" # type: ignore
|
||||
|
||||
|
||||
class RichTextCell(BaseCell):
|
||||
"""富文本单元格,支持多个带样式的文本片段"""
|
||||
|
||||
type: Literal["rich_text"] = "rich_text" # type: ignore
|
||||
spans: list[TextSpan] = Field(default_factory=list, description="文本片段列表")
|
||||
direction: Literal["column", "row"] = Field("column", description="片段排列方向")
|
||||
gap: str = Field("4px", description="片段之间的间距")
|
||||
|
||||
|
||||
TableCell = (
|
||||
TextCell | ImageCell | StatusBadgeCell | ProgressBarCell | str | int | float | None
|
||||
TextCell
|
||||
| ImageCell
|
||||
| StatusBadgeCell
|
||||
| ProgressBarCell
|
||||
| RichTextCell
|
||||
| str
|
||||
| int
|
||||
| float
|
||||
| None
|
||||
)
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user