插件化目录初版提交

This commit is contained in:
yoimiya-kokomi 2022-03-26 16:21:44 +08:00
parent 620a0373ee
commit 7540e6b433
353 changed files with 1961 additions and 69 deletions

12
.idea/miao-plugin.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/miao-plugin.iml" filepath="$PROJECT_DIR$/.idea/miao-plugin.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

52
.idea/workspace.xml Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="8b7e4a51-6647-4b82-a102-b4b894d7928f" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/apps/character.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/character/character.css" beforeDir="false" afterPath="$PROJECT_DIR$/resources/character/character.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/character/character.html" beforeDir="false" afterPath="$PROJECT_DIR$/resources/character/character.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/character/character.js" beforeDir="false" afterPath="$PROJECT_DIR$/config.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/character/h.jpg" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/apps/character/w.jpg" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/index.js" beforeDir="false" afterPath="$PROJECT_DIR$/index.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/miao-plugin.js" beforeDir="false" afterPath="$PROJECT_DIR$/miao-plugin.js" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectId" id="26q2WrgqGMhiO9lAOk0fE9BWvtL" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="vue.rearranger.settings.migration" value="true" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="8b7e4a51-6647-4b82-a102-b4b894d7928f" name="Changes" comment="" />
<created>1648136859374</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1648136859374</updated>
<workItem from="1648136860539" duration="1102000" />
<workItem from="1648140745824" duration="69000" />
<workItem from="1648140890688" duration="1838000" />
<workItem from="1648224805226" duration="302000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

525
apps/character.js Normal file
View File

@ -0,0 +1,525 @@
import fetch from "node-fetch";
import { segment } from "oicq";
import lodash from "lodash";
import fs from "fs";
let getUrl, getServer;
import { Character } from "../components/models.js";
//角色昵称
let nameID = "";
let genshin = {};
await init();
export async function init(isUpdate = false) {
let _path = "file://" + process.cwd();
console.log(_path + "config/gen");
let version = isUpdate ? new Date().getTime() : 0;
genshin = await import(_path + `/config/genshin/roleId.js?version=${version}`);
nameID = "";
}
//#神里
export async function character(e, { render, MysApi }) {
let roleId = roleIdToName(e.msg.replace(/#|老婆|老公|[1|2|5][0-9]{8}/g, "").trim());
let hutao = Character.get("胡桃");
console.log(hutao.a)
return true;
if (!roleId) return false;
getUrl = MysApi.getUrl;
getServer = MysApi.getServer;
let uidRes = await getUid(e);
if (!uidRes.uid && uidRes.isSelf) {
e.reply("请先发送#+你游戏的uid");
return true;
}
if (!(await limitGet(e))) return true;
let uid = uidRes.uid;
let res = await mysApi(e, uid, "character", {
role_id: uid,
server: getServer(uid),
});
if (res.retcode == "-1") {
return true;
}
if (checkRetcode(res, uid, e)) {
return true;
}
let avatars = res.data.avatars;
let length = avatars.length;
avatars = lodash.keyBy(avatars, "id");
if (roleId == 20000000) {
if (avatars["10000005"]) {
roleId = "10000005";
}
if (avatars["10000007"]) {
roleId = "10000007";
}
}
if (!avatars[roleId]) {
let name = lodash.truncate(e.sender.card, { length: 8 });
if (length > 8) {
e.reply([segment.at(e.user_id, name), `\n没有${e.msg}`]);
} else {
e.reply([segment.at(e.user_id, name), "\n请先在米游社展示该角色"]);
}
return true;
}
limitSet(e);
avatars = avatars[roleId];
let skill = await getSkill(e, uid, avatars);
let type = "character";
let base64 = await render("miao-plugin", type, {
_plugin: true,
save_id: uid,
uid: uid,
skill,
...get_character(avatars),
}, "png");
if (base64) {
e.reply(segment.image(`base64://${base64}`));
}
return true; //事件结束不再往下
}
//获取角色技能数据
async function getSkill(e, uid, avatars) {
let skill = {};
if (NoteCookie && NoteCookie[e.user_id] && NoteCookie[e.user_id].uid == uid && NoteCookie[e.user_id].cookie.includes("cookie_token")) {
let skillres = await mysApi(e, uid, "detail", {
role_id: uid,
server: getServer(uid),
avatar_id: avatars.id,
});
if (skillres.retcode == 0 && skillres.data && skillres.data.skill_list) {
skill.id = avatars.id;
let skill_list = lodash.orderBy(skillres.data.skill_list, ["id"], ["asc"]);
for (let val of skill_list) {
val.level_original = val.level_current;
if (val.name.includes("普通攻击")) {
skill.a = val;
continue;
}
if (val.max_level >= 10 && !skill.e) {
skill.e = val;
continue;
}
if (val.max_level >= 10 && !skill.q) {
skill.q = val;
continue;
}
}
if (avatars.actived_constellation_num >= 3) {
if (avatars.constellations[2].effect.includes(skill.e.name)) {
skill.e.level_current += 3;
} else if (avatars.constellations[2].effect.includes(skill.q.name)) {
skill.q.level_current += 3;
}
}
if (avatars.actived_constellation_num >= 5) {
if (avatars.constellations[4].effect.includes(skill.e.name)) {
skill.e.level_current += 3;
} else if (avatars.constellations[4].effect.includes(skill.q.name)) {
skill.q.level_current += 3;
}
}
}
}
return skill;
}
function get_character(avatars) {
let list = [];
let set = {};
let setArr = [];
let text1 = "";
let text2 = "";
let bg = 2;
let weapon = {
type: "weapon",
name: avatars.weapon.name,
showName: genshin.abbr[avatars.weapon.name] ? genshin.abbr[avatars.weapon.name] : avatars.weapon.name,
level: avatars.weapon.level,
affix_level: avatars.weapon.affix_level,
};
for (let val of avatars.reliquaries) {
if (set[val.set.name]) {
set[val.set.name]++;
if (set[val.set.name] == 2) {
if (text1) {
text2 = "2件套" + val.set.affixes[0].effect;
} else {
text1 = "2件套" + val.set.affixes[0].effect;
}
}
if (set[val.set.name] == 4) {
text2 = "4件套" + val.set.name;
}
} else {
set[val.set.name] = 1;
}
list.push({
type: "reliquaries",
name: val.name,
level: val.level,
});
}
for (let val of Object.keys(set)) {
setArr.push({
name: val,
num: set[val],
showName: genshin.abbr[val] ? genshin.abbr[val] : val,
});
}
if (avatars.reliquaries.length >= 2 && !text1) {
text1 = "无套装效果";
}
if (avatars.id == "10000005") {
avatars.name = "空";
} else if (avatars.id == "10000007") {
avatars.name = "荧";
}
let reliquaries = list[0];
return {
name: avatars.name,
showName: genshin.abbr[avatars.name] ? genshin.abbr[avatars.name] : avatars.name,
level: avatars.level,
fetter: avatars.fetter,
actived_constellation_num: avatars.actived_constellation_num,
weapon,
text1,
text2,
bg,
reliquaries,
set: setArr,
};
}
//获取uid
async function getUid(e) {
let res;
let reg = /[1|2|5][0-9]{8}/g;
//从消息中获取
if (e.msg) {
res = e.msg.match(reg);
if (res) {
//redis保存uid
redis.set(`genshin:uid:${e.user_id}`, res[0], { EX: 2592000 });
return { isSelf: false, uid: res[0] };
}
}
//从群昵称获取
res = e.sender.card.toString().match(reg);
if (res) {
//redis保存uid
redis.set(`genshin:uid:${e.user_id}`, res[0], { EX: 2592000 });
return { isSelf: true, uid: res[0] };
}
//从redis获取
res = await redis.get(`genshin:uid:${e.user_id}`);
if (res) {
redis.expire(`genshin:uid:${e.user_id}`, 2592000);
return { isSelf: true, uid: res };
}
return { isSelf: true, uid: false };
}
async function mysApi(e, uid, type, data = {}) {
if (BotConfig.mysCookies.length <= 0) {
Bot.logger.error("请打开config.js,配置米游社cookie");
return { retcode: -300 };
}
let dayEnd = getDayEnd();
let cookie, index, isNew;
let selfCookie = NoteCookie[e.user_id];
//私聊发送的cookie
if (selfCookie && selfCookie.uid == uid) {
cookie = selfCookie.cookie;
}
//配置里面的cookie
else if (BotConfig.dailyNote && BotConfig.dailyNote[e.user_id] && BotConfig.dailyNote[e.user_id].uid == uid) {
cookie = BotConfig.dailyNote[e.user_id].cookie;
} else {
//获取uid集合
let uid_arr = await redis.get(`genshin:ds:qq:${e.user_id}`);
if (uid_arr) {
uid_arr = JSON.parse(uid_arr);
if (!uid_arr.includes(uid)) {
uid_arr.push(uid);
await redis.set(`genshin:ds:qq:${e.user_id}`, JSON.stringify(uid_arr), {
EX: dayEnd,
});
}
} else {
uid_arr = [uid];
await redis.set(`genshin:ds:qq:${e.user_id}`, JSON.stringify(uid_arr), {
EX: dayEnd,
});
}
if (uid_arr.length > e.groupConfig.mysUidLimit && !e.isMaster) {
return { retcode: -200 };
}
//限制无用uid查询
if (uid < 100000050) {
return { retcode: 10102, message: "Data is not public for the user" };
}
isNew = false;
index = await redis.get(`genshin:ds:uid:${uid}`);
if (!index) {
//获取没有到30次的index
for (let i in BotConfig.mysCookies) {
//跳过达到上限的cookie
if (await redis.get(`genshin:ds:max:${i}}`)) {
continue;
}
let count = await redis.sendCommand(["scard", `genshin:ds:index:${i}`]);
if (count < 27) {
index = i;
break;
}
}
//查询已达上限
if (!index) {
return { retcode: -100 };
}
isNew = true;
}
if (!BotConfig.mysCookies[index]) {
return { retcode: -300 };
}
if (!BotConfig.mysCookies[index].includes("ltoken")) {
Bot.logger.error("米游社cookie错误请重新配置");
return { retcode: -400 };
}
}
let { url, headers, query, body } = getUrl(type, uid, data);
headers.Cookie = cookie || BotConfig.mysCookies[index];
let param = {
headers,
timeout: 10000,
};
if (body) {
param.method = "post";
param.body = body;
} else {
param.method = "get";
}
let response = {};
try {
response = await fetch(url, param);
} catch (error) {
Bot.logger.error(error);
return false;
}
if (!response.ok) {
Bot.logger.error(response);
return false;
}
const res = await response.json();
if (!res) {
Bot.logger.mark(`mys接口没有返回`);
return false;
}
if (isNew) {
await redis.sendCommand(["sadd", `genshin:ds:index:${index}`, uid]);
redis.expire(`genshin:ds:index:${index}`, dayEnd);
redis.set(`genshin:ds:uid:${uid}`, index, { EX: dayEnd });
}
if (res.retcode != 0 && ![10102, 1008, -1].includes(res.retcode)) {
let ltuid = headers.Cookie.match(/ltuid=(\w{0,9})/g)[0].replace(/ltuid=|;/g, "");
if (selfCookie && selfCookie.uid == uid) {
Bot.logger.mark(`mys接口报错:${JSON.stringify(res)}体力配置cookieltuid:${ltuid}`);
//体力cookie失效
if (res.message == "Please login") {
delete NoteCookie[e.user_id];
}
} else {
Bot.logger.mark(`mys接口报错:${JSON.stringify(res)},第${Number(index) + 1}个cookieltuid:${ltuid}`);
//标记达到上限的cookie自动切换下一个
if ([10101].includes(res.retcode)) {
redis.set(`genshin:ds:max:${index}`, "1", { EX: dayEnd });
}
}
}
return res;
}
function checkRetcode(res, uid, e) {
let qqName = "";
switch (res.retcode) {
case 0:
Bot.logger.debug(`mys查询成功:${uid}`);
return false;
case -1:
break;
case -100:
e.reply("无法查询,已达上限\n请配置更多cookie");
break;
case -200:
qqName = lodash.truncate(e.sender.card, { length: 8 });
e.reply([segment.at(e.user_id, qqName), "\n今日查询已达上限"]);
break;
case -300:
e.reply("尚未配置公共查询cookie无法查询原神角色信息\n私聊发送【配置cookie】进行设置");
break;
case -400:
e.reply("米游社cookie错误请重新配置");
break;
case 1001:
case 10001:
case 10103:
e.reply("米游社接口报错,暂时无法查询");
break;
case 1008:
qqName = lodash.truncate(e.sender.card, { length: 8 });
e.reply([segment.at(e.user_id, qqName), "\n请先去米游社绑定角色"]);
break;
case 10101:
e.reply("查询已达今日上限");
break;
case 10102:
if (res.message == "Data is not public for the user") {
qqName = lodash.truncate(e.sender.card, { length: 8 });
e.reply([segment.at(e.user_id, qqName), "\n米游社数据未公开"]);
} else {
e.reply(`id:${uid}请先去米游社绑定角色`);
}
break;
}
return true;
}
/**
* @param {角色昵称} keyword
* @param {是否搜索角色默认名} search_val
* @returns
*/
export function roleIdToName(keyword, search_val = false) {
if (!keyword) {
return false;
}
if (search_val) {
return genshin.roleId[keyword][0] ? genshin.roleId[keyword][0] : "";
}
if (!nameID) {
nameID = new Map();
for (let i in genshin.roleId) {
for (let val of genshin.roleId[i]) {
nameID.set(val, i);
}
}
}
let name = nameID.get(keyword);
return name ? name : "";
}
async function limitGet(e) {
if (!e.isGroup) {
return true;
}
if (e.isMaster) {
return true;
}
let key = `genshin:limit:${e.user_id}`;
let num = await redis.get(key);
if (num && num >= e.groupConfig.mysDayLimit - 1) {
let name = lodash.truncate(e.sender.card, { length: 8 });
e.reply([segment.at(e.user_id, name), "\n今日查询已达上限"]);
return false;
}
return true;
}
async function limitSet(e) {
if (!e.isGroup) {
return true;
}
let key = `genshin:limit:${e.user_id}`;
let dayEnd = getDayEnd();
await redis.incr(key);
redis.expire(key, dayEnd);
}
function getDayEnd() {
let now = new Date();
let dayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), "23", "59", "59").getTime() / 1000;
return dayEnd - parseInt(now.getTime() / 1000);
}

View File

@ -1,13 +0,0 @@
*{
padding:0;
margin:0;
}
html,body{
width:100%;
height:10px;
position:relative;
}
img.bg{
width:100%;
}

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="character.css">
<style>
body {
background-color: lightgreen;
}
@media only screen and (orientation: landscape) {
body {
background-color: lightblue;
}
}
</style>
<title>test</title>
</head>
<body>
<div class="content" style="background-image:url(w.jpg)">
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

97
components/Data.js Normal file
View File

@ -0,0 +1,97 @@
import lodash from "lodash";
import fs from "fs";
let Data = {
/*
* 根据指定的path依次检查与创建目录
* */
createDir(rootPath = "", path, includeFile = false) {
console.log(rootPath, path)
let pathList = path.split("/"),
nowPath = rootPath;
pathList.forEach((name, idx) => {
name = name.trim();
if (!includeFile && idx < pathList.length - 1) {
nowPath += name + "/";
if (name) {
if (!fs.existsSync(nowPath)) {
console.log(nowPath)
fs.mkdirSync(nowPath);
}
}
}
})
},
/*
* 读取json
* */
readJSON(root, path) {
if (!/\.json$/.test(path)) {
path = path + ".json";
}
// 检查并创建目录
Data.createDir(root, path, true);
let jsonRet = fs.readFileSync(`${root}/${path}`, "utf8");
return JSON.parse(jsonRet);
},
/*
* 写JSON
* */
writeJson(path, file, data, space = "\t") {
if (!/\.json$/.test(file)) {
file = file + ".json";
}
// 检查并创建目录
Data.createDir(path, true);
return fs.writeFileSync(`${path}/${file}`, JSON.stringify(data, null, space));
},
/*
* 返回一个从 target 中选中的属性的对象
*
* keyList : 获取字段列表逗号分割字符串
* key1, key2, toKey1:fromKey1, toKey2:fromObj.key
*
* defaultData: 当某个字段为空时会选取defaultData的对应内容
* toKeyPrefix返回数据的字段前缀默认为空defaultData中的键值无需包含toKeyPrefix
*
* */
getData(target, keyList = "", cfg = {}) {
target = target || {};
let defaultData = cfg.defaultData || {};
let ret = {};
// 分割逗号
if (typeof (keyList) === "string") {
keyList = keyList.split(",");
}
lodash.forEach(keyList, (keyCfg) => {
// 处理通过:指定 toKey & fromKey
let _keyCfg = keyCfg.split(":");
let keyTo = _keyCfg[0].trim(),
keyFrom = (_keyCfg[1] || _keyCfg[0]).trim(),
keyRet = keyTo;
if (cfg.lowerFirstKey) {
keyRet = lodash.lowerFirst(keyRet);
console.log('keyRet', keyRet)
}
if (cfg.keyPrefix) {
keyRet = cfg.keyPrefix + keyRet;
}
// 通过Data.getVal获取数据
ret[keyRet] = Data.getVal(target, keyFrom, defaultData[keyTo], cfg);
})
return ret;
},
getVal(target, keyFrom, defaultValue) {
return lodash.get(target, keyFrom, defaultValue);
}
};
export default Data;

158
components/MysApi.js Normal file
View File

@ -0,0 +1,158 @@
import md5 from "md5";
import lodash from 'lodash';
import fetch from "node-fetch";
let MysApi = {
getUrl(type, uid, data = {}) {
let url = "https://api-takumi.mihoyo.com";
let game_record = "/game_record/app/genshin/api/";
let server = MysApi.getServer(uid);
let query, body;
switch (type) {
//首页宝箱
case "index":
url += game_record + "index";
query = `role_id=${uid}&server=${server}`;
break;
//深渊
case "spiralAbyss":
url += game_record + "spiralAbyss";
query = `role_id=${uid}&schedule_type=${data.schedule_type}&server=${server}`;
break;
//角色详情
case "character":
url += game_record + "character";
body = JSON.stringify(data);
break;
//树脂每日任务只能当前id
case "dailyNote":
url += game_record + "dailyNote";
query = `role_id=${uid}&server=${server}`;
break;
case "detail":
url += "/event/e20200928calculate/v1/sync/avatar/detail";
query = `uid=${uid}&region=${server}&avatar_id=${data.avatar_id}`;
break;
case "getAnnouncement":
url += "/game_record/card/wapi/getAnnouncement";
break;
case "getGameRecordCard":
url += "/game_record/card/wapi/getGameRecordCard";
query = `uid=${uid}`;//米游社id
break;
case "bbs_sign_info":
url += "/event/bbs_sign_reward/info";
query = `act_id=e202009291139501&region=${server}&uid=${uid}`;
break;
case "bbs_sign_home":
url += "/event/bbs_sign_reward/home";
query = `act_id=e202009291139501&region=${server}&uid=${uid}`;
break;
case "bbs_sign":
url += "/event/bbs_sign_reward/sign";
body = JSON.stringify({ act_id: "e202009291139501", region: server, uid: uid, });
break;
case "ys_ledger":
url = "https://hk4e-api.mihoyo.com/event/ys_ledger/monthInfo";
query = `month=${data.month}&bind_uid=${uid}&bind_region=${server}`;
break;
}
if (query) {
url += "?" + query;
}
let headers;
if (type === "bbs_sign") {
headers = MysApi.getHeaders_sign();
} else {
headers = MysApi.getHeaders(query, body);
}
return { url, headers, query, body };
},
getServer(uid) {
switch (uid.toString()[0]) {
case "1":
case "2":
return "cn_gf01"; //官服
case "5":
return "cn_qd01"; //B服
}
return "cn_gf01"; //官服
},
//# Github-@lulu666lulu
getDs(q = "", b = "") {
let n = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs";
let t = Math.round(new Date().getTime() / 1000);
let r = Math.floor(Math.random() * 900000 + 100000);
let DS = md5(`salt=${n}&t=${t}&r=${r}&b=${b}&q=${q}`);
return `${t},${r},${DS}`;
},
//签到ds
getDS_sign() {
const n = "h8w582wxwgqvahcdkpvdhbh2w9casgfl";
const t = Math.round(new Date().getTime() / 1000);
const r = lodash.sampleSize("abcdefghijklmnopqrstuvwxyz0123456789", 6).join("");
const DS = md5(`salt=${n}&t=${t}&r=${r}`);
return `${t},${r},${DS}`;
},
getHeaders(q = "", b = "") {
return {
"x-rpc-app_version": "2.20.1",
"x-rpc-client_type": 5,
DS: MysApi.getDs(q, b),
};
},
getHeaders_sign() {
return {
"x-rpc-app_version": "2.3.0",
"x-rpc-client_type": 5,
"x-rpc-device_id": MysApi.guid(),
"User-Agent": " miHoYoBBS/2.3.0",
DS: MysApi.getDS_sign(),
};
},
guid() {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
},
// 按type请求
request: async function (type, cfg) {
let { uid } = cfg;
let { url, headers } = MysApi.getUrl(type, uid);
return await MysApi.fetch(url, headers, cfg);
},
// 发送请求
fetch: async function (url, cfg) {
let { cookie, error, success, headers, method } = cfg;
headers = headers || {};
method = method || "get";
headers.Cookie = cookie;
let response = await fetch(url, { method, headers });
if (!response.ok) {
return await error(-1, {
msg: "米游社接口错误"
})
}
let res = await response.json();
if (res.retcode * 1 !== 0) {
return await error(res.retcode * 1, res)
}
return await success(res.data, res)
}
}
export default MysApi;

295
components/User.js Normal file
View File

@ -0,0 +1,295 @@
import UserModel from "./models/UserModel.js";
import { segment } from "oicq";
import fetch from "node-fetch";
import { MysApi } from "./index.js";
import md5 from "md5";
const getUidByToken = async function (token) {
let ltoken = `ltoken=${token["ltoken"]}ltuid=${token["ltuid"]};`;
let cookie_token = `cookie_token=${token["cookie_token"]} account_id=${token["ltuid"]};`;
ltoken += cookie_token;
let uid = 0;
let url = host + "binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn";
await MysApi.fetch(url, {
method: "get",
cookie: ltoken,
error: async () => {
throw `cookie错误${res.message}`;
},
success: async (data) => {
for (let val of data.list) {
//米游社默认展示的角色
if (val.is_chosen) {
uid = val.game_uid;
break;
}
}
if (!uid) {
uid = data.list[0].game_uid;
}
}
});
return uid;
};
let User = {};
/*
* 在文本中检索uid若未查找到则返回false
* */
User.matchUid = function (msg) {
let ret = /[1|2|5][0-9]{8}/g.exec(msg);
if (ret) {
return ret[0];
}
return false;
}
/*
* 返回需要绑定 cookie
*
* */
User.replyNeedBind = function (e, replyMsg = "") {
replyMsg = replyMsg || `您尚未绑定米游社cookie无法进行操作`;
let helpMsg = "获取cookie后发送至当前聊天窗口即可Cookie获取方式https://docs.qq.com/doc/DUWNVQVFTU3liTVlO";
if (e.isGroup) {
replyMsg = segment.image(`file:///${_path}/resources/help/help.png`);
e.reply([replyMsg, helpMsg]);
} else {
e.reply(replyMsg);
e.reply(helpMsg);
}
return false;
};
/*
* 获取当前用户消息所查询的目标用户
*
* 策略优先级依次递减
* 1. 消息里包含 uid
* 2. 存在 msg.at且msg.at 用户 是绑定用户
* 3. 存在 msg.at 且msg.at 名片包含uid
* 4. 当前用户为绑定用户
* 5. 当前用户名片包含 uid
* 6. 当前用户存在redis-uid 缓存
* */
User.getTargetUser = async function (e, selfUser) {
let res;
let reg = /[1|2|5][0-9]{8}/g;
let msg = e.msg;
let targetId, targetUser;
/*-- 有指定的查询目标 --*/
/* 消息里包含 uid的话优先匹配 */
if (e.msg) {
targetId = getUid(e.msg);
if (targetId) {
// 根据targetId查找用户
targetUser = await User.get({ uid: targetId });
//存在则返回不存在则将该uid绑定至当前用户
if (targetUser) {
return targetUser;
}
let selfUserBindUid = await selfUser.getRegUid();
// 当前用户未注册则将uid绑定至当前用户
if (!selfUser.isBind || selfUser.uid == targetId) {
await selfUser.setRegUid(targetId)
return selfUser;
} else {
// 当前用户为注册用户,返回 Draft
return User.get({ uid: targetId }, true)
}
selfUser.setRegUid(targetId);
return selfUser;
}
}
// 如果有at的用户使用被at的用户
if (!targetId && e.at) {
targetUser = await User.get({ qq: e.at.qq });
// 识别at用户的名片结果。如果at用户无uid信息则使用此结果
targetId = getUid(e.at.card.toString());
if (targetUser) {
targetUser
}
}
if (targetUser) {
let targetUserUid = await targetUser.getRegUid();
targetUser.setRegUid(targetUserUid || targetId)
if (!targetId && !targetUserUid) {
// return false;
}
}
targetUser = selfUser;
// 使用当前用户作为targetUser
if (selfUser.isBind) {
// 设置查询用户为当前用户
targetUser = selfUser;
// 从当前用户的昵称中匹配uid
targetId = getUid(e.sender.card.toString());
} else if (false) {
// selfUser.uid =
}
selfUser.setRegUid(uid);
// 存在查询用户,但无
if (!targetUser && targetId) {
// 根据uid创建的用户包含uid
return User.get({ uid: targetId }, true);
} else if (targetUser && targetId) {
// 存在目标用户但不存在查询uid的话赋值给targetUser
if (!targetUser.uid && targetId) {
targetUser.uid = targetId;
}
}
//
if (targetUser) {
targetUser.setLastQuery(targetUser.id);
}
return targetUser;
//从redis获取
res = await redis.get(`genshin:uid:${e.user_id}`);
if (res) {
redis.expire(`genshin:uid:${e.user_id}`, 2592000);
return { isSelf: true, uid: res };
}
return { isSelf: true, uid: false };
};
/*
* 获取当前 MysApi 的最佳查询User
*
* 策略优先级依次递减 sUid 在下方代指被查询的Uid
* 1. 如果 sUid 为绑定用户优先使用绑定用户自身的 cookie 在不允许跨系统调用时需传递 allowCrossUid = false )
* 2. 如果 sUid 24小时内被查询过优先使用曾经查询过该用户的 cookie
* 3. 如果 当前查询用户为绑定用户优先使用绑定用户自身的 cookie
* 4. 使用系统cookie : 暂未接管bot逻辑目前需要传入getBotCookie方法
*
* */
User.getReqUser = async function (e, allowCrossUser = true, getBotCookie=false) {
// 当前用户
let selfUser = User.get(e.user_id);
// 被查询用户
let targetUser = await User.getTargetUser(e);
// 如果 sUid 为绑定用户,优先使用绑定用户自身的 cookie
if (targetUser.isBind && allowCrossUser) {
return targetUser;
}
// 如果 sUid 24小时内被查询过优先使用曾经查询过该用户的 cookie
let lastQueryUser = targetUser.getSourceUser();
if (lastQueryUser) {
return lastQueryUser;
}
// 如果 当前查询用户为绑定用户,优先使用绑定用户自身的 cookie
if (selfUser.isBind) {
await targetUser.setSourceUser(selfUser);
return selfUser;
}
// 使用系统 cookie
// 将系统注册的cookie视作机器人同样包装为 User 用户返回
let botUser = User.getAvailableBot(e, true);
if (botUser) {
await targetUser.setSourceUser(botUser);
return botUser;
}
return false;
};
/*
* 对当前用户的类型进行检查并对不符合条件的用户进行回复
* type: all-不检查bind-绑定用户设置了有效的NoteCookiemaster-管理员
* replyMsg不符合条件的消息
* */
User.check = async function (e, type = "all", checkParams = {}) {
let self = User.get(e.user_id);
let { limit = true, action, replyMsg } = checkParams;
// 校验频度限制
if (limit) {
if (!(await limitGet(e))) return true;
}
switch (type) {
case 'bind':
// 需要是绑定用户
if (!self.isBind) {
if (!replyMsg) {
action = action || "进行操作";
replyMsg = "您尚未绑定米游社cookie无法" + action;
}
User.replyNeedBind(e, replyMsg);
return false;
}
break;
case 'master':
if (!self.isMaster) {
// 如果主动传递了replyMsg则进行回复否则静默
if (replyMsg) {
e.reply(replyMsg)
}
return false;
}
case 'all':
//不检查权限
return self;
default:
return false;
}
return self;
};
/*
* 获取可用的机器人作为UserModel返回
* noticeError 在无可用机器人时是否 e.reply 错误信息
* */
// TODO 待实现
User.getAvailableBot = async function (e, noticeError = false) {
let id = md5("BOT_" & md5('cookie'));
User.get(md5);
User.bindCookie(cookie, {
isBot: true
});
return false;
};
export default User;

1
components/index.js Normal file
View File

@ -0,0 +1 @@
export Data from "./Data.js";

View File

@ -1 +1,23 @@
export default class Base{} import { Data } from "../index.js";
export default class Base {
constructor() {
this.name = "";
}
toString() {
return this.name;
}
getData(arrList = "", cfg = {}) {
return Data.getData(this, arrList, cfg);
}
// 获取指定值数据,支持通过
getVal(key, defaultValue) {
return Data.getVal(this, key, defaultValue);
}
}

View File

@ -1,33 +1,35 @@
import Base from "./Base.js"; import Base from "./Base.js";
import { roleId, abbr } from "../../../config/genshin/roleId.js"; import { roleId, abbr } from "../../../../config/genshin/roleId.js";
import lodash from "lodash"; import lodash from "lodash";
import fs from "fs"; import fs from "fs";
import Data from "../Data.js";
import request from "request";
let characterMap = {}; let characterMap = {};
let characterAttr = {};
const characterMeta = JSON.parse(fs.readFileSync("../meta/characters.json", "utf8")); // 读取配置
let characterMeta = Data.readJSON("./plugins/miao-plugin/components/meta", "characters.json")// JSON.parse(fs.readFileSync(__dirname + "../meta/characters.json", "utf8"));
characterMeta = lodash.keyBy(characterMeta, (d) => d.Name);
const elemName = {
pyro: "火",
hydro: "水",
dendro: "草",
electro: "雷",
anemo: "风",
cryo: "冰",
geo: "岩"
};
lodash.forEach(characterMeta, (meta)=>{
characterMap[meta.Name] = new Character(name, meta)
});
lodash.forEach(roleId, function(names, id){
if(characterMap[names[0]]){
characterMap[names[0]].id = id;
}
});
class Character extends Base { class Character extends Base {
constructor(name) {
constructor(name, meta) {
super(); super();
let key = "".split(); this.name = name;
this._meta = meta; this.sName = this.name;
} this.id = YunzaiApps.mysInfo['roleIdToName'](name);
get sortName() { lodash.extend(this, getMetaData(name))
return characterAttr[this.name] || this.name;
} }
get id() { get id() {
@ -38,23 +40,126 @@ class Character extends Base {
} }
} }
getData(key){ async checkImgCache(resDir) {
// 处理img信息
let chcheDir = resDir + "/cache/";
}
} }
let cacheImgFile = async function (url, cacheDir) {
let ret = /^https:\/\/(.*)$/.exec(url);
if (ret && ret[1] && /\.(png|jpg|gif|jpeg|webp)$/.test(ret[1])) {
let fileName = ret[1];
Data.createDir(cacheDir, fileName);
request(imgUrl).pipe(fs.createWriteStream(cacheDir + fileName));
}
} }
// 获取指定角色的Meta信息
let getMetaData = function (name) {
if (!characterMeta[name]) {
return {};
}
const metaCfg = { lowerFirstKey: true },
meta = characterMeta[name];
// 处理基础信息
let ret = Data.getData(meta, "Name,Title,desc:Description,astro:AstrolabeName", metaCfg);
// 处理图像信息
ret.img = Data.getData(meta, "Weapon,Element,City,Profile,GachaCard,GachaSplash,Source", metaCfg);
// 处理元素
let elemRet = /([a-z]*).png$/.exec(meta.Element);
if (elemRet && elemRet[1]) {
ret.elementType = ret[1];
ret.element = elemName[ret.elementType];
}
// 处理属性
ret.stat = Data.getData(meta, "BaseHP,BaseATK,BaseDEF,aStat:AscensionStat,aStatValue:AscensionStatValue");
ret.statPerLv = lodash.map(meta.CharStat, (d) => Data.getData(d, "Name,Values", metaCfg));
// 处理材料
let itemKey = lodash.map("talent,boss,gemStone,Local,monster,weekly".split(","), (a) => `${a}:${lodash.upperFirst(a)}.Name`);
ret.item = Data.getData(meta, itemKey, metaCfg)
// 处理天赋
ret.talents = {
a: getTalentData(meta.NormalAttack),
e: getTalentData(meta.TalentE),
q: getTalentData(meta.TalentQ),
};
// 处理其他天赋
ret.passiveTalents = lodash.map(meta.PassiveTalents, (d) => Data.getData(d, "Name,desc:Description,icon:Source", metaCfg))
// 处理命座信息
let cons = {};
lodash.forEach(meta.Constellation, (data, key) => {
cons[key.replace("Constellation")] = Data.getData(data, "Name,icon:Source,desc:Description", metaCfg);
})
ret.cons = cons;
return ret;
}
// 获取Meta中的天赋信息
const getTalentData = function (data) {
let ret = Data.getData(data, "Name,icon:Source,desc:Description", { lowerFirstKey: true });
let attr = [], table = [], tableKeys;
lodash.forEach(data.Table, (tr) => {
let tmp = { name: tr.Name }, isTable = true, isDef = false, lastVal;
// 检查当前行是否是表格数据
lodash.forEach(tr.Values, (v) => {
// 如果为空则退出循环
if (v === "") {
isTable = false;
return false;
}
if (typeof (lastVal) === "undefined") {
// 设定初始值
lastVal = v;
} else if (lastVal != v) {
// 如果与初始值不一样,则标记退出循环
isDef = true;
return false;
}
});
if (isTable && isDef) {
if (!tableKeys) {
tableKeys = lodash.keys(tr.Values);
}
tmp.value = lodash.map(tableKeys, (k) => tr.Values[k])
table.push(tmp);
} else {
tmp.value = lastVal;
attr.push(tmp)
}
})
ret.attr = attr;
ret.table = table;
ret.tableKeys = tableKeys;
return ret;
}
Character.get = function (val) { Character.get = function (val) {
let name = YunzaiApps.mysInfo.roleIdToName(val); let roleid = YunzaiApps.mysInfo['roleIdToName'](val);
let name = YunzaiApps.mysInfo['roleIdToName'](roleid, true);
if (!name) { if (!name) {
return false; return false;
} }
if (!characterMap[name]) { if (!characterMap[name]) {
//characterMap[name] = let character = new Character(name);
characterMap[name] = character;
} }
return characterMap[name];
}; };
export default Character; export default Character;

View File

@ -0,0 +1,273 @@
/*
* UserModel Class
* 提供用户实例相关的操作方法
*
* * TODO将与具体用户操作相关的方法逐步迁移到UserModel中外部尽量只调用实例方法
* 以确保逻辑收敛且维护性更强
* */
import BaseModel from "./BaseModel.js"
import lodash from "lodash";
import md5 from "md5";
import { MysApi, Data } from "../index.js";
const _path = process.cwd();
const redisPrefix = "cache";
const userInstanceReclaimTime = 60;
let userMap = {};
// Redis相关操作方法
const Cache = {
prefix: "genshin",
async get(type, key) {
return await redis.get(`${Cache.prefix}:${type}:${key}`);
},
async set(type, key, val, exp = 2592000) {
return await redis.set(`${Cache.prefix}:${type}:${key}`, val, { EX: exp });
},
async del(type, key) {
return await redis.del(`${Cache.prefix}:${type}:${key}`);
}
};
const saveCookieFile = function () {
Data.writeJson("./data/NoteCookie/", "NoteCookie", NoteCookie);
};
// UserModel class
class UserModel extends BaseModel {
// 初始化用户
constructor(id) {
super();
// 一个id对应一个用户根据id检索用户信息
this.id = id;
// 检索是否存在NoteCookie信息
let data = NoteCookie[id];
if (data) {
this._data = data;
this.isPush = data.isPush;
this.isSignAuto = data.isSignAuto;
this.uid = data.uid;
} else {
this._data = {};
}
}
// 是绑定的cookie用户
// 需要存在NoteCookie记录且存在 cookie 与 uid 才认为是正确记录
get isBind() {
let dbData = NoteCookie[this.id];
return !!(dbData && dbData.cookie && dbData.uid);
}
// 是否是管理员
// TODO
get isMaster() {
return !this.isBot && BotConfig.masterQQ && BotConfig.masterQQ.includes(Number(this.id));
}
get isBot() {
// todo
return false;
}
// 获取当前用户cookie
get cookie() {
return this._data.cookie;
}
// 获取当前用户uid
get uid() {
return this._uid || this._data.uid || this._reg_uid;
}
set uid(uid) {
this._uid = uid;
this._reg_uid = uid;
}
// 保存用户信息
/*
async _save() {
// todo
return
let data = NoteCookie[this.id] || this._data || {};
// 将信息更新至 NoteCookie
data.id = this.id;
data.uid = this._uid || this._data.uid;
data.cookie = this._cookie || this._data.cookie;
data.isPush = this.isPush;
data.isAutoSign = !!this.isAutoSign;
// 保存信息
NoteCookie[this.id] = data;
this._data = data;
saveCookieFile();
// 建立当前用户相关缓存
await this.refreshCache();
return this;
}
*/
// 设置&更新用户缓存
async refreshCache() {
// 设置缓存
await Cache.set("id-uid", this.qq, this.uid);
await Cache.set("uid-id", this.uid, this.id);
Bot.logger.mark(`绑定用户QQ${this.id},UID${this.uid}`);
}
// 删除用户缓存
async delCache() {
await Cache.del("id-uid", this.id);
await Cache.del("uid-id", this.uid);
}
// 获取曾经查询过当前用户的人
async getSourceUser() {
let lastQuery = await Cache.get("id-source", this.id);
if (lastQuery) {
return UserModel.get(lastQuery);
}
return false;
}
// 设置曾经查询过当前用户的人缓存23小时
async setSourceUser(user) {
await Cache.set("id-source", this.id, user.id, 3600 * 23);
}
// 删除曾经查询过当前用户的人
async delSourceUser() {
await Cache.del("id-source", this.id);
}
/* uid
*
* 1. 如果是绑定用户优先返回当前绑定的uidcookie 对应uid
* 2. 返回redis中存储的uid
*
* redis uid需要主动调用一次 getRegUid 才能被this.uid访问到
*
* */
async getRegUid() {
if (this.isBind) {
return this.uid;
}
if (!this._reg_uid) {
let uid = await Cache.get('id-regUid', this.id);
if (uid) {
this._reg_uid = uid;
}
}
return this._reg_uid;
}
async setRegUid(uid) {
// 只有非绑定用户才设置 注册uid
if (!this.isBind) {
this._reg_uid = uid;
Cache.set('id-regUid', this.id, uid);
Cache.set('regUid-id', this.uid, this.id);
}
}
}
/* UserModel static function */
/*
* 获取用户实例
* query为获取条件默认为 id
*
* */
UserModel.get = async function (query, getDraftWhenNotFound = false) {
let user = await getUser(query, getDraftWhenNotFound);
user._reclaimFn && clearTimeout(user._reclaimFn);
user._reclaimFn = setTimeout(() => {
delete userMap[user.id];
}, userInstanceReclaimTime);
userMap[user.id] = user;
return user;
};
// 格式化查询
const formatQuery = function (query) {
if (typeof (query) === "string") {
return { id: query };
}
return query;
};
let getUser = async function (query, getDraftWhenNotFound = false) {
query = formatQuery(query);
let id = "";
// 根据id获取用户
if (query.id) {
id = query.id;
} else if (query.uid) {
// 根据uid检索id
id = await Cache.get("uid-id", query.uid);
if (!id) {
// 如未查找到则从注册uid中检索
id = await Cache.get("regUid-id", query.uid)
}
} else if (query.token) {
// 根据token检索id
// 不常用,仅用在机器人绑定环节
id = await Cache.get("token-id", query.token);
}
// 已有实例优先使用已有的
if (userMap[id]) {
return userMap[id];
}
// 如果是注册用户则返回新instance
if (NoteCookie[id]) {
return new UserModel(id);
}
// 如果允许返回Draft则生成并返回
if (getDraftWhenNotFound) {
return getDraft(query);
}
// 未查询到用户则返回false
return false;
}
let getDraft = function (query) {
let id = '';
if (query.id) {
id = query.id;
} else if (query.uid) {
id = '_UID_' + query.uid;
} else if (query.token) {
id = "_CK_" + md5(query.token);
}
let user = new UserModel(id);
user.id = query.id;
user.uid = query.uid;
user.cookie = query.cookie;
return user;
}
export default UserModel;

3
config.js Normal file
View File

@ -0,0 +1,3 @@
export default{
}

View File

@ -0,0 +1,13 @@
export const rule = {
character: {
reg: "^#(.*)$",
priority: 208,
describe: "【#刻晴】角色详情",
}
};
export { character } from "./apps/character.js";

View File

View File

@ -0,0 +1,304 @@
@font-face {
font-family: "HWZhongSong";
src: url("../../font/华文中宋.TTF");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "tttgbnumber";
src: url("../../font/tttgbnumber.ttf");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "NZBZ";
src: url("../font/NZBZ.ttf");
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
html, body {
width: 500px;
}
body {
font-size: 16px;
color: #fff;
font-family: "tttgbnumber";
transform: scale(1.40);
transform-origin: 0 0;
}
.container {
position: relative;
background-color: #1234;
width: 100%;
}
.container img.bg {
width: 100%;
margin-bottom: -1px;
}
.info {
position: absolute;
background: rgba(0, 0, 0, .5);
bottom: 0px;
left: 0px;
right: 0px;
box-shadow: 0 -5px 10px 0 #000;
padding:35px 10px 10px 35px;
}
.role_box {
padding: 5px 10px;
background-repeat: no-repeat;
position: relative;
}
.title {
font-size: 36px;
}
.role_name {
font-family: "NZBZ";
font-size: 80px;
letter-spacing: 5px;
line-height: 90px;
height: 100px;
text-shadow: 0 0 1px #000, 3px 3px 6px #000;
display: inline-block;
position:absolute;
top:-70px;
left:20px;
}
.role_name:after {
content: "";
display: block;
position: absolute;
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.5) 20%, rgba(255, 255, 255, 0.5) 80%, rgba(255, 255, 255, 0) 100%);
right: -50px;
min-width: 360px;
height: 1px;
bottom: 0;
left: -50px;
opacity: 1;
transition: width 0.3s 0.1s, opacity 0.3s 0.1s;
}
.weapon {
text-align: center;
font-size: 20px;
padding: 3px;
background: rgba(0, 0, 0, 0.6);
border-radius: 5px;
height: 100px;
width: 100px;
position: absolute;
overflow: hidden;
right:15px;
top:45px;
}
.lv {
font-family: "tttgbnumber";
font-size: 26px;
margin: 10px 5px 2px 5px;
}
.weapon .num {
position: absolute;
bottom: 0px;
font-size: 12px;
border-radius: 5px;
padding: 1px 5px;
background-color: rgba(0, 0, 0, var(--bg-opacity));
--bg-opacity: 0.75;
}
.weapon_num {
position: absolute;
top: 0;
right: 0;
width: 30px;
height: 30px;
font-size: 20px;
text-align: center;
line-height: 30px;
background: #000;
border-radius: 5px;
padding: 1px 3px;
}
.weapon img {
width: 100%;
transform: scale(1.2, 1.2);
}
.equiv .img_box {
width: 46px;
height: 46px;
border: 1px solid #d3bc8d;
border-radius: 5px;
overflow: hidden;
}
.equiv img {
width: 100%;
transform: scale(1.2, 1.2);
}
.detail {
margin-left: 5px;
width: 300px;
margin-top: 3px;
padding: 5px 0;
display: flex;
position: relative;
}
.skill {
margin-left: 5px;
width: 300px;
padding-bottom: 6px;
display: flex;
position: relative;
}
.text_box::before {
content: "";
display: block;
position: absolute;
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.5) 20%,
rgba(255, 255, 255, 0.5) 80%,
rgba(255, 255, 255, 0) 100%
);
width: 0%;
height: 1px;
top: 0;
left: -15px;
width: 300px;
opacity: 1;
transition: width 0.3s 0.1s, opacity 0.3s 0.1s;
}
.text_box::after {
content: "";
display: block;
position: absolute;
background-image: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.5) 20%,
rgba(255, 255, 255, 0.5) 80%,
rgba(255, 255, 255, 0) 100%
);
width: 0%;
height: 1px;
bottom: 0;
left: -15px;
width: 300px;
opacity: 1;
transition: width 0.3s 0.1s, opacity 0.3s 0.1s;
}
.detail p,
.skill p {
margin-right: 4px;
line-height: 16px;
width: 90px;
white-space: nowrap;
}
.no_skill {
padding: 10px 0;
}
.star {
width: 16px;
vertical-align: -2px;
margin-right: 1px;
}
.equiv {
margin-top: 12px;
}
.row {
display: flex;
flex-wrap: wrap;
/*margin-bottom: 5px;*/
}
.equiv {
margin: 0 10px 8px 10px;
text-align: center;
font-size: 20px;
padding: 3px;
background: rgba(0, 0, 0, 0.6);
border-radius: 5px;
height: 52px;
width: 52px;
position: relative;
display:flex;
}
.equiv .num {
position: absolute;
bottom: 0px;
font-size: 12px;
/*background: rgba(0,0,0,.6);*/
border-radius: 5px;
padding: 1px 5px;
background-color: rgba(0, 0, 0, var(--bg-opacity));
--bg-opacity: 0.75;
border-radius: 9999px;
}
.equiv .img_box {
width: 46px;
height: 46px;
border: 1px solid #d3bc8d;
border-radius: 5px;
overflow: hidden;
}
.equiv img {
width: 100%;
transform: scale(1.2, 1.2);
}
.equiv_info {
display: inline-block;
font-size: 15px;
padding: 5px 5px 1px 7px;
border-radius: 10px;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), rgba(114, 102, 104, 0.3);
margin-left: 5px;
}
.equiv_info .text {
margin-bottom: 5px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}

View File

@ -0,0 +1,55 @@
<!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="{{_res_path}}/MiaoPlugin/character/character.css?v=1.0"/>
<link rel="preload" href="{{_res_path}}/font/tttgbnumber.ttf" as="font">
<link rel="preload" href="{{_res_path}}/font/华文中宋.TTF" as="font">
<link rel="preload" href="{{_res_path}}/genshin/logo/bg/{{name}}1.png" as="image">
<link rel="preload" href="{{_res_path}}/genshin/logo/bg/{{name}}2.png" as="image">
</head>
<body>
<div class="container" id="container">
<div class="info">
<div class="role_name">
{{"神里凌华"}}
</div>
<div class="lv">ID:{{uid}} Lv.{{level}} ❤{{fetter}}</div>
<div class="weapon">
<div class="img_box">
<img src="{{_res_path}}/genshin/logo/weapon/{{weapon.name}}.png"/>
</div>
<p class="num">lv{{weapon.level}}</p>
<p class="weapon_num">{{weapon.affix_level}}</p>
</div>
{{ if skill.a }}
<div class="skill">
<p> 爆发:<span>{{ skill.q.level_current}}</span></p>
<p> 战技:<span>{{ skill.e.level_current}}</span></p>
<p> 普攻:<span>{{ skill.a.level_current}}</span></p>
</div>
{{/if}}
<div class="equiv">
<div class="item">
<div class="img_box">
<img src="{{_res_path}}/genshin/logo/{{reliquaries.type}}/{{reliquaries.name}}.png"/>
</div>
<p class="num">+{{reliquaries.level}}</p>
</div>
</div>
<div class="equiv_info">
<div class="text">{{text1}}</div>
<div class="text">{{text2}}</div>
</div>
</div>
<div>
<img src="{{_res_path}}/MiaoPlugin/characterImg/刻晴/1.jpg" class="bg"></div>
</div>
</body>
<script type="text/javascript"></script>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Some files were not shown because too many files have changed in this diff Show More