mirror of
https://github.com/venera-app/venera-configs.git
synced 2025-09-27 08:27:24 +00:00

* add new source from comick * fix some code * fix gif load and comic list info(none-type/chapter/volume) * add some comick hidden tags * revise coding error in file * info updata time * fix no-EN error * add new function - Multi-language comic selection support - Added comic recommendations - Fixed empty chapter return bug - Resolved tag click issues - Optimized data processing * Optimize network request Remove redundant requests and prevent async deadlocks * Update comick.js * new small comic source from baihehui * Fixed some bugs and added some sorting methods * Fixed some bugs and added some sorting methods * Add a new resource from ykmh * Remove invalid request * fixed chapter api * Update index.json * Update index.json * Update comick.js * fix search bug from manhuagui Fix ”querySelectorAll“ bug in search page. Add multi-group of chapters in info page. * add lanraragi * Update manhuagui.js login, comment, favorites * Update index.json * Update index.json
1350 lines
42 KiB
JavaScript
1350 lines
42 KiB
JavaScript
/** @type {import('./_venera_.js')} */
|
||
class ManHuaGui extends ComicSource {
|
||
name = "漫画柜";
|
||
|
||
key = "ManHuaGui";
|
||
|
||
version = "1.1.0";
|
||
|
||
minAppVersion = "1.4.0";
|
||
|
||
url = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/manhuagui.js";
|
||
|
||
baseUrl = "https://www.manhuagui.com";
|
||
|
||
account = {
|
||
login: async (username, password) => {
|
||
let headers = {
|
||
'content-type': 'application/x-www-form-urlencoded',
|
||
'accept': 'application/json, text/javascript, */*; q=0.01',
|
||
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||
'cache-control': 'no-cache',
|
||
'pragma': 'no-cache',
|
||
'x-requested-with': 'XMLHttpRequest',
|
||
'origin': this.baseUrl,
|
||
'referer': `${this.baseUrl}/`,
|
||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
|
||
};
|
||
let body = `txtUserName=${encodeURIComponent(username)}&txtPassword=${encodeURIComponent(password)}`;
|
||
let res = await Network.post(`${this.baseUrl}/tools/submit_ajax.ashx?action=user_login`, headers, body);
|
||
if (res.status !== 200) {
|
||
throw "Invalid status code: " + res.status;
|
||
}
|
||
|
||
let setCookieHeader = res.headers['set-cookie'];
|
||
if (!setCookieHeader) {
|
||
throw "Set-Cookie header not found";
|
||
}
|
||
|
||
let cookies = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
||
let myCookie = null;
|
||
|
||
for (let cookie of cookies) {
|
||
let match = cookie.match(/my=([^;]+)/);
|
||
if (match) {
|
||
myCookie = match[1];
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!myCookie) {
|
||
throw "my cookie not found in Set-Cookie header";
|
||
}
|
||
|
||
this.saveData('mhg_cookie', "my="+myCookie);
|
||
return "ok";
|
||
},
|
||
|
||
logout: function() {
|
||
this.deleteData('mhg_cookie');
|
||
},
|
||
|
||
registerWebsite: "https://www.manhuagui.com/user/register"
|
||
|
||
};
|
||
|
||
isAppVersionAfter(target) {
|
||
if (!APP || !APP.version) return false;
|
||
let current = APP.version;
|
||
let targetArr = target.split('.');
|
||
let currentArr = current.split('.');
|
||
for (let i = 0; i < 3; i++) {
|
||
if (parseInt(currentArr[i]) < parseInt(targetArr[i])) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async getHtml(url) {
|
||
let mhg_cookie = this.loadData("mhg_cookie");
|
||
let headers = {
|
||
accept:
|
||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||
"cache-control": "no-cache",
|
||
pragma: "no-cache",
|
||
priority: "u=0, i",
|
||
"sec-ch-ua":
|
||
'"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
|
||
"sec-ch-ua-mobile": "?0",
|
||
"sec-ch-ua-platform": '"Windows"',
|
||
"sec-fetch-dest": "document",
|
||
"sec-fetch-mode": "navigate",
|
||
"sec-fetch-site": "same-origin",
|
||
"sec-fetch-user": "?1",
|
||
"upgrade-insecure-requests": "1",
|
||
Referer: "https://www.manhuagui.com/",
|
||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||
cookie: mhg_cookie
|
||
};
|
||
let res = await Network.get(url, headers);
|
||
if (res.status !== 200) {
|
||
throw "Invalid status code: " + res.status;
|
||
}
|
||
let document = new HtmlDocument(res.body);
|
||
return document;
|
||
}
|
||
parseSimpleComic(e) {
|
||
let urlElement = e.querySelector(".ell > a");
|
||
if (!urlElement) {
|
||
console.warn("parseSimpleComic: Missing .ell > a element");
|
||
return null;
|
||
}
|
||
let url = urlElement.attributes["href"];
|
||
let id = url.split("/")[2];
|
||
let title = urlElement.text.trim();
|
||
|
||
let imgElement = e.querySelector("img");
|
||
if (!imgElement) {
|
||
console.warn("parseSimpleComic: Missing img element");
|
||
return null;
|
||
}
|
||
let cover = imgElement.attributes["src"] || imgElement.attributes["data-src"];
|
||
if (!cover) {
|
||
console.warn("parseSimpleComic: Missing cover attribute");
|
||
return null;
|
||
}
|
||
cover = `https:${cover}`;
|
||
|
||
let descriptionElement = e.querySelector(".tt");
|
||
let description = descriptionElement ? descriptionElement.text.trim() : "";
|
||
|
||
return new Comic({
|
||
id,
|
||
title,
|
||
cover,
|
||
description,
|
||
});
|
||
}
|
||
|
||
parseComic(e) {
|
||
let simple = this.parseSimpleComic(e);
|
||
let sl = e.querySelector(".sl");
|
||
let status = sl ? "连载" : "完结";
|
||
let tmp = e.querySelector(".updateon").childNodes;
|
||
let update = tmp[0].replace("更新于:", "").trim();
|
||
let tags = [status, update];
|
||
|
||
return new Comic({
|
||
id: simple.id,
|
||
title: simple.title,
|
||
cover: simple.cover,
|
||
description: simple.description,
|
||
tags,
|
||
author,
|
||
});
|
||
}
|
||
/**
|
||
* [Optional] init function
|
||
*/
|
||
init() {
|
||
var LZString = (function () {
|
||
var f = String.fromCharCode;
|
||
var keyStrBase64 =
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||
var baseReverseDic = {};
|
||
function getBaseValue(alphabet, character) {
|
||
if (!baseReverseDic[alphabet]) {
|
||
baseReverseDic[alphabet] = {};
|
||
for (var i = 0; i < alphabet.length; i++) {
|
||
baseReverseDic[alphabet][alphabet.charAt(i)] = i;
|
||
}
|
||
}
|
||
return baseReverseDic[alphabet][character];
|
||
}
|
||
var LZString = {
|
||
decompressFromBase64: function (input) {
|
||
if (input == null) return "";
|
||
if (input == "") return null;
|
||
return LZString._0(input.length, 32, function (index) {
|
||
return getBaseValue(keyStrBase64, input.charAt(index));
|
||
});
|
||
},
|
||
_0: function (length, resetValue, getNextValue) {
|
||
var dictionary = [],
|
||
next,
|
||
enlargeIn = 4,
|
||
dictSize = 4,
|
||
numBits = 3,
|
||
entry = "",
|
||
result = [],
|
||
i,
|
||
w,
|
||
bits,
|
||
resb,
|
||
maxpower,
|
||
power,
|
||
c,
|
||
data = {
|
||
val: getNextValue(0),
|
||
position: resetValue,
|
||
index: 1,
|
||
};
|
||
for (i = 0; i < 3; i += 1) {
|
||
dictionary[i] = i;
|
||
}
|
||
bits = 0;
|
||
maxpower = Math.pow(2, 2);
|
||
power = 1;
|
||
while (power != maxpower) {
|
||
resb = data.val & data.position;
|
||
data.position >>= 1;
|
||
if (data.position == 0) {
|
||
data.position = resetValue;
|
||
data.val = getNextValue(data.index++);
|
||
}
|
||
bits |= (resb > 0 ? 1 : 0) * power;
|
||
power <<= 1;
|
||
}
|
||
switch ((next = bits)) {
|
||
case 0:
|
||
bits = 0;
|
||
maxpower = Math.pow(2, 8);
|
||
power = 1;
|
||
while (power != maxpower) {
|
||
resb = data.val & data.position;
|
||
data.position >>= 1;
|
||
if (data.position == 0) {
|
||
data.position = resetValue;
|
||
data.val = getNextValue(data.index++);
|
||
}
|
||
bits |= (resb > 0 ? 1 : 0) * power;
|
||
power <<= 1;
|
||
}
|
||
c = f(bits);
|
||
break;
|
||
case 1:
|
||
bits = 0;
|
||
maxpower = Math.pow(2, 16);
|
||
power = 1;
|
||
while (power != maxpower) {
|
||
resb = data.val & data.position;
|
||
data.position >>= 1;
|
||
if (data.position == 0) {
|
||
data.position = resetValue;
|
||
data.val = getNextValue(data.index++);
|
||
}
|
||
bits |= (resb > 0 ? 1 : 0) * power;
|
||
power <<= 1;
|
||
}
|
||
c = f(bits);
|
||
break;
|
||
case 2:
|
||
return "";
|
||
}
|
||
dictionary[3] = c;
|
||
w = c;
|
||
result.push(c);
|
||
while (true) {
|
||
if (data.index > length) {
|
||
return "";
|
||
}
|
||
bits = 0;
|
||
maxpower = Math.pow(2, numBits);
|
||
power = 1;
|
||
while (power != maxpower) {
|
||
resb = data.val & data.position;
|
||
data.position >>= 1;
|
||
if (data.position == 0) {
|
||
data.position = resetValue;
|
||
data.val = getNextValue(data.index++);
|
||
}
|
||
bits |= (resb > 0 ? 1 : 0) * power;
|
||
power <<= 1;
|
||
}
|
||
switch ((c = bits)) {
|
||
case 0:
|
||
bits = 0;
|
||
maxpower = Math.pow(2, 8);
|
||
power = 1;
|
||
while (power != maxpower) {
|
||
resb = data.val & data.position;
|
||
data.position >>= 1;
|
||
if (data.position == 0) {
|
||
data.position = resetValue;
|
||
data.val = getNextValue(data.index++);
|
||
}
|
||
bits |= (resb > 0 ? 1 : 0) * power;
|
||
power <<= 1;
|
||
}
|
||
dictionary[dictSize++] = f(bits);
|
||
c = dictSize - 1;
|
||
enlargeIn--;
|
||
break;
|
||
case 1:
|
||
bits = 0;
|
||
maxpower = Math.pow(2, 16);
|
||
power = 1;
|
||
while (power != maxpower) {
|
||
resb = data.val & data.position;
|
||
data.position >>= 1;
|
||
if (data.position == 0) {
|
||
data.position = resetValue;
|
||
data.val = getNextValue(data.index++);
|
||
}
|
||
bits |= (resb > 0 ? 1 : 0) * power;
|
||
power <<= 1;
|
||
}
|
||
dictionary[dictSize++] = f(bits);
|
||
c = dictSize - 1;
|
||
enlargeIn--;
|
||
break;
|
||
case 2:
|
||
return result.join("");
|
||
}
|
||
if (enlargeIn == 0) {
|
||
enlargeIn = Math.pow(2, numBits);
|
||
numBits++;
|
||
}
|
||
if (dictionary[c]) {
|
||
entry = dictionary[c];
|
||
} else {
|
||
if (c === dictSize) {
|
||
entry = w + w.charAt(0);
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
result.push(entry);
|
||
dictionary[dictSize++] = w + entry.charAt(0);
|
||
enlargeIn--;
|
||
w = entry;
|
||
if (enlargeIn == 0) {
|
||
enlargeIn = Math.pow(2, numBits);
|
||
numBits++;
|
||
}
|
||
}
|
||
},
|
||
};
|
||
return LZString;
|
||
})();
|
||
|
||
function splitParams(str) {
|
||
let params = [];
|
||
let currentParam = "";
|
||
let stack = [];
|
||
|
||
for (let i = 0; i < str.length; i++) {
|
||
const char = str[i];
|
||
|
||
if (char === "(" || char === "[" || char === "{") {
|
||
stack.push(char);
|
||
currentParam += char;
|
||
} else if (char === ")" && stack[stack.length - 1] === "(") {
|
||
stack.pop();
|
||
currentParam += char;
|
||
} else if (char === "]" && stack[stack.length - 1] === "[") {
|
||
stack.pop();
|
||
currentParam += char;
|
||
} else if (char === "}" && stack[stack.length - 1] === "{") {
|
||
stack.pop();
|
||
currentParam += char;
|
||
} else if (char === "," && stack.length === 0) {
|
||
params.push(currentParam.trim());
|
||
currentParam = "";
|
||
} else {
|
||
currentParam += char;
|
||
}
|
||
}
|
||
|
||
if (currentParam) {
|
||
params.push(currentParam.trim());
|
||
}
|
||
|
||
return params;
|
||
}
|
||
|
||
function extractParams(str) {
|
||
let params_part = str.split("}(")[1].split("))")[0];
|
||
let params = splitParams(params_part);
|
||
params[5] = {};
|
||
params[3] = LZString.decompressFromBase64(params[3].split("'")[1]).split(
|
||
"|"
|
||
);
|
||
return params;
|
||
}
|
||
|
||
function formatData(p, a, c, k, e, d) {
|
||
e = function (c) {
|
||
return (
|
||
(c < a ? "" : e(parseInt(c / a))) +
|
||
((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
|
||
);
|
||
};
|
||
if (!"".replace(/^/, String)) {
|
||
while (c--) d[e(c)] = k[c] || e(c);
|
||
k = [
|
||
function (e) {
|
||
return d[e];
|
||
},
|
||
];
|
||
e = function () {
|
||
return "\\w+";
|
||
};
|
||
c = 1;
|
||
}
|
||
while (c--)
|
||
if (k[c]) p = p.replace(new RegExp("\\b" + e(c) + "\\b", "g"), k[c]);
|
||
return p;
|
||
}
|
||
function extractFields(text) {
|
||
// 创建一个对象存储提取的结果
|
||
const result = {};
|
||
|
||
// 提取files数组
|
||
const filesMatch = text.match(/"files":\s*\[(.*?)\]/);
|
||
if (filesMatch && filesMatch[1]) {
|
||
// 提取所有文件名并去除引号和空格
|
||
result.files = filesMatch[1]
|
||
.split(",")
|
||
.map((file) => file.trim().replace(/"/g, ""));
|
||
}
|
||
|
||
// 提取path
|
||
const pathMatch = text.match(/"path":\s*"([^"]+)"/);
|
||
if (pathMatch && pathMatch[1]) {
|
||
result.path = pathMatch[1];
|
||
}
|
||
|
||
// 提取len
|
||
const lenMatch = text.match(/"len":\s*(\d+)/);
|
||
if (lenMatch && lenMatch[1]) {
|
||
result.len = parseInt(lenMatch[1], 10);
|
||
}
|
||
|
||
// 提取sl对象
|
||
const slMatch = text.match(/"sl":\s*({[^}]+})/);
|
||
if (slMatch && slMatch[1]) {
|
||
try {
|
||
// 将提取的字符串转换为对象
|
||
result.sl = JSON.parse(slMatch[1].replace(/(\w+):/g, '"$1":'));
|
||
} catch (e) {
|
||
console.error("解析sl字段失败:", e);
|
||
result.sl = null;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
this.getImgInfos = function (script) {
|
||
let params = extractParams(script);
|
||
let imgData = formatData(...params);
|
||
let imgInfos = extractFields(imgData);
|
||
return imgInfos;
|
||
};
|
||
}
|
||
|
||
// explore page list
|
||
explore = [
|
||
{
|
||
title: "漫画柜",
|
||
type: "multiPartPage",
|
||
/**
|
||
* 参考 manhuagui_explore.html,抓取“热门漫画最新更新”与 tab 板块
|
||
*/
|
||
load: async (page) => {
|
||
let document = await this.getHtml(this.baseUrl);
|
||
let parts = [];
|
||
|
||
// 1. 热门漫画最新更新
|
||
let updateSection = document.querySelector(".update-cont");
|
||
if (updateSection) {
|
||
let updateComics = [];
|
||
let uls = updateSection.querySelectorAll("ul");
|
||
for (let ul of uls) {
|
||
let comics = ul.querySelectorAll("li").map(e => this.parseSimpleComic(e)).filter(c => c);
|
||
updateComics.push(...comics);
|
||
}
|
||
if (updateComics.length > 0) {
|
||
parts.push({ title: "热门漫画最新更新", comics: updateComics });
|
||
}
|
||
}
|
||
|
||
// 2. tab 板块(热门连载漫画、经典完结漫画、最新上架漫画、2020新番漫画)
|
||
let tabTitles = document.querySelectorAll("#cmt-tab li");
|
||
let tabParts = document.querySelectorAll("#cmt-cont ul.cover-list");
|
||
for (let i = 0; i < tabTitles.length; i++) {
|
||
let title = tabTitles[i].text.trim();
|
||
let comics = tabParts[i].querySelectorAll("li").map(e => this.parseSimpleComic(e)).filter(c => c);
|
||
if (comics.length > 0) {
|
||
parts.push({ title, comics });
|
||
}
|
||
}
|
||
|
||
return parts;
|
||
},
|
||
loadNext(next) {},
|
||
},
|
||
];
|
||
|
||
// categories
|
||
category = {
|
||
/// title of the category page, used to identify the page, it should be unique
|
||
title: "漫画柜",
|
||
parts: [
|
||
{
|
||
name: "类型",
|
||
type: "fixed",
|
||
itemType: "category",
|
||
categories: [
|
||
"全部",
|
||
"热血",
|
||
"冒险",
|
||
"魔幻",
|
||
"神鬼",
|
||
"搞笑",
|
||
"萌系",
|
||
"爱情",
|
||
"科幻",
|
||
"魔法",
|
||
"格斗",
|
||
"武侠",
|
||
"机战",
|
||
"战争",
|
||
"竞技",
|
||
"体育",
|
||
"校园",
|
||
"生活",
|
||
"励志",
|
||
"历史",
|
||
"伪娘",
|
||
"宅男",
|
||
"腐女",
|
||
"耽美",
|
||
"百合",
|
||
"后宫",
|
||
"治愈",
|
||
"美食",
|
||
"推理",
|
||
"悬疑",
|
||
"恐怖",
|
||
"四格",
|
||
"职场",
|
||
"侦探",
|
||
"社会",
|
||
"音乐",
|
||
"舞蹈",
|
||
"杂志",
|
||
"黑道",
|
||
],
|
||
categoryParams: [
|
||
"",
|
||
"rexue",
|
||
"maoxian",
|
||
"mohuan",
|
||
"shengui",
|
||
"gaoxiao",
|
||
"mengxi",
|
||
"aiqing",
|
||
"kehuan",
|
||
"mofa",
|
||
"gedou",
|
||
"wuxia",
|
||
"jizhan",
|
||
"zhanzheng",
|
||
"jingji",
|
||
"tiyu",
|
||
"xiaoyuan",
|
||
"shenghuo",
|
||
"lizhi",
|
||
"lishi",
|
||
"weiniang",
|
||
"zhainan",
|
||
"funv",
|
||
"danmei",
|
||
"baihe",
|
||
"hougong",
|
||
"zhiyu",
|
||
"meishi",
|
||
"tuili",
|
||
"xuanyi",
|
||
"kongbu",
|
||
"sige",
|
||
"zhichang",
|
||
"zhentan",
|
||
"shehui",
|
||
"yinyue",
|
||
"wudao",
|
||
"zazhi",
|
||
"heidao",
|
||
],
|
||
},
|
||
],
|
||
// enable ranking page
|
||
enableRankingPage: false,
|
||
};
|
||
|
||
/// category comic loading related
|
||
categoryComics = {
|
||
/**
|
||
* load comics of a category
|
||
* @param category {string} - category name
|
||
* @param param {string?} - category param
|
||
* @param options {string[]} - options from optionList
|
||
* @param page {number} - page number
|
||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||
*/
|
||
load: async (category, param, options, page) => {
|
||
let area = options[0];
|
||
let genre = param;
|
||
let age = options[1];
|
||
let status = options[2];
|
||
let sort = options[3] || "index";
|
||
// log(
|
||
// "info",
|
||
// this.name,
|
||
// ` 加载分类漫画: ${area} | ${genre} | ${age} | ${status}`
|
||
// );
|
||
// 字符串之间用“_”连接,空字符串除外
|
||
let params = [area, genre, age, status].filter((e) => e != "").join("_");
|
||
|
||
let url = `${this.baseUrl}/list/${params}/${sort}_p${page}.html`;
|
||
|
||
let document = await this.getHtml(url);
|
||
let maxPage = document
|
||
.querySelector(".result-count")
|
||
.querySelectorAll("strong")[1].text;
|
||
maxPage = parseInt(maxPage);
|
||
let comics = document
|
||
.querySelectorAll("#contList > li")
|
||
.map((e) => this.parseSimpleComic(e))
|
||
.filter((comic) => comic !== null); // 过滤掉 null 值
|
||
return {
|
||
comics,
|
||
maxPage,
|
||
};
|
||
},
|
||
// provide options for category comic loading
|
||
optionList: [
|
||
{
|
||
options: [
|
||
"-全部",
|
||
"japan-日本",
|
||
"hongkong-港台",
|
||
"other-其它",
|
||
"europe-欧美",
|
||
"china-内地",
|
||
"korea-韩国",
|
||
],
|
||
},
|
||
{
|
||
options: [
|
||
"-全部",
|
||
"shaonv-少女",
|
||
"shaonian-少年",
|
||
"qingnian-青年",
|
||
"ertong-儿童",
|
||
"tongyong-通用",
|
||
],
|
||
},
|
||
{
|
||
options: ["-全部", "lianzai-连载", "wanjie-完结"],
|
||
},
|
||
{
|
||
options: ["update-最新更新", "index-最新发布", "view-人气最旺", "rate-评分最高"],
|
||
},
|
||
],
|
||
ranking: {
|
||
// 对于单个选项,使用“-”分隔值和文本,左侧为值,右侧为文本
|
||
options: [
|
||
"-最新发布",
|
||
"update-最新更新",
|
||
"view-人气最旺",
|
||
"rate-评分最高",
|
||
],
|
||
/**
|
||
* 加载排行榜漫画
|
||
* @param option {string} - 来自optionList的选项
|
||
* @param page {number} - 页码
|
||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||
*/
|
||
load: async (option, page) => {
|
||
let url = `${this.baseUrl}/list/${option}_p${page}.html`;
|
||
let document = await this.getHtml(url);
|
||
let maxPage = document
|
||
.querySelector(".result-count")
|
||
.querySelectorAll("strong")[1].text;
|
||
maxPage = parseInt(maxPage);
|
||
let comics = document
|
||
.querySelector("#contList")
|
||
.querySelectorAll("li")
|
||
.map((e) => this.parseComic(e));
|
||
return {
|
||
comics,
|
||
maxPage,
|
||
};
|
||
},
|
||
},
|
||
};
|
||
|
||
/**
|
||
* 专门解析搜索结果页面中的漫画信息
|
||
* @param {HTMLElement} item - 搜索结果中的单个漫画项
|
||
* @returns {Comic} - 解析后的漫画对象
|
||
*/
|
||
parseSearchComic(item) {
|
||
try {
|
||
// 获取漫画链接和ID
|
||
let linkElement = item.querySelector(".book-detail dl dt a");
|
||
if (!linkElement) return null;
|
||
|
||
let url = linkElement.attributes["href"];
|
||
let id = url.split("/")[2];
|
||
let title = linkElement.text.trim();
|
||
|
||
// 获取封面图片
|
||
let coverElement = item.querySelector(".book-cover .bcover img");
|
||
let cover = coverElement ? coverElement.attributes["src"] : null;
|
||
if (cover) {
|
||
cover = cover.startsWith("//") ? `https:${cover}` : cover;
|
||
}
|
||
|
||
// 获取更新状态和描述
|
||
let statusElement = item.querySelector(".tags.status span .red");
|
||
let status = statusElement ? statusElement.text.trim() : "";
|
||
|
||
let updateElement = item.querySelector(".tags.status span .red:nth-child(2)");
|
||
let updateTime = updateElement ? updateElement.text.trim() : "";
|
||
|
||
// 获取评分信息
|
||
let scoreElement = item.querySelector(".book-score .score-avg strong");
|
||
let score = scoreElement ? scoreElement.text.trim() : "";
|
||
|
||
// 获取作者信息
|
||
let authorElements = item.querySelectorAll(".tags a[href*='/author/']");
|
||
let author = authorElements.length > 0
|
||
? authorElements.map(a => a.text.trim()).join(", ")
|
||
: "";
|
||
|
||
// 获取类型信息
|
||
let typeElements = item.querySelectorAll(".tags a[href*='/list/']");
|
||
let types = typeElements.length > 0
|
||
? typeElements.map(a => a.text.trim())
|
||
: [];
|
||
|
||
// 获取简介
|
||
let introElement = item.querySelector(".intro span");
|
||
let description = introElement ? introElement.text.replace("简介:", "").trim() : "";
|
||
|
||
// 如果简介为空,使用更新状态作为描述
|
||
if (!description && status) {
|
||
description = `状态: ${status}`;
|
||
if (updateTime) description += `, 更新: ${updateTime}`;
|
||
}
|
||
|
||
return new Comic({
|
||
id,
|
||
title,
|
||
cover,
|
||
description,
|
||
tags: [...types, status],
|
||
author,
|
||
score
|
||
});
|
||
} catch (error) {
|
||
console.error("解析搜索结果项时出错:", error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// search related
|
||
search = {
|
||
/**
|
||
* load search result
|
||
* @param keyword {string}
|
||
* @param options {string[]} - options from optionList
|
||
* @param page {number}
|
||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||
*/
|
||
load: async (keyword, options, page) => {
|
||
let url = ""
|
||
if (options[0]) {
|
||
let type = options[0].split("-")[0];
|
||
if (type == '0') {
|
||
url = `${this.baseUrl}/s/${keyword}_p${page}.html`;
|
||
} else{
|
||
url = `${this.baseUrl}/s/${keyword}_o${type}_p${page}.html`;
|
||
}
|
||
}else{
|
||
url = `${this.baseUrl}/s/${keyword}_p${page}.html`;
|
||
}
|
||
let document = await this.getHtml(url);
|
||
|
||
// 检查是否有结果计数元素
|
||
let resultCount = document.querySelector(".result-count");
|
||
if (!resultCount) {
|
||
// 没有搜索结果或页面结构不同
|
||
return {
|
||
comics: [],
|
||
maxPage: 1
|
||
};
|
||
}
|
||
|
||
let comicNum = resultCount.querySelectorAll("strong")[1].text;
|
||
comicNum = parseInt(comicNum);
|
||
// 每页10个
|
||
let maxPage = Math.ceil(comicNum / 10);
|
||
|
||
// 在搜索结果页面中,漫画列表位于 .book-result ul 下
|
||
let comicList = document.querySelector(".book-result ul");
|
||
if (!comicList) {
|
||
return {
|
||
comics: [],
|
||
maxPage: maxPage || 1
|
||
};
|
||
}
|
||
|
||
// 使用专门的搜索解析函数解析每个漫画项
|
||
let comics = comicList.querySelectorAll("li.cf")
|
||
.map(item => this.parseSearchComic(item))
|
||
.filter(comic => comic !== null); // 过滤掉解析失败的项
|
||
|
||
return {
|
||
comics,
|
||
maxPage,
|
||
};
|
||
},
|
||
|
||
optionList: [
|
||
{
|
||
type: "select",
|
||
options: ["0-最新更新", "1-最近最热","2-最新上架", "3-评分最高"],
|
||
label: "sort",
|
||
default: null,
|
||
},
|
||
],
|
||
|
||
enableTagsSuggestions: false,
|
||
};
|
||
|
||
/// single comic related
|
||
comic = {
|
||
/**
|
||
* load comic info
|
||
* @param id {string}
|
||
* @returns {Promise<ComicDetails>}
|
||
*/
|
||
loadInfo: async (id) => {
|
||
let url = `${this.baseUrl}/comic/${id}/`;
|
||
let document = await this.getHtml(url);
|
||
// ANCHOR 基本信息
|
||
let book = document.querySelector(".book-cont");
|
||
let title = book
|
||
.querySelector(".book-title")
|
||
.querySelector("h1")
|
||
.text.trim();
|
||
let subtitle = book
|
||
.querySelector(".book-title")
|
||
.querySelector("h2")
|
||
.text.trim();
|
||
let cover = book.querySelector(".hcover").querySelector("img").attributes[
|
||
"src"
|
||
];
|
||
cover = `https:${cover}`;
|
||
let description = book
|
||
.querySelector("#intro-all")
|
||
.querySelectorAll("p")
|
||
.map((e) => e.text.trim())
|
||
.join("\n");
|
||
// log("warn", this.name, { title, subtitle, cover, description });
|
||
|
||
let detail_list = book.querySelectorAll(".detail-list span");
|
||
|
||
function parseDetail(idx) {
|
||
let ele = detail_list[idx].querySelectorAll("a");
|
||
if (ele.length > 0) {
|
||
return ele.map((e) => e.text.trim());
|
||
}
|
||
return [""];
|
||
}
|
||
let createYear = parseDetail(0);
|
||
let area = parseDetail(1);
|
||
let genre = parseDetail(3);
|
||
let author = parseDetail(4);
|
||
// let alias = parseDetail(5);
|
||
|
||
// let lastChapter = parseDetail(6);
|
||
let status = detail_list[7].text.trim();
|
||
|
||
let tags = {
|
||
年代: createYear,
|
||
状态: [status],
|
||
作者: author,
|
||
地区: area,
|
||
类型: genre,
|
||
};
|
||
let updateTime = detail_list[8].text.trim();
|
||
|
||
// ANCHOR 章节信息
|
||
// 支持多分组
|
||
let chaptersMap = new Map();
|
||
|
||
// 查找所有章节分组标题
|
||
let chapterGroups = document.querySelectorAll(".chapter h4 span");
|
||
|
||
// 处理每个分组
|
||
for (let i = 0; i < chapterGroups.length; i++) {
|
||
let groupName = chapterGroups[i].text.trim();
|
||
let groupChapters = new Map();
|
||
|
||
let chapterList = document.querySelectorAll(".chapter-list")[i];
|
||
if (chapterList) {
|
||
let lis = chapterList.querySelectorAll("li");
|
||
for (let li of lis) {
|
||
let a = li.querySelector("a");
|
||
let id = a.attributes["href"].split("/").pop().replace(".html", "");
|
||
let title = a.querySelector("span").text.trim();
|
||
groupChapters.set(id, title);
|
||
}
|
||
|
||
groupChapters = new Map([...groupChapters].sort((a, b) => a[0] - b[0]));
|
||
|
||
chaptersMap.set(groupName, groupChapters);
|
||
}
|
||
}
|
||
|
||
let chapters;
|
||
if (this.isAppVersionAfter && this.isAppVersionAfter("1.3.0")) {
|
||
chapters = chaptersMap;
|
||
} else {
|
||
chapters = new Map();
|
||
for (let [_, groupChapters] of chaptersMap) {
|
||
for (let [id, title] of groupChapters) {
|
||
chapters.set(id, title);
|
||
}
|
||
}
|
||
chapters = new Map([...chapters].sort((a, b) => a[0] - b[0]));
|
||
}
|
||
|
||
let recommend = [];
|
||
let similar = document.querySelector(".similar-list");
|
||
if (similar) {
|
||
let similar_list = similar.querySelectorAll("li");
|
||
for (let li of similar_list) {
|
||
let comic = this.parseSimpleComic(li);
|
||
recommend.push(comic);
|
||
}
|
||
}
|
||
|
||
return new ComicDetails({
|
||
title,
|
||
subtitle,
|
||
cover,
|
||
description,
|
||
tags,
|
||
updateTime,
|
||
chapters,
|
||
recommend,
|
||
});
|
||
},
|
||
|
||
/**
|
||
* load images of a chapter
|
||
* @param comicId {string}
|
||
* @param epId {string?}
|
||
* @returns {Promise<{images: string[]}>}
|
||
*/
|
||
loadEp: async (comicId, epId) => {
|
||
let url = `${this.baseUrl}/comic/${comicId}/${epId}.html`;
|
||
let document = await this.getHtml(url);
|
||
let script = document.querySelectorAll("script")[4].innerHTML;
|
||
let infos = this.getImgInfos(script);
|
||
|
||
let imgDomain = `https://us.hamreus.com`;
|
||
let images = [];
|
||
for (let f of infos.files) {
|
||
let imgUrl =
|
||
imgDomain + infos.path + f + `?e=${infos.sl.e}&m=${infos.sl.m}`;
|
||
images.push(imgUrl);
|
||
}
|
||
return {
|
||
images,
|
||
};
|
||
},
|
||
/**
|
||
* [Optional] provide configs for an image loading
|
||
* @param url
|
||
* @param comicId
|
||
* @param epId
|
||
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
|
||
*/
|
||
onImageLoad: (url, comicId, epId) => {
|
||
return {
|
||
headers: {
|
||
accept:
|
||
"image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
|
||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||
"cache-control": "no-cache",
|
||
pragma: "no-cache",
|
||
priority: "i",
|
||
"sec-ch-ua":
|
||
'"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
|
||
"sec-ch-ua-mobile": "?0",
|
||
"sec-ch-ua-platform": '"Windows"',
|
||
"sec-fetch-dest": "image",
|
||
"sec-fetch-mode": "no-cors",
|
||
"sec-fetch-site": "cross-site",
|
||
"sec-fetch-storage-access": "active",
|
||
Referer: "https://www.manhuagui.com/",
|
||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||
},
|
||
};
|
||
},
|
||
/**
|
||
* [Optional] provide configs for a thumbnail loading
|
||
* @param url {string}
|
||
* @returns {ImageLoadingConfig | Promise<ImageLoadingConfig>}
|
||
*
|
||
* `ImageLoadingConfig.modifyImage` and `ImageLoadingConfig.onLoadFailed` will be ignored.
|
||
* They are not supported for thumbnails.
|
||
*/
|
||
onThumbnailLoad: (url) => {
|
||
let headers = {
|
||
accept:
|
||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||
"cache-control": "no-cache",
|
||
pragma: "no-cache",
|
||
priority: "u=0, i",
|
||
"sec-ch-ua":
|
||
'"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
|
||
"sec-ch-ua-mobile": "?0",
|
||
"sec-ch-ua-platform": '"Windows"',
|
||
"sec-fetch-dest": "document",
|
||
"sec-fetch-mode": "navigate",
|
||
"sec-fetch-site": "none",
|
||
"sec-fetch-user": "?1",
|
||
"upgrade-insecure-requests": "1",
|
||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||
};
|
||
|
||
return {
|
||
headers,
|
||
};
|
||
},
|
||
|
||
/**
|
||
* [Optional] load comments
|
||
*
|
||
* Since app version 1.0.6, rich text is supported in comments.
|
||
* Following html tags are supported: ['a', 'b', 'i', 'u', 's', 'br', 'span', 'img'].
|
||
* span tag supports style attribute, but only support font-weight, font-style, text-decoration.
|
||
* All images will be placed at the end of the comment.
|
||
* Auto link detection is enabled, but only http/https links are supported.
|
||
* @param comicId {string}
|
||
* @param subId {string?} - ComicDetails.subId
|
||
* @param page {number}
|
||
* @param replyTo {string?} - commentId to reply, not null when reply to a comment
|
||
* @returns {Promise<{comments: Comment[], maxPage: number?}>}
|
||
*/
|
||
loadComments: async (comicId, subId, page, replyTo) => {
|
||
if(replyTo){
|
||
page = replyTo.split('//')[1];
|
||
replyTo = replyTo.split('//')[0];
|
||
}
|
||
|
||
let url = `${this.baseUrl}/tools/submit_ajax.ashx?action=comment_list&book_id=${comicId}&page_index=${page}`;
|
||
|
||
let headers = {
|
||
accept: "application/json, text/javascript, */*; q=0.01",
|
||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
||
"cache-control": "no-cache",
|
||
pragma: "no-cache",
|
||
"sec-ch-ua": '"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
|
||
"sec-ch-ua-mobile": "?0",
|
||
"sec-ch-ua-platform": '"Windows"',
|
||
"sec-fetch-dest": "empty",
|
||
"sec-fetch-mode": "cors",
|
||
"sec-fetch-site": "same-origin",
|
||
"x-requested-with": "XMLHttpRequest",
|
||
Referer: `${this.baseUrl}/comic/${comicId}/`,
|
||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||
};
|
||
|
||
let res = await Network.get(url, headers);
|
||
|
||
if (res.status !== 200) {
|
||
throw `获取评论失败,状态码: ${res.status}`;
|
||
}
|
||
|
||
let data = JSON.parse(res.body);
|
||
|
||
const replyChains = new Map();
|
||
const isSubReply = new Set();
|
||
const replyToMap = new Map();
|
||
|
||
if (data.commentIds && data.commentIds.length > 0) {
|
||
for (let commentIdString of data.commentIds) {
|
||
const commentIds = commentIdString.split(',');
|
||
if (commentIds.length > 1) {
|
||
const mainCommentId = commentIds[commentIds.length - 1];
|
||
|
||
if (!replyChains.has(mainCommentId)) {
|
||
replyChains.set(mainCommentId, []);
|
||
}
|
||
|
||
for (let i = 0; i < commentIds.length - 1; i++) {
|
||
const replyId = commentIds[i];
|
||
isSubReply.add(replyId); // 标记为子回复
|
||
|
||
if (!replyChains.get(mainCommentId).includes(replyId)) {
|
||
replyChains.get(mainCommentId).push(replyId);
|
||
}
|
||
|
||
const targetId = (i === 0) ? mainCommentId : commentIds[i + 1];
|
||
replyToMap.set(replyId, targetId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const commentList = [];
|
||
|
||
if (data.comments) {
|
||
if (replyTo) {
|
||
const replies = [...(replyChains.get(replyTo) || [])].reverse();
|
||
|
||
for (let replyId of replies) {
|
||
const comment = data.comments[replyId];
|
||
if (comment) {
|
||
const directReplyToId = replyToMap.get(replyId);
|
||
let replyUserName = "";
|
||
if (directReplyToId && directReplyToId !== replyTo && data.comments[directReplyToId]) {
|
||
replyUserName = data.comments[directReplyToId].user_name || "匿名用户";
|
||
}
|
||
|
||
commentList.push(new Comment({
|
||
id: `${comment.id}//${page}`,
|
||
userName: replyUserName ?
|
||
`${comment.user_name || "匿名用户"} ☞ ${replyUserName}` :
|
||
comment.user_name || "匿名用户",
|
||
avatar: comment.avatar ? `https:${comment.avatar}` : "https://cf.mhgui.com/images/default.png",
|
||
content: comment.content ? comment.content : "已隐藏评论",
|
||
time: comment.add_time,
|
||
replyCount: 0, // 回复的回复暂不支持
|
||
}));
|
||
}
|
||
}
|
||
} else {
|
||
const mainComments = [];
|
||
for (const [id, comment] of Object.entries(data.comments)) {
|
||
if (!isSubReply.has(id)) {
|
||
const replyCount = replyChains.has(id) ? replyChains.get(id).length : (comment.reply_count || 0);
|
||
mainComments.push(new Comment({
|
||
id: `${comment.id}//${page}`,
|
||
userName: comment.user_name || "匿名用户",
|
||
avatar: comment.avatar ? `https:${comment.avatar}` : "https://cf.mhgui.com/images/default.png",
|
||
content: comment.content ? comment.content : "已隐藏评论",
|
||
time: comment.add_time,
|
||
replyCount: replyCount,
|
||
}));
|
||
}
|
||
}
|
||
commentList.push(...mainComments.reverse());
|
||
}
|
||
}
|
||
|
||
return {
|
||
comments: commentList,
|
||
maxPage: replyTo ? 1 : (Math.ceil(data.total / 10) || 1)
|
||
};
|
||
},
|
||
|
||
/**
|
||
* 处理标签点击事件
|
||
* @param namespace {string} 标签命名空间
|
||
* @param tag {string} 标签名称
|
||
* @returns {Object} 跳转操作
|
||
*/
|
||
onClickTag: (namespace, tag) => {
|
||
// 点击类型标签时,跳转到对应的分类页面
|
||
if (namespace === "类型") {
|
||
// 根据标签查找对应的参数值
|
||
const categoryPart = this.category.parts.find(part => part.name === "类型");
|
||
if (categoryPart) {
|
||
const index = categoryPart.categories.findIndex(cat => cat === tag);
|
||
if (index !== -1) {
|
||
const param = categoryPart.categoryParams[index];
|
||
return {
|
||
action: 'category',
|
||
keyword: tag,
|
||
param: param
|
||
};
|
||
}
|
||
}
|
||
}
|
||
if (namespace === "作者") {
|
||
return {
|
||
action: 'search',
|
||
keyword: tag,
|
||
param: tag
|
||
};
|
||
}
|
||
|
||
// 默认返回null,表示不处理此类标签点击
|
||
return null;
|
||
},
|
||
};
|
||
/// favorites related
|
||
favorites = {
|
||
multiFolder: false,
|
||
/**
|
||
* load comics of the favorites
|
||
* @param page {number} - page number
|
||
* @param folder {string?} - folder name, unused for now
|
||
* @returns {Promise<{comics: Comic[], maxPage: number}>}
|
||
*/
|
||
loadComics: async (page, folder) => {
|
||
let mhg_cookie = this.loadData("mhg_cookie");
|
||
if (!mhg_cookie) {
|
||
throw "请先登录漫画柜账号";
|
||
}
|
||
let url = `${this.baseUrl}/user/book/shelf/${page}`;
|
||
let document = await this.getHtml(url);
|
||
let comicElements = document.querySelectorAll('.dy_content_li');
|
||
let comics = [];
|
||
for (let el of comicElements) {
|
||
let a = el.querySelector('.dy_img a');
|
||
if (!a) continue;
|
||
let href = a.attributes['href'];
|
||
let id = href.split('/')[2];
|
||
let img = a.querySelector('img');
|
||
let cover = img ? (img.attributes['src'] || img.attributes['data-src']) : '';
|
||
if (cover && !cover.startsWith('http')) cover = 'https:' + cover;
|
||
// dy_r 解析详细信息
|
||
let dy_r = el.querySelector('.dy_r');
|
||
let title = '';
|
||
let updateTitle = '';
|
||
let updateChapter = '';
|
||
let updateDate = '';
|
||
let lastReadChapter = '';
|
||
let lastReadDate = '';
|
||
if (dy_r) {
|
||
// 标题
|
||
let h3 = dy_r.querySelector('h3');
|
||
if (h3) {
|
||
let h3a = h3.querySelector('a');
|
||
if (h3a) title = h3a.text.trim();
|
||
}
|
||
// 更新内容
|
||
let pList = dy_r.querySelectorAll('p');
|
||
if (pList.length > 0) {
|
||
let updateP = pList[0];
|
||
let updateEm = updateP.querySelectorAll('em');
|
||
if (updateEm.length > 0) {
|
||
let chapterA = updateEm[0].querySelector('a');
|
||
if (chapterA) updateChapter = chapterA.text.trim();
|
||
updateDate = updateEm.length > 1 ? updateEm[1].text.trim() : '';
|
||
}
|
||
updateTitle = updateP.text.replace(/更新内容:/, '').trim();
|
||
}
|
||
// 最近阅读
|
||
if (pList.length > 1) {
|
||
let readP = pList[1];
|
||
let readEm = readP.querySelectorAll('em');
|
||
if (readEm.length > 0) {
|
||
let lastA = readEm[0].querySelector('a');
|
||
if (lastA) lastReadChapter = lastA.text.trim();
|
||
lastReadDate = readEm.length > 1 ? readEm[1].text.trim() : '';
|
||
}
|
||
}
|
||
}
|
||
// 兼容无dy_r时的title
|
||
if (!title) {
|
||
if (a.attributes['title']) {
|
||
title = a.attributes['title'];
|
||
} else {
|
||
title = a.text.trim();
|
||
}
|
||
}
|
||
// tags 信息
|
||
let tags = [];
|
||
if (updateChapter) tags.push(`更新:${updateChapter}`);
|
||
if (updateDate) tags.push(`更新日期:${updateDate}`);
|
||
if (lastReadChapter) tags.push(`最近阅读:${lastReadChapter}`);
|
||
if (lastReadDate) tags.push(`最近阅读时间:${lastReadDate}`);
|
||
comics.push(new Comic({
|
||
id,
|
||
title,
|
||
subTitle: updateChapter || updateTitle || '',
|
||
cover,
|
||
description: '',
|
||
tags,
|
||
}));
|
||
}
|
||
// 页码信息
|
||
let maxPage = 1;
|
||
// 优先用“共N记录”计算
|
||
let recordInfo = document.querySelector('.flickr.right span');
|
||
if (recordInfo) {
|
||
let match = recordInfo.text.match(/共(\d+)记录/);
|
||
if (match) {
|
||
let total = parseInt(match[1], 10);
|
||
maxPage = Math.ceil(total / 20);
|
||
}
|
||
} else {
|
||
// 兼容旧逻辑
|
||
let pageBtns = document.querySelectorAll('.page-btns a');
|
||
for (let btn of pageBtns) {
|
||
let num = parseInt(btn.text.trim(), 10);
|
||
if (!isNaN(num) && num > maxPage) maxPage = num;
|
||
}
|
||
}
|
||
return {
|
||
comics,
|
||
maxPage
|
||
};
|
||
},
|
||
addOrDelFavorite: async (comicId, folderId, isAdding, favoriteId) => {
|
||
if (!isAdding) {
|
||
throw '暂不支持取消收藏';
|
||
}
|
||
let mhg_cookie = this.loadData("mhg_cookie");
|
||
if (!mhg_cookie) {
|
||
throw "请先登录漫画柜账号";
|
||
}
|
||
let url = `${this.baseUrl}/tools/submit_ajax.ashx?action=user_book_shelf_add`;
|
||
let headers = {
|
||
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||
'x-requested-with': 'XMLHttpRequest',
|
||
'referer': `${this.baseUrl}/comic/${comicId}/`,
|
||
cookie: mhg_cookie,
|
||
};
|
||
let body = `book_id=${encodeURIComponent(comicId)}`;
|
||
let res = await Network.post(url, headers, body);
|
||
if (res.status !== 200) {
|
||
throw `添加收藏失败,状态码: ${res.status}`;
|
||
}
|
||
let data = {};
|
||
try {
|
||
data = JSON.parse(res.body);
|
||
} catch (e) {}
|
||
if (data.state !== true && data.state !== 1) {
|
||
throw data.msg || '添加收藏失败';
|
||
}
|
||
return 'ok';
|
||
},
|
||
};
|
||
}
|