diff --git a/core/dependence/influxdb.py b/core/dependence/influxdb.py new file mode 100644 index 0000000..b4e286a --- /dev/null +++ b/core/dependence/influxdb.py @@ -0,0 +1,3 @@ +from gram_core.dependence.influxdb import InfluxDatabase + +__all__ = ("InfluxDatabase",) diff --git a/core/services/self_help/__init__.py b/core/services/self_help/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/services/self_help/models.py b/core/services/self_help/models.py new file mode 100644 index 0000000..704ff81 --- /dev/null +++ b/core/services/self_help/models.py @@ -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"] + ) diff --git a/core/services/self_help/repositories.py b/core/services/self_help/repositories.py new file mode 100644 index 0000000..b848527 --- /dev/null +++ b/core/services/self_help/repositories.py @@ -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 diff --git a/core/services/self_help/services.py b/core/services/self_help/services.py new file mode 100644 index 0000000..d0d63cd --- /dev/null +++ b/core/services/self_help/services.py @@ -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 diff --git a/modules/action_log/__init__.py b/modules/action_log/__init__.py new file mode 100644 index 0000000..de052e6 --- /dev/null +++ b/modules/action_log/__init__.py @@ -0,0 +1 @@ +"""登录日志分析""" diff --git a/modules/action_log/client.py b/modules/action_log/client.py new file mode 100644 index 0000000..e2b2524 --- /dev/null +++ b/modules/action_log/client.py @@ -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 diff --git a/modules/action_log/date.py b/modules/action_log/date.py new file mode 100644 index 0000000..2697e11 --- /dev/null +++ b/modules/action_log/date.py @@ -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) diff --git a/modules/action_log/models.py b/modules/action_log/models.py new file mode 100644 index 0000000..23679bd --- /dev/null +++ b/modules/action_log/models.py @@ -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 diff --git a/pdm.lock b/pdm.lock index d74d17d..556e95d 100644 --- a/pdm.lock +++ b/pdm.lock @@ -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 = [ diff --git a/plugins/jobs/import_action_log.py b/plugins/jobs/import_action_log.py new file mode 100644 index 0000000..5d6c260 --- /dev/null +++ b/plugins/jobs/import_action_log.py @@ -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("全部账号导入登录记录任务完成") diff --git a/plugins/tools/action_log_system.py b/plugins/tools/action_log_system.py new file mode 100644 index 0000000..b704a47 --- /dev/null +++ b/plugins/tools/action_log_system.py @@ -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) diff --git a/plugins/zzz/action_log.py b/plugins/zzz/action_log.py new file mode 100644 index 0000000..4a1e68e --- /dev/null +++ b/plugins/zzz/action_log.py @@ -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, + ) + ] diff --git a/requirements.txt b/requirements.txt index 87c29b1..dafd982 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/resources/img/logo.png b/resources/img/logo.png index 3e0bce0..80fd301 100644 Binary files a/resources/img/logo.png and b/resources/img/logo.png differ diff --git a/resources/zzz/action_log/action_log.css b/resources/zzz/action_log/action_log.css new file mode 100644 index 0000000..6373ac3 --- /dev/null +++ b/resources/zzz/action_log/action_log.css @@ -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; +} diff --git a/resources/zzz/action_log/action_log.html b/resources/zzz/action_log/action_log.html new file mode 100644 index 0000000..7ad5b6f --- /dev/null +++ b/resources/zzz/action_log/action_log.html @@ -0,0 +1,133 @@ + + +
+ + + + + +