新增全新商店界面 (#1816)

* 🎉 新增商店模板及样式配置

* 🎨 移除webui注释

* 🎨 新增神秘药水道具及商店样式调整

* 🎨 添加商店样式底部边距调整

---------

Co-authored-by: Flern <cb56ec362bbbfb5272eb941281299d8d-qq_connect@git.osc>
This commit is contained in:
HibiKier 2025-01-06 19:21:35 +08:00 committed by GitHub
parent ec70144d7e
commit 2a5c06702a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1119 additions and 214 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

View File

@ -0,0 +1,321 @@
@font-face {
font-family: fzrzFont;
/* 导入的字体文件 */
src: url("../../font/fzrzExtraBold.ttf");
}
@font-face {
font-family: msyhFont;
/* 导入的字体文件 */
src: url("../../font/msyh.ttf");
}
@font-face {
font-family: hywhFont;
/* 导入的字体文件 */
src: url("../../font/HYWenHei-85W.ttf");
}
@font-face {
font-family: syhtFont;
/* 导入的字体文件 */
src: url("../../font/syht.otf");
}
body {
position: absolute;
left: -8px;
top: -8px;
}
.wrapper {
width: 850px;
font-family: "hywhFont";
padding: 10px 0;
background-color: #fbe4e4;
box-sizing: border-box;
}
.top-title {
color: #e87692;
font-size: 85px;
text-align: center;
font-family: "fzrzFont";
margin-bottom: -30px;
}
.split {
background-image: url("./res/img/split.png");
background-repeat: no-repeat;
background-position: center;
height: 15px;
margin-top: 70px;
margin-bottom: 30px;
}
.top-head {
background-image: url("./res/img/head.png");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
height: 586px;
}
.top-aaaa {
font-family: 'syhtFont';
font-size: 34px;
text-align: center;
color: #E87692;
height: 50px;
}
.shop-border {
margin-bottom: 50px;
}
.shop-item {
padding-top: 100px;
box-sizing: border-box;
}
.shop-item-border {
display: flex;
position: relative;
}
.shop-item-title {
background-image: url("./res/img/title-bk.png");
background-size: contain;
background-repeat: no-repeat;
background-position: center;
height: 100px;
margin-bottom: 20px;
display: flex;
justify-content: center;
}
.shop-item-title-text {
color: white;
font-size: 30px;
margin-top: 13px;
}
.shop-item-left {
min-height: 460px;
position: relative;
width: 158px;
}
.shop-item-left-qq {
position: absolute;
height: 100px;
}
.left-img {
position: absolute;
left: 5px;
z-index: 10;
}
.shop-item-right {
width: 210px;
position: relative;
}
.shop-item-right-zx {
height: 460px;
position: absolute;
z-index: 3;
top: 10px;
}
.right-img {
position: absolute;
right: -2px;
z-index: 10;
}
.shop-item-mid {
width: 480px;
box-sizing: border-box;
padding-top: 20px;
position: relative;
}
.shop-item-mid-bk-inner {
width: 520px;
box-sizing: border-box;
padding-top: 30px;
background-color: #be5972;
border: 1px solid #b14b5f;
border-radius: 10px;
position: absolute;
right: -50px;
top: 10px;
z-index: 1;
height: calc(100% - 20px);
}
.shop-item-mid-bk-out {
width: 520px;
box-sizing: border-box;
background-color: #f096a8;
border: 1px solid #812528;
border-radius: 10px;
height: 100%;
z-index: 2;
position: relative;
padding: 30px;
}
.goods-item {
background-color: #f8cfd8;
width: 100%;
min-height: 130px;
border-radius: 10px;
padding: 10px;
display: flex;
position: relative;
border: 1px solid #994446;
margin-bottom: 10px;
}
.goods-id {
position: absolute;
color: white;
font-size: 15px;
border-top-left-radius: 10px;
top: 0;
left: 0;
border-right: 60px solid transparent;
border-bottom: 60px solid transparent;
border-top: 60px solid #ea7492;
}
.goods-id-text {
position: absolute;
top: -54px;
left: 9px;
color: white;
font-size: 16px;
font-family: "fzrzFont";
transform: rotate(-45deg);
}
.goods-item-left {
height: 100%;
min-height: 130px;
display: flex;
flex-direction: column;
justify-content: space-between;
padding-top: 30px;
box-sizing: border-box;
}
.goods-item-left-o {
height: 24px;
width: 20px;
background-color: #e99eab;
border-radius: 40%;
border: 1px solid #994446;
}
.goods-item-icon {
background-color: #fefefe;
border: 1px solid #994446;
margin-left: 20px;
width: 120px;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
}
.goods-item-icon-img {
height: 104px;
width: 104px;
}
.goods-item-right-des {
width: 272px;
min-height: 80px;
background-color: #fefefe;
border: 1px solid #994446;
margin-left: 10px;
border-radius: 10px;
padding: 5px;
font-family: "msyhFont";
}
.goods-item-right-price {
min-height: 30px;
background-color: #fefefe;
border-radius: 30px;
border: 1px solid #994446;
height: 20px;
margin-left: 10px;
margin-top: 5px;
font-size: 15px;
font-family: "msyhFont";
display: flex;
align-items: center;
padding-left: 10px;
position: relative;
}
.goods-item-right-btn {
min-height: 30px;
background-color: #bf9ac6;
color: white;
border-radius: 30px;
position: absolute;
right: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0 10px;
}
.goods-item-right-btn-line {
width: 1px;
height: 20px;
background-color: #994446;
margin: 0 10px;
}
.shop-item-mid-bag1 {
position: absolute;
width: 70px;
height: 78px;
bottom: -35px;
left: -35px;
z-index: 4;
}
.shop-item-mid-bag2 {
position: absolute;
width: 121px;
height: 89px;
right: -35px;
bottom: -35px;
z-index: 4;
}
.goods-item-name {
font-size: 18px;
font-family: "syhtFont";
}
.goods-item-name-line {
height: 2px;
width: 100%;
margin: 3px 0;
background-color: #731c1c;
border-radius: 10px;
}
.create-text {
position: absolute;
bottom: 1px;
right: 10px;
color: #97979c;
font-size: 15px;
}

View File

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test</title>
<!-- <link rel="stylesheet" href="./res/font-awesome/css/font-awesome.min.css"> -->
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="wrapper">
<div class="top-title">
{{name}}的神秘商店
</div>
<div class="top-aaaa">“使用 '购买道具 [道具ID/道具名称]' 购买道具”</div>
<div class="split" style="margin-top: 30px;"></div>
<div class="top-head"></div>
<div class="shop-border">
{% for data in data_list %}
<div class="shop-item">
<div class="shop-item-title">
<span class="shop-item-title-text">{{data.partition}}</span>
</div>
<div class="shop-item-border">
<div class="shop-item-left">
</div>
<div class="shop-item-mid">
<div class="shop-item-mid-bk-inner"></div>
<div class="shop-item-mid-bk-out">
{% for goods in data['goods_list'] %}
<div class="goods-item">
<div class="goods-id">
<div class="goods-id-text">{{goods.id}}</div>
</div>
<div class="goods-item-left">
<div class="goods-item-left-o"></div>
<div class="goods-item-left-o"></div>
<div class="goods-item-left-o"></div>
</div>
<div class="goods-item-icon">
<img src="{{goods.icon}}" class="goods-item-icon-img" />
</div>
<div class="goods-item-right">
<div class="goods-item-right-des">
<div class="goods-item-name">
{{goods.name}}
</div>
<div class="goods-item-name-line"></div>
{{goods.description}}
</div>
<div class="goods-item-right-price">
<span>{{goods.price}}金币</span>
<span class="goods-item-right-btn">
<span class="goods-item-right-btn-buy">立即购买</span>
<span class="goods-item-right-btn-line"></span>
<span class="goods-item-right-btn-limit">限购: {{goods.daily_limit}}</span>
</span>
</div>
</div>
</div>
{% endfor %}
</div>
<img src="./res/img/bag1.png" class="shop-item-mid-bag1" />
<img src="./res/img/bag2.png" class="shop-item-mid-bag2" />
</div>
<div class="shop-item-right">
</div>
</div>
<!-- <div class="shop-item-border">
<div class="shop-item-left">
<img src="./res/img/2.png" class="shop-item-right-zx" />
</div>
<div class="shop-item-mid">
<div class="shop-item-mid-bk-inner"></div>
<div class="shop-item-mid-bk-out">
<div class="goods-item">
<div class="goods-id">
<div class="goods-id-text">11</div>
</div>
<div class="goods-item-left">
<div class="goods-item-left-o"></div>
<div class="goods-item-left-o"></div>
<div class="goods-item-left-o"></div>
</div>
<div class="goods-item-icon">
<img src="./res/img/bag1.png" class="goods-item-icon-img" />
</div>
<div class="goods-item-right">
<div class="goods-item-right-des">
<div class="goods-item-name">
签到道具
</div>
<div class="goods-item-name-line"></div>
背包
</div>
<div class="goods-item-right-price">
<span>100金币</span>
<span class="goods-item-right-btn">
<span class="goods-item-right-btn-buy">立即购买</span>
<span class="goods-item-right-btn-line"></span>
<span class="goods-item-right-btn-limit">限购: 0</span>
</span>
</div>
</div>
</div>
</div>
<img src="./res/img/bag1.png" class="shop-item-mid-bag1" />
<img src="./res/img/bag2.png" class="shop-item-mid-bag2" />
</div>
<div class="shop-item-right">
<img src="./res/img/1.png" class="shop-item-right-zx" />
</div>
</div> -->
</div>
{% endfor %}
</div>
<div class="split"></div>
<div class="create-text">Create By ZhenXun</div>
</div>
</body>
<script type="text/javascript" src="main.js">
</script>
</html>

View File

@ -0,0 +1,183 @@
const leftElements = document.getElementsByClassName("shop-item-left")
const rightElements = document.getElementsByClassName("shop-item-right")
const defaultList = [
"1.png",
"2.png",
"3.png",
"4.png",
"5.png",
"qq.png",
"xx1.png",
"xx2.png",
]
const leftRightImgList = ["1.png", "2.png", "3.png", "4.png", "5.png"]
const leftRightImgList2 = ["qq.png", "xx1.png", "xx2.png"]
var leftRightImg = null
var leftRightImg2 = null
function randomImg() {
const randomIndex = Math.floor(Math.random() * leftRightImgList.length)
const randImg = leftRightImgList[randomIndex]
if (leftRightImg == randImg) {
return randomImg()
}
leftRightImg = randImg
return randImg
}
function randomImg2() {
const randomIndex = Math.floor(Math.random() * leftRightImgList2.length)
const randImg = leftRightImgList2[randomIndex]
if (leftRightImg == randImg) {
return randomImg2()
}
leftRightImg2 = randImg
return randImg
}
function getRandomInt(min, max) {
const mathMin = Math.ceil(min)
const mathMax = Math.floor(max)
return Math.floor(Math.random() * (mathMax - mathMin + 1)) + mathMin
}
function createImgElement(is_qq, is_left, start_height, height) {
const imgElement = document.createElement("img")
const className = is_left ? "left-img" : "right-img"
if (is_qq) {
imgElement.className = "shop-item-left-qq " + className
imgElement.src = "./res/img/left_right/" + randomImg2()
imgElement.style.top = getRandomInt(start_height, height - 20) + "px"
if (is_left) {
imgElement.style.left = getRandomInt(10, 40) + "px"
} else {
imgElement.style.right = getRandomInt(10, 40) + "px"
}
imgElement.style.height = getRandomInt(80, 120) + "px"
imgElement.style.transform = "rotate(" + getRandomInt(0, 45) + "deg)"
} else {
imgElement.className = "shop-item-right-zx " + className
imgElement.src = "./res/img/left_right/" + randomImg()
imgElement.style.top = getRandomInt(start_height, height - 20) + "px"
}
return imgElement
}
function getTop(dom) {
return parseInt(dom.style.top.slice(0, -2))
}
const randomIndex = Math.floor(Math.random() * defaultList.length)
const leftImg = defaultList[randomIndex]
var start = true
if (["qq.png", "xx1.png", "xx2.png"].includes(leftImg)) {
start = true
} else {
start = false
}
for (let i = 0; i < leftElements.length; i++) {
leftHeight = leftElements[i].offsetHeight
if (leftHeight <= 1000) {
// 长度不够,只增加一个
if (start) {
leftElements[i].appendChild(
createImgElement(true, true, 20, leftHeight - 50)
)
rightElements[i].appendChild(createImgElement(false, false, 10, 60))
} else {
leftElements[i].appendChild(createImgElement(false, true, 10, 60))
rightElements[i].appendChild(
createImgElement(true, false, 20, leftHeight - 50)
)
}
} else {
// 先添加一个气球
const firstDom = createImgElement(true, true, 20, 200)
leftElements[i].appendChild(firstDom)
let startHeight = 100 + getTop(firstDom)
let endHeight = 300 + getTop(firstDom)
let firstIsQq = false
let inx = 0
while (leftHeight - endHeight >= 200) {
// 避免过多重复
rand = Math.random()
if (inx >= 2) {
rand = 0.4
inx = 0
}
if (inx <= -1) {
rand = 0.6
inx = 0
}
// 真寻和气球随机加
if (rand > 0.5) {
firstIsQq = true
inx += 1
const imgDom = createImgElement(true, true, startHeight, endHeight)
leftElements[i].appendChild(imgDom)
startHeight = getRandomInt(250, 350) + getTop(imgDom)
endHeight = getRandomInt(450, 500) + getTop(imgDom)
} else {
if (leftHeight - startHeight < 700) {
continue
}
inx -= 1
const imgDom = createImgElement(false, true, startHeight, endHeight)
leftElements[i].appendChild(imgDom)
startHeight = getRandomInt(400, 700) + getTop(imgDom)
endHeight = getRandomInt(600, 900) + getTop(imgDom)
if (leftHeight - startHeight < 900) {
break
}
}
}
startHeight = 10
endHeight = 200
inx = 0
while (leftHeight - endHeight >= 200) {
// 真寻和气球随机加
rand = Math.random()
if (rand > 0.5 && firstIsQq) {
firstIsQq = false
rand = 0.4
}
// 避免过多重复
if (inx >= 2) {
rand = 0.4
inx = 0
}
if (inx <= -1) {
rand = 0.6
inx = 0
}
if (rand > 0.5) {
inx += 1
const imgDom = createImgElement(true, false, startHeight, endHeight)
rightElements[i].appendChild(imgDom)
startHeight = getRandomInt(250, 350) + getTop(imgDom)
endHeight = getRandomInt(450, 500) + getTop(imgDom)
} else {
if (leftHeight - startHeight < 700) {
continue
}
inx -= 1
const imgDom = createImgElement(false, false, startHeight, endHeight)
rightElements[i].appendChild(imgDom)
startHeight = getRandomInt(400, 700) + getTop(imgDom)
endHeight = getRandomInt(600, 900) + getTop(imgDom)
}
}
}
start = !start
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -16,7 +16,7 @@ from nonebot_plugin_alconna import (
)
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.utils import BaseBlock, PluginExtraData
from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.depends import UserName
from zhenxun.utils.enum import BlockType, PluginType
@ -25,6 +25,7 @@ from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
from ._data_source import ShopManage, gold_rank
from .goods_register import * # noqa: F403
__plugin_meta__ = PluginMetadata(
name="商店",
@ -45,6 +46,14 @@ __plugin_meta__ = PluginMetadata(
plugin_type=PluginType.NORMAL,
menu_type="商店",
limits=[BaseBlock(check_type=BlockType.GROUP)],
configs=[
RegisterConfig(
key="style",
value="zhenxun",
help="商店样式类型,[normal, zhenxun]",
default_value="zhenxun",
)
],
).dict(),
)
@ -108,7 +117,7 @@ _matcher.shortcut(
@_matcher.assign("$main")
async def _(session: Uninfo, arparma: Arparma):
image = await ShopManage.build_shop_image()
image = await ShopManage.get_shop_image()
logger.info("查看商店", arparma.header_result, session=session)
await MessageUtils.build_message(image).send()

View File

@ -11,7 +11,6 @@ from nonebot_plugin_alconna import UniMessage, UniMsg
from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel, create_model
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.models.friend_user import FriendUser
from zhenxun.models.goods_info import GoodsInfo
from zhenxun.models.group_member_info import GroupInfoUser
@ -20,19 +19,12 @@ from zhenxun.models.user_gold_log import UserGoldLog
from zhenxun.models.user_props_log import UserPropsLog
from zhenxun.services.log import logger
from zhenxun.utils.enum import GoldHandle, PropHandle
from zhenxun.utils.image_utils import BuildImage, ImageTemplate, text2image
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
from zhenxun.utils.platform import PlatformUtils
ICON_PATH = IMAGE_PATH / "shop_icon"
RANK_ICON_PATH = IMAGE_PATH / "_icon"
PLATFORM_PATH = {
"dodo": RANK_ICON_PATH / "dodo.png",
"discord": RANK_ICON_PATH / "discord.png",
"kaiheila": RANK_ICON_PATH / "kook.png",
"qq": RANK_ICON_PATH / "qq.png",
}
from .config import ICON_PATH, PLATFORM_PATH, base_config
from .html_image import html_image
from .normal_image import normal_image
class Goods(BaseModel):
@ -138,6 +130,12 @@ async def gold_rank(
class ShopManage:
uuid2goods: dict[str, Goods] = {} # noqa: RUF012
@classmethod
async def get_shop_image(cls) -> bytes:
if base_config.get("style") == "zhenxun":
return await html_image()
return await normal_image()
@classmethod
def __build_params(
cls,
@ -492,200 +490,3 @@ class ShopManage:
"""
user = await UserConsole.get_user(user_id, platform)
return user.gold
@classmethod
async def build_shop_image(cls) -> BuildImage:
"""制作商店图片
返回:
BuildImage: 商店图片
"""
goods_lst = await GoodsInfo.get_all_goods()
h = 10
_list: list[GoodsInfo] = [
goods
for goods in goods_lst
if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time
]
# A = BuildImage(1100, h, color="#f9f6f2")
total_n = 0
image_list = []
for idx, goods in enumerate(_list):
name_image = BuildImage(
580, 40, font_size=25, color="#e67b6b", font="CJGaoDeGuo.otf"
)
await name_image.text(
(15, 0), f"{idx + 1}.{goods.goods_name}", center_type="height"
)
await name_image.line((380, -5, 280, 45), "#a29ad6", 5)
await name_image.text((390, 0), "售价:", center_type="height")
if goods.goods_discount != 1:
discount_price = int(goods.goods_discount * goods.goods_price)
old_price_image = await BuildImage.build_text_image(
str(goods.goods_price), font_color=(194, 194, 194), size=15
)
await old_price_image.line(
(
0,
int(old_price_image.height / 2),
old_price_image.width + 1,
int(old_price_image.height / 2),
),
(0, 0, 0),
)
await name_image.paste(old_price_image, (440, 0))
await name_image.text((440, 15), str(discount_price), (255, 255, 255))
else:
await name_image.text(
(440, 0),
str(goods.goods_price),
(255, 255, 255),
center_type="height",
)
_tmp = await BuildImage.build_text_image(str(goods.goods_price), size=25)
await name_image.text(
(
440 + _tmp.width,
0,
),
" 金币",
center_type="height",
)
des_image = None
font_img = BuildImage(600, 80, font_size=20, color="#a29ad6")
p = font_img.getsize("简介:")[0] + 20
if goods.goods_description:
des_list = goods.goods_description.split("\n")
desc = ""
for des in des_list:
if font_img.getsize(des)[0] > font_img.width - p - 20:
msg = ""
tmp = ""
for i in range(len(des)):
if font_img.getsize(tmp)[0] < font_img.width - p - 20:
tmp += des[i]
else:
msg += tmp + "\n"
tmp = des[i]
desc += msg
if tmp:
desc += tmp
else:
desc += des + "\n"
if desc[-1] == "\n":
desc = desc[:-1]
des_image = await text2image(desc, color="#a29ad6")
goods_image = BuildImage(
600,
(50 + des_image.height) if des_image else 50,
font_size=20,
color="#a29ad6",
font="CJGaoDeGuo.otf",
)
if des_image:
await goods_image.text((15, 50), "简介:")
await goods_image.paste(des_image, (p, 50))
await name_image.circle_corner(5)
await goods_image.paste(name_image, (0, 5), center_type="width")
await goods_image.circle_corner(20)
bk = BuildImage(
1180,
(50 + des_image.height) if des_image else 50,
font_size=15,
color="#f9f6f2",
font="CJGaoDeGuo.otf",
)
if goods.icon and (ICON_PATH / goods.icon).exists():
icon = BuildImage(70, 70, background=ICON_PATH / goods.icon)
await bk.paste(icon)
await bk.paste(goods_image, (70, 0))
n = 0
_w = 650
# 添加限时图标和时间
if goods.goods_limit_time > 0:
n += 140
_limit_time_logo = BuildImage(
40, 40, background=f"{IMAGE_PATH}/other/time.png"
)
await bk.paste(_limit_time_logo, (_w + 50, 0))
_time_img = await BuildImage.build_text_image("限时!", size=23)
await bk.paste(
_time_img,
(_w + 90, 10),
)
limit_time = time.strftime(
"%Y-%m-%d %H:%M", time.localtime(goods.goods_limit_time)
).split()
y_m_d = limit_time[0]
_h_m = limit_time[1].split(":")
h_m = f"{_h_m[0]}{_h_m[1]}"
await bk.text((_w + 55, 38), str(y_m_d))
await bk.text((_w + 65, 57), str(h_m))
_w += 140
if goods.goods_discount != 1:
n += 140
_discount_logo = BuildImage(
30, 30, background=f"{IMAGE_PATH}/other/discount.png"
)
await bk.paste(_discount_logo, (_w + 50, 10))
_tmp = await BuildImage.build_text_image("折扣!", size=23)
await bk.paste(_tmp, (_w + 90, 15))
_tmp = await BuildImage.build_text_image(
f"{10 * goods.goods_discount:.1f}",
size=30,
font_color=(85, 156, 75),
)
await bk.paste(_tmp, (_w + 50, 44))
_w += 140
if goods.daily_limit != 0:
n += 140
_daily_limit_logo = BuildImage(
35, 35, background=f"{IMAGE_PATH}/other/daily_limit.png"
)
await bk.paste(_daily_limit_logo, (_w + 50, 10))
_tmp = await BuildImage.build_text_image(
"限购!",
size=23,
)
await bk.paste(_tmp, (_w + 90, 20))
_tmp = await BuildImage.build_text_image(
f"{goods.daily_limit}", size=30
)
await bk.paste(_tmp, (_w + 72, 45))
total_n = max(total_n, n)
if n:
await bk.line((650, -1, 650 + n, -1), "#a29ad6", 5)
# await bk.aline((650, 80, 650 + n, 80), "#a29ad6", 5)
# 添加限时图标和时间
image_list.append(bk)
# await A.apaste(bk, (0, current_h), True)
# current_h += 90
current_h = 0
h = sum(img.height + 10 for img in image_list) or 400
A = BuildImage(1100, h, color="#f9f6f2")
for img in image_list:
await A.paste(img, (0, current_h))
current_h += img.height + 10
w = 950
if total_n:
w += total_n
h = A.height + 230 + 100
h = max(h, 1000)
shop_logo = BuildImage(100, 100, background=f"{IMAGE_PATH}/other/shop_text.png")
shop = BuildImage(w, h, font_size=20, color="#f9f6f2")
await shop.paste(A, (20, 230))
await shop.paste(shop_logo, (450, 30))
tip = "注【通过 购买道具 序号 或者 商品名称 购买】"
await shop.text(
(
int((1000 - shop.getsize(tip)[0]) / 2),
170,
),
"注【通过 序号 或者 商品名称 购买】",
)
await shop.text(
(20, h - 100),
"神秘药水\t\t售价9999999金币\n\t\t鬼知道会有什么效果~",
)
return shop

View File

@ -0,0 +1,20 @@
from zhenxun.configs.config import Config
from zhenxun.configs.path_config import IMAGE_PATH, TEMPLATE_PATH
base_config = Config.get("shop")
ICON_PATH = IMAGE_PATH / "shop_icon"
RANK_ICON_PATH = IMAGE_PATH / "_icon"
PLATFORM_PATH = {
"dodo": RANK_ICON_PATH / "dodo.png",
"discord": RANK_ICON_PATH / "discord.png",
"kaiheila": RANK_ICON_PATH / "kook.png",
"qq": RANK_ICON_PATH / "qq.png",
}
LEFT_RIGHT_IMAGE = ["1.png", "2.png", "qq.png"]
LEFT_RIGHT_PATH = TEMPLATE_PATH / "shop" / "res" / "img"

View File

@ -0,0 +1,161 @@
from zhenxun.models.user_console import UserConsole
from zhenxun.utils.decorator.shop import shop_register
@shop_register(
name="神秘药水",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="小秘密",
icon="mysterious_potion.png",
)
async def _(user_id: str):
await UserConsole.add_gold(
user_id,
1000000,
"shop",
)
return "使用道具神秘药水成功!你滴金币+1000000"
@shop_register(
name="测试道具A1",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A2",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A3",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A4",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A5",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A6",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A7",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A8",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A9",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A10",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A11",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A12",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass
@shop_register(
name="测试道具A13",
price=999999,
des="鬼知道会有什么效果,要不试试?",
partition="TEST",
icon="mysterious_potion.png",
)
async def _(user_id: str):
pass

View File

@ -0,0 +1,63 @@
import time
from nonebot_plugin_htmlrender import template_to_pic
from pydantic import BaseModel
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import TEMPLATE_PATH
from zhenxun.models.goods_info import GoodsInfo
from zhenxun.utils._build_image import BuildImage
from .config import ICON_PATH
class GoodsItem(BaseModel):
goods_list: list[dict]
"""商品列表"""
partition: str
"""分区名称"""
async def html_image() -> bytes:
"""构建图片"""
goods_list: list[tuple[int, GoodsInfo]] = [
(i + 1, goods)
for i, goods in enumerate(await GoodsInfo.get_all_goods())
if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time
]
partition_dict: dict[str, list[dict]] = {}
for goods in goods_list:
if not goods[1].partition:
goods[1].partition = "默认分区"
if goods[1].partition not in partition_dict:
partition_dict[goods[1].partition] = []
partition_dict[goods[1].partition].append(
{
"id": goods[0],
"price": goods[1].goods_price,
"daily_limit": goods[1].daily_limit or "",
"name": goods[1].goods_name,
"icon": "data:image/png;base64,"
+ BuildImage.open(ICON_PATH / goods[1].icon).pic2bs4()[9:],
"description": goods[1].goods_description,
}
)
data_list = []
for partition in partition_dict:
data_list.append(
GoodsItem(
goods_list=partition_dict[partition],
partition=partition,
)
)
return await template_to_pic(
template_path=str((TEMPLATE_PATH / "shop").absolute()),
template_name="main.html",
templates={"name": BotConfig.self_nickname, "data_list": data_list},
pages={
"viewport": {"width": 850, "height": 1024},
"base_url": f"file://{TEMPLATE_PATH}",
},
wait=2,
)

View File

@ -0,0 +1,203 @@
import time
from zhenxun.configs.path_config import IMAGE_PATH
from zhenxun.models.goods_info import GoodsInfo
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.image_utils import text2image
from .config import ICON_PATH
async def normal_image() -> bytes:
"""制作商店图片
返回:
BuildImage: 商店图片
"""
goods_lst = await GoodsInfo.get_all_goods()
h = 10
_list: list[GoodsInfo] = [
goods
for goods in goods_lst
if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time
]
# A = BuildImage(1100, h, color="#f9f6f2")
total_n = 0
image_list = []
for idx, goods in enumerate(_list):
name_image = BuildImage(
580, 40, font_size=25, color="#e67b6b", font="CJGaoDeGuo.otf"
)
await name_image.text(
(15, 0), f"{idx + 1}.{goods.goods_name}", center_type="height"
)
await name_image.line((380, -5, 280, 45), "#a29ad6", 5)
await name_image.text((390, 0), "售价:", center_type="height")
if goods.goods_discount != 1:
discount_price = int(goods.goods_discount * goods.goods_price)
old_price_image = await BuildImage.build_text_image(
str(goods.goods_price), font_color=(194, 194, 194), size=15
)
await old_price_image.line(
(
0,
int(old_price_image.height / 2),
old_price_image.width + 1,
int(old_price_image.height / 2),
),
(0, 0, 0),
)
await name_image.paste(old_price_image, (440, 0))
await name_image.text((440, 15), str(discount_price), (255, 255, 255))
else:
await name_image.text(
(440, 0),
str(goods.goods_price),
(255, 255, 255),
center_type="height",
)
_tmp = await BuildImage.build_text_image(str(goods.goods_price), size=25)
await name_image.text(
(
440 + _tmp.width,
0,
),
" 金币",
center_type="height",
)
des_image = None
font_img = BuildImage(600, 80, font_size=20, color="#a29ad6")
p = font_img.getsize("简介:")[0] + 20
if goods.goods_description:
des_list = goods.goods_description.split("\n")
desc = ""
for des in des_list:
if font_img.getsize(des)[0] > font_img.width - p - 20:
msg = ""
tmp = ""
for i in range(len(des)):
if font_img.getsize(tmp)[0] < font_img.width - p - 20:
tmp += des[i]
else:
msg += tmp + "\n"
tmp = des[i]
desc += msg
if tmp:
desc += tmp
else:
desc += des + "\n"
if desc[-1] == "\n":
desc = desc[:-1]
des_image = await text2image(desc, color="#a29ad6")
goods_image = BuildImage(
600,
(50 + des_image.height) if des_image else 50,
font_size=20,
color="#a29ad6",
font="CJGaoDeGuo.otf",
)
if des_image:
await goods_image.text((15, 50), "简介:")
await goods_image.paste(des_image, (p, 50))
await name_image.circle_corner(5)
await goods_image.paste(name_image, (0, 5), center_type="width")
await goods_image.circle_corner(20)
bk = BuildImage(
1180,
(50 + des_image.height) if des_image else 50,
font_size=15,
color="#f9f6f2",
font="CJGaoDeGuo.otf",
)
if goods.icon and (ICON_PATH / goods.icon).exists():
icon = BuildImage(70, 70, background=ICON_PATH / goods.icon)
await bk.paste(icon)
await bk.paste(goods_image, (70, 0))
n = 0
_w = 650
# 添加限时图标和时间
if goods.goods_limit_time > 0:
n += 140
_limit_time_logo = BuildImage(
40, 40, background=f"{IMAGE_PATH}/other/time.png"
)
await bk.paste(_limit_time_logo, (_w + 50, 0))
_time_img = await BuildImage.build_text_image("限时!", size=23)
await bk.paste(
_time_img,
(_w + 90, 10),
)
limit_time = time.strftime(
"%Y-%m-%d %H:%M", time.localtime(goods.goods_limit_time)
).split()
y_m_d = limit_time[0]
_h_m = limit_time[1].split(":")
h_m = f"{_h_m[0]}{_h_m[1]}"
await bk.text((_w + 55, 38), str(y_m_d))
await bk.text((_w + 65, 57), str(h_m))
_w += 140
if goods.goods_discount != 1:
n += 140
_discount_logo = BuildImage(
30, 30, background=f"{IMAGE_PATH}/other/discount.png"
)
await bk.paste(_discount_logo, (_w + 50, 10))
_tmp = await BuildImage.build_text_image("折扣!", size=23)
await bk.paste(_tmp, (_w + 90, 15))
_tmp = await BuildImage.build_text_image(
f"{10 * goods.goods_discount:.1f}",
size=30,
font_color=(85, 156, 75),
)
await bk.paste(_tmp, (_w + 50, 44))
_w += 140
if goods.daily_limit != 0:
n += 140
_daily_limit_logo = BuildImage(
35, 35, background=f"{IMAGE_PATH}/other/daily_limit.png"
)
await bk.paste(_daily_limit_logo, (_w + 50, 10))
_tmp = await BuildImage.build_text_image(
"限购!",
size=23,
)
await bk.paste(_tmp, (_w + 90, 20))
_tmp = await BuildImage.build_text_image(f"{goods.daily_limit}", size=30)
await bk.paste(_tmp, (_w + 72, 45))
total_n = max(total_n, n)
if n:
await bk.line((650, -1, 650 + n, -1), "#a29ad6", 5)
# await bk.aline((650, 80, 650 + n, 80), "#a29ad6", 5)
# 添加限时图标和时间
image_list.append(bk)
# await A.apaste(bk, (0, current_h), True)
# current_h += 90
current_h = 0
h = sum(img.height + 10 for img in image_list) or 400
A = BuildImage(1100, h, color="#f9f6f2")
for img in image_list:
await A.paste(img, (0, current_h))
current_h += img.height + 10
w = 950
if total_n:
w += total_n
h = A.height + 230 + 100
h = max(h, 1000)
shop_logo = BuildImage(100, 100, background=f"{IMAGE_PATH}/other/shop_text.png")
shop = BuildImage(w, h, font_size=20, color="#f9f6f2")
await shop.paste(A, (20, 230))
await shop.paste(shop_logo, (450, 30))
tip = "注【通过 购买道具 序号 或者 商品名称 购买】"
await shop.text(
(
int((1000 - shop.getsize(tip)[0]) / 2),
170,
),
"注【通过 序号 或者 商品名称 购买】",
)
await shop.text(
(20, h - 100),
"神秘药水\t\t售价9999999金币\n\t\t鬼知道会有什么效果~",
)
return shop.pic2bytes()

View File

@ -27,10 +27,12 @@ class GoodsInfo(Model):
"""每日限购"""
is_passive = fields.BooleanField(default=False)
"""是否为被动道具"""
partition = fields.CharField(255, null=True)
"""分区名称"""
icon = fields.TextField(null=True)
"""图标路径"""
class Meta:
class Meta: # type: ignore
table = "goods_info"
table_description = "商品数据表"
@ -44,6 +46,7 @@ class GoodsInfo(Model):
goods_limit_time: int = 0,
daily_limit: int = 0,
is_passive: bool = False,
partition: str | None = None,
icon: str | None = None,
) -> str:
"""添加商品
@ -56,6 +59,7 @@ class GoodsInfo(Model):
goods_limit_time: 商品限时
daily_limit: 每日购买限制
is_passive: 是否为被动道具
partition: 分区名称
icon: 图标
"""
if not await cls.exists(goods_name=goods_name):
@ -69,6 +73,7 @@ class GoodsInfo(Model):
goods_limit_time=goods_limit_time,
daily_limit=daily_limit,
is_passive=is_passive,
partition=partition,
icon=icon,
)
return str(uuid_)
@ -159,4 +164,5 @@ class GoodsInfo(Model):
"ALTER TABLE goods_info ADD icon VARCHAR(255);",
# 删除 daily_purchase_limit 字段
"ALTER TABLE goods_info DROP daily_purchase_limit;",
"ALTER TABLE goods_info ADD partition VARCHAR(255);",
]

View File

@ -17,6 +17,7 @@ class Goods(BaseModel):
daily_limit: int
icon: str | None = None
is_passive: bool
partition: str | None
func: Callable
kwargs: dict[str, str] = {}
send_success_msg: bool
@ -73,6 +74,7 @@ class ShopRegister(dict):
load_status: tuple[bool, ...],
daily_limit: tuple[int, ...],
is_passive: tuple[bool, ...],
partition: tuple[str, ...],
icon: tuple[str, ...],
send_success_msg: tuple[bool, ...],
max_num_limit: tuple[int, ...],
@ -89,6 +91,7 @@ class ShopRegister(dict):
load_status: 是否加载
daily_limit: 每日限购
is_passive: 是否被动道具
partition: 分区名称
icon: 图标
send_success_msg: 成功时发送消息
max_num_limit: 单次最大使用次数
@ -97,7 +100,7 @@ class ShopRegister(dict):
def add_register_item(func: Callable):
if name in self._data.keys():
raise ValueError("该商品已注册,请替换其他名称!")
for n, p, d, dd, lmt, s, dl, pa, i, ssm, mnl in zip(
for n, p, d, dd, lmt, s, dl, pa, par, i, ssm, mnl in zip(
name,
price,
des,
@ -106,6 +109,7 @@ class ShopRegister(dict):
load_status,
daily_limit,
is_passive,
partition,
icon,
send_success_msg,
max_num_limit,
@ -124,6 +128,7 @@ class ShopRegister(dict):
limit_time=lmt,
daily_limit=dl,
is_passive=pa,
partition=par,
func=func,
send_success_msg=ssm,
max_num_limit=mnl,
@ -135,6 +140,7 @@ class ShopRegister(dict):
goods.daily_limit = dl
goods.icon = i
goods.is_passive = pa
goods.partition = par
goods.func = func
goods.kwargs = _temp_kwargs
goods.send_success_msg = ssm
@ -162,6 +168,7 @@ class ShopRegister(dict):
goods.limit_time,
goods.daily_limit,
goods.is_passive,
goods.partition,
goods.icon,
)
if uuid:
@ -186,6 +193,7 @@ class ShopRegister(dict):
load_status: bool | tuple[bool, ...] = True,
daily_limit: int | tuple[int, ...] = 0,
is_passive: bool | tuple[bool, ...] = False,
partition: str | tuple[str, ...] | None = None,
icon: str | tuple[str, ...] = "",
send_success_msg: bool | tuple[bool, ...] = True,
max_num_limit: int | tuple[int, ...] = 1,
@ -202,11 +210,11 @@ class ShopRegister(dict):
load_status: 是否加载
daily_limit: 每日限购
is_passive: 是否被动道具
partition: 分区名称
icon: 图标
send_success_msg: 成功时发送消息
max_num_limit: 单次最大使用次数
"""
_tuple_list = []
_current_len = -1
for x in [name, price, des, discount, limit_time, load_status]:
if isinstance(x, tuple):
@ -226,6 +234,7 @@ class ShopRegister(dict):
_load_status = self.__get(load_status, _current_len)
_daily_limit = self.__get(daily_limit, _current_len)
_is_passive = self.__get(is_passive, _current_len)
_partition = self.__get(partition, _current_len)
_icon = self.__get(icon, _current_len)
_send_success_msg = self.__get(send_success_msg, _current_len)
_max_num_limit = self.__get(max_num_limit, _current_len)
@ -238,6 +247,7 @@ class ShopRegister(dict):
_load_status,
_daily_limit,
_is_passive,
_partition,
_icon,
_send_success_msg,
_max_num_limit,