112
This commit is contained in:
+20
-7
@@ -4,7 +4,7 @@ from astrbot.api import logger
|
|||||||
|
|
||||||
from .class_reqsest import APIClient
|
from .class_reqsest import APIClient
|
||||||
from .cless_mysql import AsyncMySQL
|
from .cless_mysql import AsyncMySQL
|
||||||
from .function_basic import load_template, extract_field,flatten_field,extract_fields,gold_to_string
|
from .function_basic import load_template,extract_field,flatten_field,extract_fields,gold_to_string,plot_line_chart_base64
|
||||||
|
|
||||||
class JX3Function:
|
class JX3Function:
|
||||||
def __init__(self, api_config,db: AsyncMySQL ):
|
def __init__(self, api_config,db: AsyncMySQL ):
|
||||||
@@ -141,17 +141,26 @@ class JX3Function:
|
|||||||
return return_data
|
return return_data
|
||||||
# 加载模板
|
# 加载模板
|
||||||
try:
|
try:
|
||||||
return_data["temp"] = load_template("jinjia.html")
|
return_data["temp"] = load_template("temp_test.html")
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
logger.error(f"加载模板失败: {e}")
|
logger.error(f"加载模板失败: {e}")
|
||||||
return_data["msg"] = "系统错误:模板文件不存在"
|
return_data["msg"] = "系统错误:模板文件不存在"
|
||||||
return return_data
|
return return_data
|
||||||
|
try:
|
||||||
|
chart_base64 = plot_line_chart_base64(data, "date", "priceWanbaolou", "万宝楼金价走势",True)
|
||||||
|
logger.info(f"生成折线图成功: {chart_base64}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成折线图失败: {e}")
|
||||||
|
return_data["msg"] = "系统错误:生成折线图失败"
|
||||||
|
return return_data
|
||||||
# 准备模板渲染数据
|
# 准备模板渲染数据
|
||||||
try:
|
try:
|
||||||
|
|
||||||
return_data["data"] = {
|
return_data["data"] = {
|
||||||
"items": data,
|
"items": data,
|
||||||
"server": api_config["params"]["serverName"],
|
"server": api_config["params"]["serverName"],
|
||||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"img": chart_base64
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"处理数据时出错: {e}")
|
logger.error(f"处理数据时出错: {e}")
|
||||||
@@ -252,7 +261,8 @@ class JX3Function:
|
|||||||
return_data["msg"] = f"成功批量插入 {len(extracted_data)} 条数据!"
|
return_data["msg"] = f"成功批量插入 {len(extracted_data)} 条数据!"
|
||||||
return_data["code"] = 200
|
return_data["code"] = 200
|
||||||
return return_data
|
return return_data
|
||||||
|
|
||||||
|
|
||||||
async def wujia(self,Name: str = "秃盒"):
|
async def wujia(self,Name: str = "秃盒"):
|
||||||
return_data = {
|
return_data = {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
@@ -338,7 +348,8 @@ class JX3Function:
|
|||||||
return return_data
|
return return_data
|
||||||
return_data["code"] = 200
|
return_data["code"] = 200
|
||||||
return return_data
|
return return_data
|
||||||
|
|
||||||
|
|
||||||
async def __get_wbl_data(self,search_id):
|
async def __get_wbl_data(self,search_id):
|
||||||
"""获取万宝楼数据(公示和在售)"""
|
"""获取万宝楼数据(公示和在售)"""
|
||||||
try:
|
try:
|
||||||
@@ -360,7 +371,8 @@ class JX3Function:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取万宝楼数据出错: {e}")
|
logger.error(f"获取万宝楼数据出错: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def __process_wbl_records(self,records):
|
async def __process_wbl_records(self,records):
|
||||||
"""处理万宝楼记录数据"""
|
"""处理万宝楼记录数据"""
|
||||||
processed = []
|
processed = []
|
||||||
@@ -381,7 +393,8 @@ class JX3Function:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
return processed
|
return processed
|
||||||
|
|
||||||
|
|
||||||
async def jiaoyihang(self,Name: str = "守缺式",server: str = "梦江南"):
|
async def jiaoyihang(self,Name: str = "守缺式",server: str = "梦江南"):
|
||||||
return_data = {
|
return_data = {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
|
|||||||
+52
-1
@@ -1,5 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.font_manager import FontProperties
|
||||||
|
from io import BytesIO
|
||||||
|
import base64
|
||||||
|
|
||||||
def load_template(template_name):
|
def load_template(template_name):
|
||||||
"""
|
"""
|
||||||
@@ -113,4 +117,51 @@ def gold_to_string(gold_amount):
|
|||||||
if started:
|
if started:
|
||||||
parts.append(f"{value}{unit}")
|
parts.append(f"{value}{unit}")
|
||||||
|
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_line_chart_base64(data, x_field, y_field, title=None, reverse_x=False):
|
||||||
|
"""
|
||||||
|
根据列表数据绘制折线图,返回 base64 图片字符串。
|
||||||
|
|
||||||
|
:param data: list[dict] 数据列表
|
||||||
|
:param x_field: str X轴字段
|
||||||
|
:param y_field: str Y轴字段
|
||||||
|
:param title: str 图表标题(可选)
|
||||||
|
:param reverse_x: bool 是否反转 X 轴方向(默认 False:从左往右;True:从右往左)
|
||||||
|
:return: str base64 图片字符串,可直接放到 <img src="..."> 中
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
raise ValueError("数据列表不能为空")
|
||||||
|
|
||||||
|
# 字体设置(支持中文)
|
||||||
|
plt.rcParams['font.sans-serif'] = ['SimHei']
|
||||||
|
plt.rcParams['axes.unicode_minus'] = False
|
||||||
|
|
||||||
|
# 提取 X / Y 数据
|
||||||
|
x_values = [str(item.get(x_field, "")) for item in data]
|
||||||
|
y_values = [float(item.get(y_field, 0)) for item in data]
|
||||||
|
|
||||||
|
# 创建画布
|
||||||
|
plt.figure(figsize=(8, 5))
|
||||||
|
plt.plot(x_values, y_values, marker='o', color='#4a90e2', linewidth=2)
|
||||||
|
|
||||||
|
# 反转 X 轴
|
||||||
|
if reverse_x:
|
||||||
|
plt.gca().invert_xaxis()
|
||||||
|
|
||||||
|
# 标题与标签
|
||||||
|
if title is None:
|
||||||
|
title = f"{y_field} 折线图"
|
||||||
|
|
||||||
|
plt.grid(True, linestyle='--', alpha=0.5)
|
||||||
|
plt.xticks(rotation=30)
|
||||||
|
plt.tight_layout()
|
||||||
|
|
||||||
|
# 转 Base64
|
||||||
|
buffer = BytesIO()
|
||||||
|
plt.savefig(buffer, format='png', dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||||
|
return f"data:image/png;base64,{img_base64}"
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
# core/jx3jiaoyihang.py
|
|
||||||
from astrbot.api import logger
|
|
||||||
from urllib.parse import quote
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from .class_reqsest import api_data_get, api_data_post
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_all_pages(base_url, initial_params, max_pages=None):
|
|
||||||
"""
|
|
||||||
获取所有分页数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_url: API基础URL
|
|
||||||
initial_params: 初始请求参数
|
|
||||||
max_pages: 最大页数限制(可选)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: 所有页面的数据列表
|
|
||||||
"""
|
|
||||||
all_data = []
|
|
||||||
current_page = 1
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# 设置当前页码
|
|
||||||
params = initial_params.copy()
|
|
||||||
params["page"] = str(current_page)
|
|
||||||
|
|
||||||
# 请求数据
|
|
||||||
data = api_data_get(base_url, params)
|
|
||||||
|
|
||||||
if not data or "list" not in data:
|
|
||||||
break
|
|
||||||
|
|
||||||
# 添加当前页数据到总列表
|
|
||||||
all_data.extend(data["list"])
|
|
||||||
|
|
||||||
# 检查是否还有更多页面
|
|
||||||
total_pages = data.get("pages", 1)
|
|
||||||
if current_page >= total_pages:
|
|
||||||
break
|
|
||||||
|
|
||||||
# 检查是否达到最大页数限制
|
|
||||||
if max_pages and current_page >= max_pages:
|
|
||||||
break
|
|
||||||
|
|
||||||
current_page += 1
|
|
||||||
|
|
||||||
return all_data
|
|
||||||
|
|
||||||
|
|
||||||
def merge_item_data_by_itemid(price_data, item_list_data):
|
|
||||||
"""
|
|
||||||
根据ItemId和id字段将物品名称信息合并到价格数据中
|
|
||||||
|
|
||||||
Args:
|
|
||||||
price_data: 第一组数据,包含价格信息的字典
|
|
||||||
item_list_data: 第二组数据,可以是字典(包含"list"键)或直接是物品列表
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 合并后的数据,包含价格和名称信息
|
|
||||||
"""
|
|
||||||
# 提取价格数据中的物品信息
|
|
||||||
price_items = price_data.get("data", {})
|
|
||||||
|
|
||||||
# 处理不同类型的item_list_data输入
|
|
||||||
if isinstance(item_list_data, dict) and "list" in item_list_data:
|
|
||||||
# 如果是字典且包含"list"键
|
|
||||||
item_list = item_list_data.get("list", [])
|
|
||||||
elif isinstance(item_list_data, list):
|
|
||||||
# 如果直接是列表
|
|
||||||
item_list = item_list_data
|
|
||||||
else:
|
|
||||||
# 其他情况,使用空列表
|
|
||||||
item_list = []
|
|
||||||
|
|
||||||
# 创建一个字典用于快速查找物品名称,使用id作为键
|
|
||||||
item_name_map = {}
|
|
||||||
for item in item_list:
|
|
||||||
item_id = item.get("id")
|
|
||||||
item_name = item.get("Name")
|
|
||||||
if item_id and item_name:
|
|
||||||
item_name_map[item_id] = item_name
|
|
||||||
|
|
||||||
# 创建一个新的结果字典
|
|
||||||
merged_data = {"code": price_data.get("code", 0), "msg": price_data.get("msg", ""), "data": {}}
|
|
||||||
|
|
||||||
# 遍历价格数据,添加名称信息
|
|
||||||
for item_id, price_info in price_items.items():
|
|
||||||
# 复制价格信息
|
|
||||||
merged_item = price_info.copy()
|
|
||||||
|
|
||||||
# 使用ItemId字段查找对应的名称
|
|
||||||
item_id_to_match = merged_item.get("ItemId")
|
|
||||||
if item_id_to_match and item_id_to_match in item_name_map:
|
|
||||||
merged_item["Name"] = item_name_map[item_id_to_match]
|
|
||||||
else:
|
|
||||||
merged_item["Name"] = "未知物品"
|
|
||||||
|
|
||||||
# 添加到结果中
|
|
||||||
merged_data["data"][item_id] = merged_item
|
|
||||||
|
|
||||||
return merged_data
|
|
||||||
|
|
||||||
|
|
||||||
#交易行数据查询函数
|
|
||||||
def jx3_data_jiaoyihang(inserver="眉间雪", inname="武技殊影图"):
|
|
||||||
"""
|
|
||||||
获取剑三交易行某区某物品价格数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
inserver: 第一组数据,服务器名称
|
|
||||||
inname: 物品名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: 合并后的数据,包含价格和名称信息
|
|
||||||
"""
|
|
||||||
# 第一步:获取所有物品列表数据(处理分页)
|
|
||||||
custom_url = f"https://node.jx3box.com/item_merged/name/{quote(inname)}"
|
|
||||||
initial_params = {
|
|
||||||
"client": "std",
|
|
||||||
"strict": "0",
|
|
||||||
"per": "50" # 每页数量,可以根据需要调整
|
|
||||||
}
|
|
||||||
|
|
||||||
# 获取所有页面的物品数据
|
|
||||||
all_items = fetch_all_pages(custom_url, initial_params)
|
|
||||||
|
|
||||||
if not all_items:
|
|
||||||
#logger.error(f"未找到物品: {inname}")
|
|
||||||
return "未找到改物品"
|
|
||||||
|
|
||||||
# 提取所有ID
|
|
||||||
ids = [item.get("id") for item in all_items if item.get("id")]
|
|
||||||
|
|
||||||
if not ids:
|
|
||||||
#logger.error(f"物品 {inname} 没有有效的ID")
|
|
||||||
return "未找到改物品"
|
|
||||||
|
|
||||||
# 返回格式化结果
|
|
||||||
stringids = ",".join(ids)
|
|
||||||
logger.info(f"搜索到物品: {inname}, 共找到 {len(ids)} 个物品\nIDs: {stringids}")
|
|
||||||
|
|
||||||
# 第二步:获取价格数据
|
|
||||||
price_url = "https://next2.jx3box.com/api/item-price/list"
|
|
||||||
price_params = {
|
|
||||||
"server": inserver,
|
|
||||||
"itemIds": stringids
|
|
||||||
}
|
|
||||||
|
|
||||||
price_data = api_data_get(price_url, price_params)
|
|
||||||
|
|
||||||
if not price_data or "data" not in price_data:
|
|
||||||
return "无交易行数据"
|
|
||||||
|
|
||||||
if not price_data["data"] or (isinstance(price_data["data"], dict) and not price_data["data"]) or (isinstance(price_data["data"], list) and not price_data["data"]):
|
|
||||||
return "无交易行数据"
|
|
||||||
|
|
||||||
# 第三步:合并数据
|
|
||||||
merged_data = merge_item_data_by_itemid(price_data, {"list": all_items})
|
|
||||||
|
|
||||||
# 处理合并后的数据
|
|
||||||
result_items = []
|
|
||||||
for item_id, item_info in merged_data.get("data", {}).items():
|
|
||||||
# 格式化价格(假设价格是以铜钱为单位,转换为金)
|
|
||||||
lowest_price = item_info.get("LowestPrice", 0)
|
|
||||||
bricks = lowest_price // 100000000 if lowest_price else 0
|
|
||||||
gold = (lowest_price % 100000000) // 10000 if lowest_price else 0
|
|
||||||
silver = (lowest_price % 10000) // 100 if lowest_price else 0
|
|
||||||
copper = lowest_price % 100 if lowest_price else 0
|
|
||||||
|
|
||||||
price_str = f"{bricks}砖{gold}金{silver}银{copper}铜" if lowest_price else "无价格"
|
|
||||||
|
|
||||||
result_items.append({
|
|
||||||
"name": item_info.get("Name", "未知物品"),
|
|
||||||
"price": price_str,
|
|
||||||
"avg_price": item_info.get("AvgPrice", 0),
|
|
||||||
"sample_size": item_info.get("SampleSize", 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
# 按价格排序
|
|
||||||
result_items.sort(key=lambda x: x["avg_price"])
|
|
||||||
|
|
||||||
return result_items
|
|
||||||
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
aiomysql
|
aiomysql
|
||||||
|
matplotlib
|
||||||
+100
-120
@@ -2,9 +2,13 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>剑网3交易行价格查询</title>
|
<title>剑网3各平台金价查询</title>
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: 'Microsoft YaHei', sans-serif;
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
@@ -33,7 +37,10 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
.server-info { font-size: 18px; opacity: 0.9; }
|
.server-info {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
.summary {
|
.summary {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
@@ -44,169 +51,142 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.items-count { font-weight: 600; color: #4a90e2; }
|
.items-count {
|
||||||
.update-time { color: #6c757d; font-size: 14px; }
|
font-weight: 600;
|
||||||
|
color: #4a90e2;
|
||||||
|
}
|
||||||
|
.update-time {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
.table-container {
|
.table-container {
|
||||||
padding: 0 30px 30px 30px;
|
padding: 0 30px 30px 30px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
text-align: center;
|
text-align: center
|
||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
width: 100%; border-collapse: collapse; margin-top: 20px;
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
border-radius: 8px; overflow: hidden;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||||
color: white; padding: 16px 20px; text-align: left;
|
color: white;
|
||||||
font-weight: 500; font-size: 16px; position: sticky; top: 0;
|
padding: 16px 20px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 20px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
th:first-child {
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
}
|
||||||
|
th:last-child {
|
||||||
|
border-top-right-radius: 8px;
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
padding: 14px 20px; border-bottom: 1px solid #e9ecef;
|
padding: 14px 20px;
|
||||||
font-size: 18px; color: #495057;
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background-color: #e9f7fe;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.item-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2c3e50;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
text-align: right;
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.sample {
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
tr:nth-child(even) { background-color: #f8f9fa; }
|
|
||||||
tr:hover { background-color: #e9f7fe; transition: background-color 0.2s; }
|
|
||||||
.item-name { font-weight: 500; color: #2c3e50; max-width: 500px; }
|
|
||||||
.price { text-align: right; color: #e74c3c; font-weight: 600; font-size: 16px; }
|
|
||||||
.footer {
|
.footer {
|
||||||
background-color: #343a40; color: #f8f9fa;
|
background-color: #343a40;
|
||||||
padding: 15px 30px; text-align: center; font-size: 14px;
|
color: #f8f9fa;
|
||||||
|
padding: 15px 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.item-icon {
|
.highlight {
|
||||||
width: 20px; height: 20px; margin-right: 8px;
|
background-color: #fff3cd;
|
||||||
vertical-align: middle; border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
.chart-container {
|
@media (max-width: 1320px) {
|
||||||
width: 100%; max-width: 1200px; margin: 30px auto;
|
body {
|
||||||
padding: 18px; background:#fff; border-radius:10px;
|
padding: 10px;
|
||||||
box-shadow: 0 6px 20px rgba(0,0,0,0.06);
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.chart-container h2 { font-size:18px; color:#243b55; margin-bottom: 15px; text-align:center; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Header -->
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>剑网3交易行价格查询</h1>
|
<h1>剑网3各平台金价查询</h1>
|
||||||
<div class="server-info">服务器: {{ server }}</div>
|
<div class="server-info">服务器: {{ server }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Summary -->
|
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<div class="items-count">共找到 <strong>{{ items|length }}</strong> 件物品</div>
|
<div class="items-count">共找到 <strong>{{ items|length }}</strong> 条数据</div>
|
||||||
<div class="update-time">数据更新时间: {{ update_time }}</div>
|
<div class="update-time">数据更新时间: {{ update_time }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table -->
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40%; text-align: center">物品名称</th>
|
<th style="width: 28%; text-align: center;">时间</th>
|
||||||
<th style="width: 25%; text-align: center;">低价</th>
|
<th style="width: 12%; text-align: center;">贴吧</th>
|
||||||
<th style="width: 15%; text-align: center;">数量</th>
|
<th style="width: 12%; text-align: center;">万宝楼</th>
|
||||||
<th style="width: 20%; text-align: center;">数据更新时间</th>
|
<th style="width: 12%; text-align: center;">DD373</th>
|
||||||
|
<th style="width: 12%; text-align: center;">UU898</th>
|
||||||
|
<th style="width: 12%; text-align: center;">5173</th>
|
||||||
|
<th style="width: 12%; text-align: center;">7881</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="item-name">
|
<td class="date">{{ item.date }}</td>
|
||||||
<img src="{{ item.IconID }}" alt="icon" class="item-icon">
|
<td class="priceTieba">{{ item.priceTieba }}</td>
|
||||||
{{ item.Name }}
|
<td class="priceWanbaolou">{{ item.priceWanbaolou }}</td>
|
||||||
</td>
|
<td class="priceDd373">{{ item.priceDd373 }}</td>
|
||||||
<td class="AvgPrice">{{ item.AvgPrice }}</td>
|
<td class="priceUu898">{{ item.priceUu898 }}</td>
|
||||||
<td class="SampleSize">{{ item.SampleSize }}</td>
|
<td class="price5173">{{ item.price5173 }}</td>
|
||||||
<td class="Date">{{ item.Date }}</td>
|
<td class="priceDd373">{{ item.price7881 }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== SVG 趋势图(只显示 wbl 数据) ====== -->
|
<!-- 这里是折线图 -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<h2>金价走势(wbl 数据)</h2>
|
<h2>万宝楼价格走势</h2>
|
||||||
|
<img src="{{ img }}" alt="折线图">
|
||||||
{% set width = 1100 %}
|
|
||||||
{% set height = 360 %}
|
|
||||||
{% set margin = 40 %}
|
|
||||||
{% set inner_w = width - margin*2 %}
|
|
||||||
{% set inner_h = height - margin*2 %}
|
|
||||||
|
|
||||||
{% if not wbl_dates or not wbl_prices %}
|
|
||||||
<div style="padding:30px; text-align:center; color:#6c757d;">无 wbl 数据可显示</div>
|
|
||||||
{% else %}
|
|
||||||
{% set n = wbl_prices | length %}
|
|
||||||
{% set min_price = wbl_min if wbl_min is defined else (wbl_prices | min) %}
|
|
||||||
{% set max_price = wbl_max if wbl_max is defined else (wbl_prices | max) %}
|
|
||||||
{% if min_price == max_price %}
|
|
||||||
{% set min_price = min_price - 1 %}
|
|
||||||
{% set max_price = max_price + 1 %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set points = [] %}
|
|
||||||
{% for i in range(n) %}
|
|
||||||
{% set x = margin + (inner_w * i) / (n - 1 if n>1 else 1) %}
|
|
||||||
{% set norm = (wbl_prices[i] - min_price) / (max_price - min_price) %}
|
|
||||||
{% set y = margin + (inner_h * (1 - norm)) %}
|
|
||||||
{% set _ = points.append("%0.2f,%0.2f" % (x, y)) %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<svg width="{{ width }}" height="{{ height }}" viewBox="0 0 {{ width }} {{ height }}">
|
|
||||||
<!-- 背景 -->
|
|
||||||
<rect x="0" y="0" width="{{ width }}" height="{{ height }}" rx="10" fill="#ffffff" stroke="rgba(0,0,0,0.04)"></rect>
|
|
||||||
|
|
||||||
<!-- 网格线 + y 轴标签 -->
|
|
||||||
{% set grid_lines = 5 %}
|
|
||||||
{% for gi in range(grid_lines) %}
|
|
||||||
{% set gy = margin + (inner_h * gi) / (grid_lines - 1 if grid_lines>1 else 1) %}
|
|
||||||
<line x1="{{ margin }}" y1="{{ gy }}" x2="{{ width - margin }}" y2="{{ gy }}" stroke="#eef4fb" stroke-width="1"></line>
|
|
||||||
{% set price_label = max_price - ( (max_price - min_price) * gi / (grid_lines - 1 if grid_lines>1 else 1) ) %}
|
|
||||||
<text x="8" y="{{ gy + 4 }}" font-size="12" fill="#6c757d">{{ "%0.0f"|format(price_label) }}</text>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- x 轴标签 -->
|
|
||||||
{% set label_step = (n // 8) if n > 8 else 1 %}
|
|
||||||
{% for i in range(n) %}
|
|
||||||
{% set x = margin + (inner_w * i) / (n - 1 if n>1 else 1) %}
|
|
||||||
{% if i % label_step == 0 or i == n-1 %}
|
|
||||||
<text x="{{ x }}" y="{{ height - 10 }}" font-size="11" fill="#6c757d" text-anchor="middle">
|
|
||||||
{{ wbl_dates[i] }}
|
|
||||||
</text>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- 渐变填充 -->
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="gFill" x1="0" x2="0" y1="0" y2="1">
|
|
||||||
<stop offset="0%" stop-color="#dcefff" stop-opacity="0.7" />
|
|
||||||
<stop offset="100%" stop-color="#dcefff" stop-opacity="0.05" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- 填充区域 -->
|
|
||||||
<polygon points="{{ points | join(' ') }} {{ width - margin }},{{ height - margin }} {{ margin }},{{ height - margin }}" fill="url(#gFill)" stroke="none"></polygon>
|
|
||||||
|
|
||||||
<!-- 折线 -->
|
|
||||||
<polyline fill="none" stroke="#4a90e2" stroke-width="2" points="{{ points | join(' ') }}" stroke-linejoin="round" stroke-linecap="round"></polyline>
|
|
||||||
|
|
||||||
<!-- 数据点 -->
|
|
||||||
{% for p in points %}
|
|
||||||
{% set coords = p.split(',') %}
|
|
||||||
<circle cx="{{ coords[0] }}" cy="{{ coords[1] }}" r="3.2" fill="#2a5298"></circle>
|
|
||||||
{% endfor %}
|
|
||||||
</svg>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<!-- ====== End 趋势图 ====== -->
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
© 2025 剑网3交易行查询系统 | 数据仅供参考,以游戏内实际价格为准
|
© 2025 剑网3金价查询系统 | 数据仅供参考,以游戏内实际价格为准
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user