Support zzz action log

This commit is contained in:
xtaodada 2024-07-06 13:35:04 +08:00
parent bc4c4053a3
commit 0e51d50a05
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
19 changed files with 870 additions and 4 deletions

View File

@ -0,0 +1,3 @@
from gram_core.dependence.influxdb import InfluxDatabase
__all__ = ("InfluxDatabase",)

View File

View File

@ -0,0 +1,31 @@
from datetime import timedelta
from influxdb_client import Point
from influxdb_client.client.flux_table import FluxRecord
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
from modules.action_log.date import TZ
FIX = timedelta(minutes=6)
class ActionLogModel:
@staticmethod
def en(data: "ZZZSelfHelpActionLog") -> Point:
return (
Point.measurement("action_log")
.tag("uid", data.uid)
.field("id", data.id)
.field("status", data.status)
.field("reason", data.reason.value)
.field("client_ip", data.client_ip)
.time(data.time.replace(tzinfo=TZ) + FIX)
)
@staticmethod
def de(data: "FluxRecord") -> "ZZZSelfHelpActionLog":
utc_time = data.get_time()
time = utc_time.astimezone(TZ)
return ZZZSelfHelpActionLog(
id=data["id"], uid=data["uid"], datetime=time, action_name=data["reason"], client_ip=data["client_ip"]
)

View File

@ -0,0 +1,56 @@
from typing import TYPE_CHECKING, List
from gram_core.base_service import BaseService
from gram_core.dependence.influxdb import InfluxDatabase
if TYPE_CHECKING:
from influxdb_client.client.flux_table import FluxTable
from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync
from influxdb_client import Point
class ActionLogRepository(BaseService.Component):
def __init__(self, influxdb: InfluxDatabase):
self.influxdb = influxdb
self.client = influxdb.client
self.bucket = "zzz"
async def add(self, p: List["Point"]) -> bool:
async with self.client() as client:
client: "InfluxDBClientAsync"
return await client.write_api().write(self.bucket, record=p)
async def count_uptime_period(self, uid: int) -> "FluxTable":
async with self.client() as client:
client: "InfluxDBClientAsync"
query = (
'import "date"'
'import "timezone"'
'option location = timezone.location(name: "Asia/Shanghai")'
'from(bucket: "{}")'
"|> range(start: -180d)"
'|> filter(fn: (r) => r["_measurement"] == "action_log")'
'|> filter(fn: (r) => r["_field"] == "status")'
'|> filter(fn: (r) => r["_value"] == 1)'
'|> filter(fn: (r) => r["uid"] == "{}")'
"|> aggregateWindow(every: 1h, fn: count)"
).format(self.bucket, uid)
query += '|> map(fn: (r) => ({r with hour: date.hour(t: r._time)}))|> yield(name: "hourly_count")'
tables = await client.query_api().query(query)
for table in tables:
return table
async def get_data(self, uid: int, day: int = 30) -> "FluxTable":
async with self.client() as client:
client: "InfluxDBClientAsync"
query = (
'from(bucket: "{}")'
"|> range(start: -{}d)"
'|> filter(fn: (r) => r["_measurement"] == "action_log")'
'|> filter(fn: (r) => r["uid"] == "{}")'
'|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")'
).format(self.bucket, day, uid)
tables = await client.query_api().query(query)
for table in tables:
return table

View File

@ -0,0 +1,37 @@
from typing import List, TYPE_CHECKING, Dict
from core.services.self_help.models import ActionLogModel
from core.services.self_help.repositories import ActionLogRepository
from gram_core.base_service import BaseService
if TYPE_CHECKING:
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
class ActionLogService(BaseService):
def __init__(self, repository: ActionLogRepository):
self.repository = repository
async def add(self, p: List["ZZZSelfHelpActionLog"]) -> bool:
return await self.repository.add([ActionLogModel.en(data) for data in p])
async def count_uptime_period(self, uid: int) -> Dict[int, int]:
"""计算最近一个月不同时间点的登录次数"""
data = {k: 0 for k in range(24)}
r = await self.repository.count_uptime_period(uid)
if not r:
return data
for record in r.records:
if record.get_value():
data[record["hour"]] += 1
return data
async def get_data(self, uid: int, day: int = 30) -> List["ZZZSelfHelpActionLog"]:
"""获取指定天数内的某用户的登录记录"""
data = []
r = await self.repository.get_data(uid, day)
if not r:
return data
for record in r.records:
data.append(ActionLogModel.de(record))
return data

View File

@ -0,0 +1 @@
"""登录日志分析"""

View File

@ -0,0 +1,150 @@
from typing import List, Dict, Union
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
from modules.action_log.date import DateUtils
from modules.action_log.models import ActionLogPair
class ActionLogAnalyse(DateUtils):
def __init__(self, data: List[ZZZSelfHelpActionLog], data2: Dict[int, int]):
super().__init__()
self.data = data
self.data2 = data2
self.pairs = self.init_pair(data)
self.this_week_data: List[ActionLogPair] = []
self.last_week_data: List[ActionLogPair] = []
self.this_month_data: List[ActionLogPair] = []
self.last_month_data: List[ActionLogPair] = []
self.init_pair_data()
def init_pair_data(self):
"""通过时间点判断数据属于哪个时间段"""
for d in self.pairs:
if self.week_start <= d.start_time < self.week_end:
self.this_week_data.append(d)
elif self.week_last_start <= d.start_time < self.week_last_end:
self.last_week_data.append(d)
if self.month_start <= d.start_time < self.month_end:
self.this_month_data.append(d)
elif self.month_last_start <= d.start_time < self.month_last_end:
self.last_month_data.append(d)
@staticmethod
def init_pair(data: List[ZZZSelfHelpActionLog]) -> List[ActionLogPair]:
# 确保第一个数据为登入,最后一条数据为登出
if data[0].status == 0:
data.pop(0)
if data[-1].status == 1:
data.pop(-1)
pairs = []
for i in range(0, len(data), 2):
pairs.append(ActionLogPair(start=data[i], end=data[i + 1]))
return pairs
def get_this_week_duration(self) -> int:
"""本周时长"""
return (sum(d.duration.seconds for d in self.this_week_data)) if self.this_week_data else 0
def get_last_week_duration(self) -> int:
"""上周时长"""
return (sum(d.duration.seconds for d in self.last_week_data)) if self.last_week_data else 0
def get_this_month_duration(self) -> int:
"""本月时长"""
return (sum(d.duration.seconds for d in self.this_month_data)) if self.this_month_data else 0
def get_last_month_duration(self) -> int:
"""上月时长"""
return (sum(d.duration.seconds for d in self.last_month_data)) if self.last_month_data else 0
def get_this_month_avg_duration(self) -> float:
"""本月平均时长"""
data_len = len(self.this_month_data)
return (self.get_this_month_duration() / data_len) if data_len else 0
def get_last_month_avg_duration(self) -> float:
"""上月平均时长"""
data_len = len(self.last_month_data)
return (self.get_last_month_duration() / data_len) if data_len else 0
def get_this_week_long_duration(self) -> int:
"""周最长会话"""
return (max(d.duration.seconds for d in self.this_week_data)) if self.this_week_data else 0
def get_this_week_short_duration(self) -> int:
"""周最短会话"""
return (min(d.duration.seconds for d in self.this_week_data)) if self.this_week_data else 0
def get_this_month_long_duration(self) -> int:
"""月最长会话"""
return (max(d.duration.seconds for d in self.this_month_data)) if self.this_month_data else 0
def get_this_month_short_duration(self) -> int:
"""月最短会话"""
return (min(d.duration.seconds for d in self.this_month_data)) if self.this_month_data else 0
@staticmethod
def format_sec(sec: Union[int, float]) -> str:
hour = sec // 3600
minute = (sec % 3600) // 60
second = sec % 60
if hour:
return f"{int(hour)}{int(minute)}"
if minute:
return f"{int(minute)}{int(second)}"
return f"{int(second)}"
def get_data(self) -> List[Dict[str, str]]:
data = {
"本周时长": self.get_this_week_duration(),
"上周时长": self.get_last_week_duration(),
"本月时长": self.get_this_month_duration(),
"上月时长": self.get_last_month_duration(),
"本月平均": self.get_this_month_avg_duration(),
"上月平均": self.get_last_month_avg_duration(),
"周最长会话": self.get_this_week_long_duration(),
"月最长会话": self.get_this_month_long_duration(),
"周最短会话": self.get_this_week_short_duration(),
"月最短会话": self.get_this_month_short_duration(),
}
datas = []
for k, v in data.items():
datas.append(
{
"name": k,
"value": self.format_sec(v),
}
)
max_hour = max(self.data2, key=self.data2.get)
min_hour = min(self.data2, key=self.data2.get)
datas.append({"name": "最常上线", "value": f"{max_hour}"})
datas.append({"name": "最少上线", "value": f"{min_hour}"})
return datas
def get_line_data(self) -> List[Dict[str, str]]:
data = []
for k, v in self.data2.items():
data.append(
{
"month": f"{k}",
"value": v,
}
)
return data
def get_record_data(self) -> List[Dict[str, str]]:
data = []
limit = 4
data_len = len(self.data) - 1
for i in range(data_len, data_len - limit, -1):
record = self.data[i]
data.append(
{
"time": record.time.strftime("%Y年%m月%d%H:%M:%S"),
"reason": record.reason.value,
}
)
return data

View File

@ -0,0 +1,36 @@
from datetime import datetime, timedelta
from pytz import timezone
TZ = timezone("Asia/Shanghai")
class DateUtils:
def __init__(self):
self.now = datetime.now(tz=TZ)
# 本周
self.week_start = self.date_start(self.now - timedelta(days=self.now.weekday()))
self.week_end = self.week_start + timedelta(days=7)
# 上周
self.week_last_start = self.week_start - timedelta(days=7)
self.week_last_end = self.week_start
# 本月
self.month_start = self.date_start(self.now.replace(day=1))
self.month_end = self.get_month_end(self.month_start)
# 上月
month_last = (self.month_start - timedelta(days=1)).replace(day=1)
self.month_last_start = self.date_start(month_last)
self.month_last_end = self.get_month_end(month_last)
@staticmethod
def date_start(date: datetime) -> datetime:
return date.replace(hour=4, minute=0, second=0, microsecond=0)
def get_week_start(self) -> datetime:
day = self.now - timedelta(days=self.now.weekday())
return self.date_start(day)
def get_month_end(self, date: datetime) -> datetime:
next_month = date.replace(day=28) + timedelta(days=5)
next_month_start = next_month.replace(day=1)
return self.date_start(next_month_start)

View File

@ -0,0 +1,21 @@
from datetime import datetime, timedelta
from pydantic import BaseModel
from simnet.models.zzz.self_help import ZZZSelfHelpActionLog
class ActionLogPair(BaseModel):
start: ZZZSelfHelpActionLog
end: ZZZSelfHelpActionLog
@property
def start_time(self) -> datetime:
return self.start.time
@property
def end_time(self) -> datetime:
return self.end.time
@property
def duration(self) -> timedelta:
return self.end.time - self.start.time

View File

@ -2212,7 +2212,7 @@ name = "simnet"
version = "0.1.22"
requires_python = "<4.0,>=3.8"
git = "https://github.com/PaiGramTeam/SIMNet"
revision = "185733af85cabb57bf7e64bf6c46a35c515fef2e"
revision = "9839a06bb87403443ac2cbea0ffde4156b05d902"
summary = "Modern API wrapper for Genshin Impact & Honkai: Star Rail built on asyncio and pydantic."
groups = ["default"]
dependencies = [

View File

@ -0,0 +1,29 @@
import datetime
from typing import TYPE_CHECKING
from gram_core.plugin import Plugin, job, handler
from plugins.tools.action_log_system import ActionLogSystem
from utils.log import logger
if TYPE_CHECKING:
from telegram import Update
from telegram.ext import ContextTypes
class ImportActionLogJob(Plugin):
def __init__(self, action_log_system: ActionLogSystem = None):
self.action_log_system = action_log_system
@job.run_daily(time=datetime.time(hour=12, minute=1, second=0), name="ImportActionLogJob")
async def refresh(self, _: "ContextTypes.DEFAULT_TYPE"):
await self.action_log_system.daily_import_login(_)
@handler.command(command="action_log_import_all", block=False, admin=True)
async def action_log_import_all(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"):
user = update.effective_user
logger.info("用户 %s[%s] action_log_import_all 命令请求", user.full_name, user.id)
message = update.effective_message
reply = await message.reply_text("正在执行导入登录记录任务,请稍后...")
await self.refresh(context)
await reply.edit_text("全部账号导入登录记录任务完成")

View File

@ -0,0 +1,80 @@
from asyncio import sleep
from typing import TYPE_CHECKING
from simnet.errors import (
TimedOut as SimnetTimedOut,
BadRequest as SimnetBadRequest,
InvalidCookies,
)
from core.services.self_help.services import ActionLogService
from gram_core.basemodel import RegionEnum
from gram_core.plugin import Plugin
from gram_core.services.cookies import CookiesService
from gram_core.services.cookies.models import CookiesStatusEnum
from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError
from utils.log import logger
if TYPE_CHECKING:
from telegram.ext import ContextTypes
from simnet import ZZZClient
class ActionLogSystem(Plugin):
"""登录记录系统"""
def __init__(
self,
cookies: CookiesService,
helper: GenshinHelper,
action_log_service: ActionLogService,
):
self.cookies = cookies
self.helper = helper
self.action_log_service = action_log_service
async def import_action_log(self, client: "ZZZClient", authkey: str) -> bool:
data = await client.get_zzz_action_log(authkey=authkey)
# 确保第一个数据为登出、最后一条数据为登入
if not data:
return False
if data[0].status == 1:
data.pop(0)
if data[-1].status == 0:
data.pop(-1)
return await self.action_log_service.add(data)
async def daily_import_login(self, _: "ContextTypes.DEFAULT_TYPE"):
logger.info("正在执行每日刷新登录记录任务")
for cookie_model in await self.cookies.get_all(
region=RegionEnum.HYPERION, status=CookiesStatusEnum.STATUS_SUCCESS
):
user_id = cookie_model.user_id
cookies = cookie_model.data
if cookies.get("stoken") is None:
continue
try:
async with self.helper.genshin(user_id, region=RegionEnum.HYPERION) as client:
client: "StarRailClient"
try:
authkey = await client.get_authkey_by_stoken("csc")
except ValueError:
logger.warning("用户 user_id[%s] 请求登录记录失败 无 stoken", user_id)
continue
await self.import_action_log(client, authkey)
except (InvalidCookies, PlayerNotFoundError, CookiesNotFoundError):
continue
except SimnetBadRequest as exc:
logger.warning(
"用户 user_id[%s] 请求登录记录失败 [%s]%s", user_id, exc.ret_code, exc.original or exc.message
)
continue
except SimnetTimedOut:
logger.info("用户 user_id[%s] 请求登录记录超时", user_id)
continue
except Exception as exc:
logger.error("执行自动刷新登录记录时发生错误 user_id[%s]", user_id, exc_info=exc)
continue
await sleep(1)

155
plugins/zzz/action_log.py Normal file
View File

@ -0,0 +1,155 @@
from typing import TYPE_CHECKING, Dict, List, Optional
from telegram.constants import ChatAction
from telegram.ext import filters
from simnet import Region
from core.services.self_help.services import ActionLogService
from gram_core.config import config
from gram_core.plugin import Plugin, handler
from gram_core.plugin.methods.inline_use_data import IInlineUseData
from gram_core.services.template.services import TemplateService
from modules.action_log.client import ActionLogAnalyse
from plugins.tools.action_log_system import ActionLogSystem
from plugins.tools.genshin import GenshinHelper
from utils.const import RESOURCE_DIR
from utils.log import logger
from utils.uid import mask_number
if TYPE_CHECKING:
from telegram import Update
from telegram.ext import ContextTypes
from simnet import ZZZClient
from gram_core.services.template.models import RenderResult
class NotSupport(Exception):
"""不支持的服务器"""
def __init__(self, msg: str = None):
self.msg = msg
class ActionLogPlugins(Plugin):
"""登录记录信息查询"""
def __init__(
self,
helper: GenshinHelper,
action_log_service: ActionLogService,
action_log_system: ActionLogSystem,
template_service: TemplateService,
):
self.helper = helper
self.action_log_service = action_log_service
self.action_log_system = action_log_system
self.template_service = template_service
@handler.command(command="action_log_import", filters=filters.ChatType.PRIVATE, cookie=True, block=False)
async def action_log_import(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
user_id = await self.get_real_user_id(update)
message = update.effective_message
uid, offset = self.get_real_uid_or_offset(update)
self.log_user(update, logger.info, "导入登录记录")
await message.reply_chat_action(ChatAction.TYPING)
try:
async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client:
client: "ZZZClient"
if client.region != Region.CHINESE:
raise NotSupport("不支持的服务器")
try:
authkey = await client.get_authkey_by_stoken("csc")
except ValueError as e:
raise NotSupport("未绑定 stoken") from e
notice = await message.reply_text(f"{config.notice.bot_name}需要收集整理数据,还请耐心等待哦~")
bo = await self.action_log_system.import_action_log(client, authkey)
text = "导入登录记录成功" if bo else "导入登录记录失败,可能没有新记录"
await notice.edit_text(text)
self.log_user(update, logger.success, text)
except NotSupport as e:
msg = await message.reply_text(e.msg)
if filters.ChatType.GROUPS.filter(message):
self.add_delete_message_job(message, delay=60)
self.add_delete_message_job(msg, delay=60)
async def get_render_data(self, uid: int):
r = await self.action_log_service.get_data(uid, 63)
r2 = await self.action_log_service.count_uptime_period(uid)
if not r or not r2:
raise NotSupport("未查询到登录记录")
d = ActionLogAnalyse(r, r2)
data = d.get_data()
line_data = d.get_line_data()
records = d.get_record_data()
return {
"uid": mask_number(uid),
"datas": data,
"line_data": line_data,
"records": records,
}
async def add_theme_data(self, data: Dict, player_id: int):
res = RESOURCE_DIR / "img"
data["avatar"] = (res / "avatar.png").as_uri()
data["background"] = (res / "home.png").as_uri()
return data
async def render(self, client: "ZZZClient") -> "RenderResult":
data = await self.get_render_data(client.player_id)
return await self.template_service.render(
"zzz/action_log/action_log.html",
await self.add_theme_data(data, client.player_id),
full_page=True,
query_selector=".container",
)
@handler.command(command="action_log", cookie=True, block=False)
async def action_log(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
user_id = await self.get_real_user_id(update)
message = update.effective_message
uid, offset = self.get_real_uid_or_offset(update)
self.log_user(update, logger.info, "查询登录记录")
try:
async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client:
client: "ZZZClient"
render = await self.render(client)
await render.reply_photo(message)
except NotSupport as e:
msg = await message.reply_text(e.msg)
if filters.ChatType.GROUPS.filter(message):
self.add_delete_message_job(message, delay=60)
self.add_delete_message_job(msg, delay=60)
async def action_log_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
callback_query = update.callback_query
user = update.effective_user
user_id = user.id
uid = IInlineUseData.get_uid_from_context(context)
self.log_user(update, logger.info, "查询登录记录")
try:
async with self.helper.genshin(user_id, player_id=uid) as client:
client: "ZZZClient"
render = await self.render(client)
except NotSupport as e:
await callback_query.answer(e.msg, show_alert=True)
return
await render.edit_inline_media(callback_query)
async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]:
return [
IInlineUseData(
text="登录统计",
hash="action_log",
callback=self.action_log_use_by_inline,
cookie=True,
player=True,
)
]

View File

@ -88,7 +88,7 @@ rich==13.7.1
sentry-sdk==2.7.1
setuptools==70.2.0
shellingham==1.5.4
simnet @ git+https://github.com/PaiGramTeam/SIMNet@185733af85cabb57bf7e64bf6c46a35c515fef2e
simnet @ git+https://github.com/PaiGramTeam/SIMNet@9839a06bb87403443ac2cbea0ffde4156b05d902
six==1.16.0
smmap==5.0.1
sniffio==1.3.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,134 @@
@font-face {
font-family: "tttgbnumber";
src: url("../../fonts/tttgbnumber.ttf");
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
font-size: 16px;
width: 700px;
color: #1e1f20;
transform: scale(1.3);
transform-origin: 0 0;
}
.container {
width: 700px;
padding: 20px 15px 10px 15px;
background-color: #f5f6fb;
}
.head_box {
border-radius: 15px;
font-family: tttgbnumber;
padding: 10px 20px;
position: relative;
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
display: flex;
}
.head_box_avatar {
height: 60px;
margin-right: 20px;
}
.head_box .id_text {
color: white;
font-size: 24px;
}
.head_box .day_text {
color: white;
font-size: 20px;
}
.head_box .genshin_logo {
position: absolute;
top: 15px;
right: 15px;
width: 97px;
}
.base_info {
position: relative;
padding-left: 10px;
}
.uid {
font-family: tttgbnumber;
}
.data_box {
border-radius: 15px;
margin-top: 20px;
margin-bottom: 20px;
padding: 20px 15px 5px 15px;
background: #fff;
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%);
position: relative;
}
.tab_lable {
position: absolute;
top: -10px;
left: -8px;
background: #d4b98c;
color: #fff;
font-size: 14px;
padding: 3px 10px;
border-radius: 15px 0 15px 15px;
z-index: 20;
}
.data_line {
display: flex;
justify-content: space-around;
margin-bottom: 14px;
}
.data_line_item {
width: 200px;
text-align: center;
/*margin: 0 20px;*/
}
.num {
font-family: tttgbnumber;
font-size: 24px;
}
.data_box .lable {
font-size: 14px;
color: #7f858a;
line-height: 1;
margin-top: 3px;
}
#chartContainer {
width: 100%;
height: 300px;
}
#chartContainer2 {
width: 100%;
height: 300px;
}
.logo {
font-size: 14px;
font-family: "tttgbnumber";
text-align: center;
color: #7994a7;
}
.small_font {
font-size: 15px;
}

View File

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<link rel="shortcut icon" href="#" />
<link rel="stylesheet" type="text/css" href="action_log.css" />
<link rel="preload" href="../../fonts/tttgbnumber.ttf" as="font">
<style>
.head_box {
background: #fff url("{{ background }}") no-repeat;
background-size: cover;
}
</style>
<title></title>
</head>
<body>
<div class="container" id="container">
<div class="head_box">
<img class="head_box_avatar" src="{{ avatar }}" alt="Avatar">
<div>
<div class="id_text">ID: {{ uid }}</div>
<h2 class="day_text">登录统计</h2>
</div>
<img class="genshin_logo" src="./../../img/logo.png" alt=""/>
</div>
<div class="data_box">
<div class="tab_lable">数据总览</div>
<div class="data_line">
{% for data in datas[:4] %}
<div class="data_line_item">
<div class="num">{{ data.value }}</div>
<div class="lable">{{ data.name }}</div>
</div>
{% endfor %}
</div>
<div class="data_line">
{% for data in datas[4:8] %}
<div class="data_line_item">
<div class="num">{{ data.value }}</div>
<div class="lable">{{ data.name }}</div>
</div>
{% endfor %}
</div>
<div class="data_line">
{% for data in datas[8:] %}
<div class="data_line_item">
<div class="num">{{ data.value }}</div>
<div class="lable">{{ data.name }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="data_box">
<div class="tab_lable">登录时间点统计</div>
<div id="chartContainer"></div>
</div>
<div class="data_box">
<div class="tab_lable">最近日志</div>
{% for record in records %}
<div class="data_line">
<div class="data_line_item">
<div class="num small_font">{{ record.time }}</div>
</div>
<div class="data_line_item">
<div class="num small_font">{{ record.reason }}</div>
</div>
</div>
{% endfor %}
</div>
<div class="logo"> Generate by MibooGram</div>
</div>
</body>
<script src="../../genshin/pay_log/echarts.min.js"></script>
<script>
const barData = {{ line_data | tojson }};
const myChart1 = echarts.init(document.querySelector('#chartContainer'), null, {renderer: 'svg'});
const xData = barData.map(v => v.month)
const yData = barData.map(v => v.value)
const option = {
animation: false,
xAxis: {
type: 'category',
data: xData
},
legend: {
x:'left',
y:'top',
show: true,
data: [{ name: '次' }]
},
yAxis: {
type: 'value'
},
series: [
{
name:'次',
data: yData,
type: 'line',
smooth: true,
itemStyle: {
normal: {
label: {
position: 'top',
show: true,
textStyle: {
color: '#1e1f20',
fontSize: 14,
fontFamily: "tttgbnumber",
}
},
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [{
offset: 0,
color: "#1268f3"
}, {
offset: 0.6,
color: "#08a4fa"
}, {
offset: 1,
color: "#01ccfe"
}], false)
},
},
}
]
};
myChart1.setOption(option);
</script>
</html>

View File

@ -52,7 +52,7 @@
{% if hasMore %}
<div class="hasMore">*完整数据请私聊查看</div>
{% endif %}
<div class="logo">Template By Yunzai-Bot x Generated By PamGram</div>
<div class="logo">Template By Yunzai-Bot x Generated By MibooGram</div>
</div>
</body>
</html>

View File

@ -76,7 +76,7 @@
{% endfor %}
</div>
</div>
<div class="logo">Template By Yunzai-Bot x Generated By PamGram</div>
<div class="logo">Template By Yunzai-Bot x Generated By MibooGram</div>
</div>
</div>
</body>