mirror of
https://github.com/PaiGramTeam/PamGram.git
synced 2024-11-21 13:48:19 +00:00
✨ Update /play_card
placeholder image
Update current player showcase after updating character list. Use asynchronous locks to make file read/write thread-safe. Update `EnkaNetworkCache`. --------- Co-authored-by: 洛水居室 <luoshuijs@outlook.com>
This commit is contained in:
parent
b60094eef0
commit
72154924be
@ -63,7 +63,7 @@ class PlayerInfoService(BaseService):
|
||||
self.cache = redis.client
|
||||
self._players_info_repository = players_info_repository
|
||||
self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent)
|
||||
self.enka_client.set_cache(RedisCache(redis.client, key="players_info:enka_network", ttl=60))
|
||||
self.enka_client.set_cache(RedisCache(redis.client, key="players_info:enka_network", ex=60))
|
||||
self.qname = "players_info"
|
||||
|
||||
async def get_form_cache(self, player: Player):
|
||||
|
@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Union
|
||||
|
||||
@ -5,13 +6,19 @@ import aiofiles
|
||||
|
||||
from utils.const import PROJECT_ROOT
|
||||
|
||||
import ujson as jsonlib
|
||||
try:
|
||||
import ujson as jsonlib
|
||||
except ImportError:
|
||||
import json as jsonlib
|
||||
|
||||
|
||||
PLAYER_CARDS_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "player_cards")
|
||||
PLAYER_CARDS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class PlayerCardsFile:
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
def __init__(self, player_cards_path: Path = PLAYER_CARDS_PATH):
|
||||
self.player_cards_path = player_cards_path
|
||||
|
||||
@ -53,14 +60,15 @@ class PlayerCardsFile:
|
||||
uid: Union[str, int],
|
||||
data: Dict,
|
||||
) -> Dict:
|
||||
old_data = await self.load_history_info(uid)
|
||||
if old_data is None:
|
||||
async with self._lock:
|
||||
old_data = await self.load_history_info(uid)
|
||||
if old_data is None:
|
||||
await self.save_json(self.get_file_path(uid), data)
|
||||
return data
|
||||
data["avatarInfoList"] = data.get("avatarInfoList", [])
|
||||
characters = [i.get("avatarId", 0) for i in data["avatarInfoList"]]
|
||||
for i in old_data.get("avatarInfoList", []):
|
||||
if i.get("avatarId", 0) not in characters:
|
||||
data["avatarInfoList"].append(i)
|
||||
await self.save_json(self.get_file_path(uid), data)
|
||||
return data
|
||||
data["avatarInfoList"] = data.get("avatarInfoList", [])
|
||||
characters = [i.get("avatarId", 0) for i in data["avatarInfoList"]]
|
||||
for i in old_data.get("avatarInfoList", []):
|
||||
if i.get("avatarId", 0) not in characters:
|
||||
data["avatarInfoList"].append(i)
|
||||
await self.save_json(self.get_file_path(uid), data)
|
||||
return data
|
||||
|
@ -67,7 +67,7 @@ class AvatarListPlugin(Plugin):
|
||||
self.assets_service = assets_service
|
||||
self.template_service = template_service
|
||||
self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent)
|
||||
self.enka_client.set_cache(RedisCache(redis.client, key="plugin:avatar_list:enka_network", ttl=60 * 60 * 3))
|
||||
self.enka_client.set_cache(RedisCache(redis.client, key="plugin:avatar_list:enka_network", ex=60 * 60 * 3))
|
||||
self.enka_assets = EnkaAssets(lang="chs")
|
||||
self.helper = helper
|
||||
self.character_details = character_details
|
||||
|
@ -1,14 +1,12 @@
|
||||
import math
|
||||
from typing import Any, List, Tuple, Union, Optional
|
||||
from typing import Any, List, Tuple, Union, Optional, TYPE_CHECKING
|
||||
|
||||
from enkanetwork import (
|
||||
CharacterInfo,
|
||||
DigitType,
|
||||
EnkaNetworkAPI,
|
||||
EnkaNetworkResponse,
|
||||
EnkaServerError,
|
||||
Equipments,
|
||||
EquipmentsStats,
|
||||
EquipmentsType,
|
||||
HTTPException,
|
||||
Stats,
|
||||
@ -20,13 +18,13 @@ from enkanetwork import (
|
||||
EnkaPlayerNotFound,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.constants import ChatAction
|
||||
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters
|
||||
from telegram.ext import CommandHandler, MessageHandler, filters
|
||||
from telegram.helpers import create_deep_linked_url
|
||||
|
||||
from core.config import config
|
||||
from core.dependence.assets import DEFAULT_EnkaAssets
|
||||
from core.dependence.assets import DEFAULT_EnkaAssets, AssetsService
|
||||
from core.dependence.redisdb import RedisDB
|
||||
from core.handler.callbackqueryhandler import CallbackQueryHandler
|
||||
from core.plugin import Plugin, handler
|
||||
@ -40,6 +38,11 @@ from utils.helpers import download_resource
|
||||
from utils.log import logger
|
||||
from utils.patch.aiohttp import AioHttpTimeoutException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from enkanetwork import CharacterInfo, EquipmentsStats
|
||||
from telegram.ext import ContextTypes
|
||||
from telegram import Update
|
||||
|
||||
try:
|
||||
import ujson as jsonlib
|
||||
except ImportError:
|
||||
@ -47,15 +50,22 @@ except ImportError:
|
||||
|
||||
|
||||
class PlayerCards(Plugin):
|
||||
def __init__(self, player_service: PlayersService, template_service: TemplateService, redis: RedisDB):
|
||||
def __init__(
|
||||
self,
|
||||
player_service: PlayersService,
|
||||
template_service: TemplateService,
|
||||
assets_service: AssetsService,
|
||||
redis: RedisDB,
|
||||
):
|
||||
self.player_service = player_service
|
||||
self.client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent, cache=False)
|
||||
self.cache = RedisCache(redis.client, key="plugin:player_cards:enka_network")
|
||||
self.cache = RedisCache(redis.client, key="plugin:player_cards:enka_network", ex=60)
|
||||
self.player_cards_file = PlayerCardsFile()
|
||||
self.assets_service = assets_service
|
||||
self.template_service = template_service
|
||||
self.temp_photo: Optional[str] = None
|
||||
self.kitsune: Optional[str] = None
|
||||
|
||||
async def _fetch_user(self, uid) -> Union[EnkaNetworkResponse, str]:
|
||||
async def _update_enka_data(self, uid) -> Union[EnkaNetworkResponse, str]:
|
||||
try:
|
||||
data = await self.cache.get(uid)
|
||||
if data is not None:
|
||||
@ -82,25 +92,35 @@ class PlayerCards(Plugin):
|
||||
error = "未找到玩家,请检查您的UID/用户名"
|
||||
except HTTPException:
|
||||
error = "Enka.Network HTTP 服务请求错误,请稍后重试"
|
||||
old_data = await self.player_cards_file.load_history_info(uid)
|
||||
if old_data is not None:
|
||||
logger.warning("UID %s | 角色卡片使用历史数据 | %s", uid, error)
|
||||
return EnkaNetworkResponse.parse_obj(old_data)
|
||||
return error
|
||||
|
||||
async def _load_history(self, uid) -> Optional[EnkaNetworkResponse]:
|
||||
data = await self.player_cards_file.load_history_info(uid)
|
||||
if data is None:
|
||||
return None
|
||||
return EnkaNetworkResponse.parse_obj(data)
|
||||
|
||||
@handler(CommandHandler, command="player_card", block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^角色卡片查询(.*)"), block=False)
|
||||
async def player_cards(self, update: Update, context: CallbackContext) -> None:
|
||||
async def player_cards(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
|
||||
user = update.effective_user
|
||||
message = update.effective_message
|
||||
args = self.get_args(context)
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
player_info = await self.player_service.get_player(user.id)
|
||||
if player_info is None:
|
||||
buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]]
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
"点我绑定账号",
|
||||
url=create_deep_linked_url(context.bot.username, "set_uid"),
|
||||
)
|
||||
]
|
||||
]
|
||||
if filters.ChatType.GROUPS.filter(message):
|
||||
reply_message = await message.reply_text(
|
||||
"未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons)
|
||||
"未查询到您所绑定的账号信息,请先私聊派蒙绑定账号",
|
||||
reply_markup=InlineKeyboardMarkup(buttons),
|
||||
)
|
||||
self.add_delete_message_job(reply_message, delay=30)
|
||||
|
||||
@ -108,12 +128,27 @@ class PlayerCards(Plugin):
|
||||
else:
|
||||
await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons))
|
||||
return
|
||||
data = await self._fetch_user(player_info.player_id)
|
||||
if isinstance(data, str):
|
||||
await message.reply_text(data)
|
||||
return
|
||||
if data.characters is None:
|
||||
await message.reply_text("请在游戏中的角色展柜中添加角色再开启显示角色详情再使用此功能,如果已经添加了角色,请等待角色数据更新后重试")
|
||||
data = await self._load_history(player_info.player_id)
|
||||
if data is None:
|
||||
if isinstance(self.kitsune, str):
|
||||
photo = self.kitsune
|
||||
else:
|
||||
photo = open("resources/img/kitsune.png", "rb")
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
"更新面板",
|
||||
callback_data=f"update_player_card|{user.id}|{player_info.player_id}",
|
||||
)
|
||||
]
|
||||
]
|
||||
reply_message = await message.reply_photo(
|
||||
photo=photo,
|
||||
caption="角色列表未找到,请尝试点击下方按钮从 EnkaNetwork 更新角色列表",
|
||||
reply_markup=InlineKeyboardMarkup(buttons),
|
||||
)
|
||||
if reply_message.photo:
|
||||
self.kitsune = reply_message.photo[-1].file_id
|
||||
return
|
||||
if len(args) == 1:
|
||||
character_name = roleToName(args[0])
|
||||
@ -126,16 +161,20 @@ class PlayerCards(Plugin):
|
||||
)
|
||||
else:
|
||||
logger.info("用户 %s[%s] 角色卡片查询命令请求", user.full_name, user.id)
|
||||
buttons = self.gen_button(data, user.id, player_info.player_id)
|
||||
if isinstance(self.temp_photo, str):
|
||||
photo = self.temp_photo
|
||||
ttl = await self.cache.ttl(player_info.player_id)
|
||||
|
||||
buttons = self.gen_button(data, user.id, player_info.player_id, update_button=ttl < 0)
|
||||
if isinstance(self.kitsune, str):
|
||||
photo = self.kitsune
|
||||
else:
|
||||
photo = open("resources/img/kitsune.png", "rb")
|
||||
reply_message = await message.reply_photo(
|
||||
photo=photo, caption="请选择你要查询的角色,部分角色数据存在缓存,更新可能不及时", reply_markup=InlineKeyboardMarkup(buttons)
|
||||
photo=photo,
|
||||
caption="请选择你要查询的角色",
|
||||
reply_markup=InlineKeyboardMarkup(buttons),
|
||||
)
|
||||
if reply_message.photo:
|
||||
self.temp_photo = reply_message.photo[-1].file_id
|
||||
self.kitsune = reply_message.photo[-1].file_id
|
||||
return
|
||||
for characters in data.characters:
|
||||
if characters.name == character_name:
|
||||
@ -147,20 +186,71 @@ class PlayerCards(Plugin):
|
||||
render_result = await RenderTemplate(
|
||||
player_info.player_id, characters, self.template_service
|
||||
).render() # pylint: disable=W0631
|
||||
await render_result.reply_photo(message, filename=f"player_card_{player_info.player_id}_{character_name}.png")
|
||||
await render_result.reply_photo(
|
||||
message,
|
||||
filename=f"player_card_{player_info.player_id}_{character_name}.png",
|
||||
)
|
||||
|
||||
@handler(CallbackQueryHandler, pattern=r"^update_player_card\|", block=False)
|
||||
async def update_player_card(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
|
||||
user = update.effective_user
|
||||
message = update.effective_message
|
||||
callback_query = update.callback_query
|
||||
|
||||
async def get_player_card_callback(callback_query_data: str) -> Tuple[int, int]:
|
||||
_data = callback_query_data.split("|")
|
||||
_user_id = int(_data[1])
|
||||
_uid = int(_data[2])
|
||||
logger.debug("callback_query_data函数返回 user_id[%s] uid[%s]", _user_id, _uid)
|
||||
return _user_id, _uid
|
||||
|
||||
user_id, uid = await get_player_card_callback(callback_query.data)
|
||||
if user.id != user_id:
|
||||
await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True)
|
||||
return
|
||||
|
||||
ttl = await self.cache.ttl(uid)
|
||||
|
||||
if ttl > 0:
|
||||
await callback_query.answer(text=f"请等待 {ttl} 秒后再更新", show_alert=True)
|
||||
return
|
||||
|
||||
await message.reply_chat_action(ChatAction.TYPING)
|
||||
await callback_query.answer(text="正在从 EnkaNetwork 获取角色列表 请不要重复点击按钮")
|
||||
data = await self._update_enka_data(uid)
|
||||
if isinstance(data, str):
|
||||
await callback_query.answer(text=data, show_alert=True)
|
||||
return
|
||||
buttons = self.gen_button(data, user.id, uid, update_button=False)
|
||||
render_data = await self.parse_holder_data(data)
|
||||
holder = await self.template_service.render(
|
||||
"genshin/player_card/holder.html",
|
||||
render_data,
|
||||
viewport={"width": 750, "height": 580},
|
||||
ttl=60 * 10,
|
||||
caption="更新角色列表成功,请选择你要查询的角色",
|
||||
)
|
||||
await holder.edit_media(message, reply_markup=InlineKeyboardMarkup(buttons))
|
||||
|
||||
@handler(CallbackQueryHandler, pattern=r"^get_player_card\|", block=False)
|
||||
async def get_player_cards(self, update: Update, _: CallbackContext) -> None:
|
||||
async def get_player_cards(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
|
||||
callback_query = update.callback_query
|
||||
user = callback_query.from_user
|
||||
message = callback_query.message
|
||||
|
||||
async def get_player_card_callback(callback_query_data: str) -> Tuple[str, int, int]:
|
||||
async def get_player_card_callback(
|
||||
callback_query_data: str,
|
||||
) -> Tuple[str, int, int]:
|
||||
_data = callback_query_data.split("|")
|
||||
_user_id = int(_data[1])
|
||||
_uid = int(_data[2])
|
||||
_result = _data[3]
|
||||
logger.debug("callback_query_data函数返回 result[%s] user_id[%s] uid[%s]", _result, _user_id, _uid)
|
||||
logger.debug(
|
||||
"callback_query_data函数返回 result[%s] user_id[%s] uid[%s]",
|
||||
_result,
|
||||
_user_id,
|
||||
_uid,
|
||||
)
|
||||
return _result, _user_id, _uid
|
||||
|
||||
result, user_id, uid = await get_player_card_callback(callback_query.data)
|
||||
@ -173,10 +263,22 @@ class PlayerCards(Plugin):
|
||||
page = 0
|
||||
if result.isdigit():
|
||||
page = int(result)
|
||||
logger.info("用户 %s[%s] 角色卡片查询命令请求 || page[%s] uid[%s]", user.full_name, user.id, page, uid)
|
||||
logger.info(
|
||||
"用户 %s[%s] 角色卡片查询命令请求 || page[%s] uid[%s]",
|
||||
user.full_name,
|
||||
user.id,
|
||||
page,
|
||||
uid,
|
||||
)
|
||||
else:
|
||||
logger.info("用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]", user.full_name, user.id, result, uid)
|
||||
data = await self._fetch_user(uid)
|
||||
logger.info(
|
||||
"用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]",
|
||||
user.full_name,
|
||||
user.id,
|
||||
result,
|
||||
uid,
|
||||
)
|
||||
data = await self._load_history(uid)
|
||||
if isinstance(data, str):
|
||||
await message.reply_text(data)
|
||||
return
|
||||
@ -185,7 +287,7 @@ class PlayerCards(Plugin):
|
||||
await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True)
|
||||
return
|
||||
if page:
|
||||
buttons = self.gen_button(data, user.id, uid, page)
|
||||
buttons = self.gen_button(data, user.id, uid, page, not await self.cache.ttl(uid) > 0)
|
||||
await message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(buttons))
|
||||
await callback_query.answer(f"已切换到第 {page} 页", show_alert=False)
|
||||
return
|
||||
@ -208,6 +310,7 @@ class PlayerCards(Plugin):
|
||||
user_id: Union[str, int],
|
||||
uid: int,
|
||||
page: int = 1,
|
||||
update_button: bool = True,
|
||||
) -> List[List[InlineKeyboardButton]]:
|
||||
"""生成按钮"""
|
||||
buttons = [
|
||||
@ -238,6 +341,13 @@ class PlayerCards(Plugin):
|
||||
callback_data=f"get_player_card|{user_id}|{uid}|empty_data",
|
||||
)
|
||||
)
|
||||
if update_button:
|
||||
last_button.append(
|
||||
InlineKeyboardButton(
|
||||
"更新面板",
|
||||
callback_data=f"update_player_card|{user_id}|{uid}",
|
||||
)
|
||||
)
|
||||
if next_page:
|
||||
last_button.append(
|
||||
InlineKeyboardButton(
|
||||
@ -249,6 +359,30 @@ class PlayerCards(Plugin):
|
||||
send_buttons.append(last_button)
|
||||
return send_buttons
|
||||
|
||||
async def parse_holder_data(self, data: EnkaNetworkResponse) -> dict:
|
||||
"""
|
||||
生成渲染所需数据
|
||||
"""
|
||||
characters_data = []
|
||||
for idx, character in enumerate(data.characters):
|
||||
characters_data.append(
|
||||
{
|
||||
"level": character.level,
|
||||
"element": character.element.name,
|
||||
"constellation": character.constellations_unlocked,
|
||||
"rarity": character.rarity,
|
||||
"icon": (await self.assets_service.avatar(character.id).icon()).as_uri(),
|
||||
}
|
||||
)
|
||||
if idx > 6:
|
||||
break
|
||||
return {
|
||||
"uid": data.uid,
|
||||
"level": data.player.level,
|
||||
"signature": data.player.signature,
|
||||
"characters": characters_data,
|
||||
}
|
||||
|
||||
|
||||
class Artifact(BaseModel):
|
||||
"""在 enka Equipments model 基础上扩展了圣遗物评分数据"""
|
||||
@ -301,7 +435,12 @@ class Artifact(BaseModel):
|
||||
|
||||
|
||||
class RenderTemplate:
|
||||
def __init__(self, uid: Union[int, str], character: CharacterInfo, template_service: TemplateService = None):
|
||||
def __init__(
|
||||
self,
|
||||
uid: Union[int, str],
|
||||
character: "CharacterInfo",
|
||||
template_service: TemplateService = None,
|
||||
):
|
||||
self.uid = uid
|
||||
self.template_service = template_service
|
||||
# 因为需要替换线上 enka 图片地址为本地地址,先克隆数据,避免修改原数据
|
||||
@ -443,7 +582,7 @@ class RenderTemplate:
|
||||
|
||||
stats = ArtifactStatsTheory(self.character.name)
|
||||
|
||||
def substat_score(s: EquipmentsStats) -> float:
|
||||
def substat_score(s: "EquipmentsStats") -> float:
|
||||
return stats.theory(s)
|
||||
|
||||
return [
|
||||
|
39
resources/genshin/player_card/holder.html
Normal file
39
resources/genshin/player_card/holder.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>holder</title>
|
||||
<link type="text/css" href="./style.css" rel="stylesheet"/>
|
||||
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="overview">
|
||||
<div class="title">角色展柜</div>
|
||||
<div class="summarize">
|
||||
<div>
|
||||
<div>UID: {{ uid }}</div>
|
||||
<div>冒险等阶: {{ level }} 级</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>签名: {{ signature }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="characters">
|
||||
{% for character in characters %}
|
||||
<div class="character">
|
||||
{% if character.constellation > 0 %}
|
||||
{% set bg = ['blue','blue', 'green','green', 'red', 'red'][character.constellation - 1] %}
|
||||
<div style="background-color: var(--{{ bg }})">{{ character.constellation }} 命</div>
|
||||
{% endif %}
|
||||
<div class="element" style="background-image: url('../../img/element/{{ character.element }}.png')"></div>
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/{{ character.rarity }}.png')">
|
||||
<img src="{{ character.icon }}" alt=""/>
|
||||
</div>
|
||||
<div class="caption">Lv.{{ character.level }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
58
resources/genshin/player_card/holder_example.html
Normal file
58
resources/genshin/player_card/holder_example.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-ch">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>holder_example</title>
|
||||
<link type="text/css" href="./style.css" rel="stylesheet"/>
|
||||
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="overview">
|
||||
<div class="title">角色展柜</div>
|
||||
<div class="summarize">
|
||||
<div>
|
||||
<div>UID: 123456789</div>
|
||||
<div>冒险等阶: 55</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>签名: 貴方の運命は、すでに我が手中の糸が絡めとった!填充</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="characters">
|
||||
<div class="character">
|
||||
<div style="background-color: var(--green)">4命</div>
|
||||
<div class="element" style="background-image: url('../../img/element/Cryo.png')"></div>
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
<div class="caption">Lv.90</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="character">
|
||||
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
|
||||
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
resources/genshin/player_card/img/holder_bg.png
Normal file
BIN
resources/genshin/player_card/img/holder_bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 301 KiB |
146
resources/genshin/player_card/style.css
Normal file
146
resources/genshin/player_card/style.css
Normal file
@ -0,0 +1,146 @@
|
||||
:root {
|
||||
--white: rgb(246 248 249);
|
||||
--bg-color: rgb(233 229 220);
|
||||
--h-color: rgb(203 189 162);
|
||||
--red: rgb(255 86 33/ 80%);
|
||||
--blue: rgb(98 168 233/ 80%);
|
||||
--green: rgb(67 185 124/ 80%);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: rgb(246 248 249 / 50%);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 750px;
|
||||
position: relative;
|
||||
filter: drop-shadow(2px 2px 5px rgb(0 0 0 /70%));
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 27px;
|
||||
font-weight: bold;
|
||||
color: var(--h-color);
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin: 10px 0;
|
||||
color: var(--h-color);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 概览 */
|
||||
|
||||
.overview {
|
||||
height: 540px;
|
||||
padding: 20px 30px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-image: linear-gradient(to top, rgb(0 0 0 / 10%), rgb(0 0 0 / 10%)), url("./img/holder_bg.png");
|
||||
background-attachment: local;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summarize {
|
||||
font-size: 20px;
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
border: 2px solid rgb(118 121 120 / 80%);
|
||||
outline: 4px solid rgb(70, 80, 100);
|
||||
background-color: rgb(70 80 100 / 60%);
|
||||
background-image: url("../abyss/background/banner 01.png"), url("../abyss/background/banner 02.png");
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-position: right, left;
|
||||
background-size: auto 100%, auto 100%;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.summarize > div {
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
padding: 5px;
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summarize > div > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.characters {
|
||||
margin-left: 47px;
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.character {
|
||||
width: 120px;
|
||||
height: 150px;
|
||||
margin: 15px 12px;
|
||||
background-color: rgb(233 229 220);
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.characters > .character > .element {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.character > .caption {
|
||||
font-size: 16px;
|
||||
margin: 4px 0 0;
|
||||
padding: 0;
|
||||
height: min-content;
|
||||
text-align: center;
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.character > div:first-child:not(.icon, .element) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 3px;
|
||||
min-width: 27px;
|
||||
text-align: center;
|
||||
border-radius: 0 0 0 10px;
|
||||
filter: drop-shadow(1px 1px 5px rgb(0 0 0/50%));
|
||||
font-weight: 500;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.icon > img {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
@ -1,16 +1,23 @@
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, TYPE_CHECKING
|
||||
|
||||
from enkanetwork import Cache
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
try:
|
||||
import ujson as jsonlib
|
||||
except ImportError:
|
||||
import json as jsonlib
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from redis import asyncio as aioredis
|
||||
|
||||
__all__ = ("RedisCache",)
|
||||
|
||||
|
||||
class RedisCache(Cache):
|
||||
def __init__(self, redis: aioredis.Redis, key: Optional[str] = None, ttl: int = 60 * 3) -> None:
|
||||
def __init__(self, redis: "aioredis.Redis", key: Optional[str] = None, ex: int = 60 * 3) -> None:
|
||||
self.redis = redis
|
||||
self.ttl = ttl
|
||||
self.ex = ex
|
||||
self.key = key
|
||||
|
||||
def get_qname(self, key):
|
||||
@ -21,10 +28,18 @@ class RedisCache(Cache):
|
||||
data = await self.redis.get(qname)
|
||||
if data:
|
||||
json_data = str(data, encoding="utf-8")
|
||||
return json.loads(json_data)
|
||||
return jsonlib.loads(json_data)
|
||||
return None
|
||||
|
||||
async def set(self, key, value) -> None:
|
||||
qname = self.get_qname(key)
|
||||
data = json.dumps(value)
|
||||
await self.redis.set(qname, data, ex=self.ttl)
|
||||
data = jsonlib.dumps(value)
|
||||
await self.redis.set(qname, data, ex=self.ex)
|
||||
|
||||
async def exists(self, key) -> int:
|
||||
qname = self.get_qname(key)
|
||||
return await self.redis.exists(qname)
|
||||
|
||||
async def ttl(self, key) -> int:
|
||||
qname = self.get_qname(key)
|
||||
return await self.redis.ttl(qname)
|
||||
|
Loading…
Reference in New Issue
Block a user