下载祈愿数据

close #202
This commit is contained in:
BTMuli
2026-01-12 01:05:11 +08:00
parent 8d541891ae
commit 62052d126f
11 changed files with 258 additions and 37 deletions

View File

@@ -95,6 +95,7 @@
"html2canvas": "^1.4.1",
"js-md5": "^0.8.3",
"jsencrypt": "^3.5.4",
"json-bigint": "^1.0.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"qrcode.vue": "^3.6.0",
@@ -118,6 +119,7 @@
"@tauri-apps/cli": "2.9.6",
"@types/fs-extra": "^11.0.4",
"@types/js-md5": "^0.8.0",
"@types/json-bigint": "^1.0.4",
"@types/node": "^25.0.6",
"@typescript-eslint/parser": "^8.52.0",
"@typescript/native-preview": "7.0.0-dev.20260109.1",

23
pnpm-lock.yaml generated
View File

@@ -83,6 +83,9 @@ importers:
jsencrypt:
specifier: ^3.5.4
version: 3.5.4
json-bigint:
specifier: ^1.0.0
version: 1.0.0
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
@@ -147,6 +150,9 @@ importers:
'@types/js-md5':
specifier: ^0.8.0
version: 0.8.0
'@types/json-bigint':
specifier: ^1.0.4
version: 1.0.4
'@types/node':
specifier: ^25.0.6
version: 25.0.6
@@ -1461,6 +1467,9 @@ packages:
'@types/js-md5@0.8.0':
resolution: {integrity: sha512-gQkc1Felhyj+aB9jmz/ICLm1fDPQx7l/60JIBSSEC+j09JeaINlzd0Wj9LZlQkHnV5rJYkroOHE5wdbDgADJrw==}
'@types/json-bigint@1.0.4':
resolution: {integrity: sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1826,6 +1835,9 @@ packages:
resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==}
hasBin: true
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -2815,6 +2827,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -5496,6 +5511,8 @@ snapshots:
'@types/js-md5@0.8.0': {}
'@types/json-bigint@1.0.4': {}
'@types/json-schema@7.0.15': {}
'@types/json5@0.0.29': {}
@@ -5986,6 +6003,8 @@ snapshots:
baseline-browser-mapping@2.9.14: {}
bignumber.js@9.3.1: {}
binary-extensions@2.3.0: {}
birpc@2.9.0: {}
@@ -7096,6 +7115,10 @@ snapshots:
jsesc@3.1.0: {}
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.1
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1: {}

View File

@@ -2,7 +2,7 @@
<template>
<TOverlay v-model="visible" blur-val="5px">
<div class="ugo-hutao-du">
<div class="ugo-hd-title">请选择要{{ props.mode === "upload" ? "上传" : "下载" }}的数据</div>
<div class="ugo-hd-title">请选择要{{ title }}的数据</div>
<v-progress-circular v-if="loading" color="var(--tgc-od-blue)" indeterminate />
<v-item-group v-else v-model="selectedUid" :multiple="true" class="ugo-hd-list">
<v-item
@@ -38,12 +38,13 @@ import hutao from "@Hutao/index.js";
import TSUserGacha from "@Sqlm/userGacha.js";
import useHutaoStore from "@store/hutao.js";
import { storeToRefs } from "pinia";
import { ref, shallowRef, watch } from "vue";
import { computed, ref, shallowRef, watch } from "vue";
export type UgoHutaoMode = "download" | "upload" | "delete";
type UgoHutaoDuUid = { uid: string; cnt: number };
type UgoHutaoDuProps = { mode: "download" | "upload" };
type UgoHutaoDuEmits = (e: "selected", v: Array<string>, m: boolean) => void;
type UgoHutaoDuProps = { mode: UgoHutaoMode };
type UgoHutaoDuEmits = (e: "selected", v: Array<string>, m: UgoHutaoMode) => void;
const visible = defineModel<boolean>();
const emits = defineEmits<UgoHutaoDuEmits>();
@@ -52,6 +53,18 @@ const props = defineProps<UgoHutaoDuProps>();
const loading = ref<boolean>(false);
const uidList = shallowRef<Array<UgoHutaoDuUid>>([]);
const selectedUid = shallowRef<Array<string>>([]);
const title = computed<string>(() => {
switch (props.mode) {
case "upload":
return "上传";
case "download":
return "下载";
case "delete":
return "删除";
default:
return "";
}
});
const hutaoStore = useHutaoStore();
const { accessToken, isLogin } = storeToRefs(hutaoStore);
@@ -63,10 +76,10 @@ watch(
loading.value = true;
selectedUid.value = [];
uidList.value = [];
if (props.mode == "download") {
await loadDownload();
} else {
if (props.mode == "upload") {
await loadUpload();
} else {
await loadDownload();
}
loading.value = false;
}
@@ -105,7 +118,7 @@ function handleSelected(): void {
showSnackbar.warn("请选择至少一个UID");
return;
}
emits("selected", selectedUid.value, props.mode === "upload");
emits("selected", selectedUid.value, props.mode);
visible.value = false;
}
</script>

View File

@@ -44,6 +44,15 @@
>
下载
</v-btn>
<v-btn
:disabled="!isLoginHutao"
class="gacha-top-btn"
prepend-icon="mdi-delete"
variant="elevated"
@click="tryDeleteGacha()"
>
删除
</v-btn>
</div>
</div>
</template>
@@ -156,7 +165,7 @@ import GroHistory from "@comp/userGacha/gro-history.vue";
import GroIframe from "@comp/userGacha/gro-iframe.vue";
import GroOverview from "@comp/userGacha/gro-overview.vue";
import GroTable from "@comp/userGacha/gro-table.vue";
import UgoHutaoDu from "@comp/userGacha/ugo-hutao-du.vue";
import UgoHutaoDu, { type UgoHutaoMode } from "@comp/userGacha/ugo-hutao-du.vue";
import UgoUid from "@comp/userGacha/ugo-uid.vue";
import hutao from "@Hutao/index.js";
import hk4eReq from "@req/hk4eReq.js";
@@ -175,7 +184,7 @@ import { storeToRefs } from "pinia";
import { onMounted, ref, shallowRef, watch } from "vue";
import { useRouter } from "vue-router";
import { AppCharacterData, AppWeaponData } from "@/data/index.js";
import { AppCalendarData, AppCharacterData, AppWeaponData } from "@/data/index.js";
const router = useRouter();
const hutaoStore = useHutaoStore();
@@ -190,7 +199,7 @@ const tab = ref<string>("overview");
const ovShow = ref<boolean>(false);
const hutaoShow = ref<boolean>(false);
const ovMode = ref<"export" | "import">("import");
const htMode = ref<"download" | "upload">("download");
const htMode = ref<UgoHutaoMode>("download");
const selectItem = shallowRef<Array<string>>([]);
const gachaListCur = shallowRef<Array<TGApp.Sqlite.Gacha.Gacha>>([]);
const hakushiData = shallowRef<Array<TGApp.Plugins.Hakushi.ConvertData>>([]);
@@ -243,9 +252,24 @@ async function tryDownloadGacha(): Promise<void> {
hutaoShow.value = true;
}
async function handleHutaoDu(uids: Array<string>, isUpload: boolean): Promise<void> {
if (isUpload) await handleHutaoUpload(uids);
else await handleHutaoDownload(uids);
async function tryDeleteGacha(): Promise<void> {
if (!isLoginHutao.value) return;
htMode.value = "delete";
hutaoShow.value = true;
}
async function handleHutaoDu(uids: Array<string>, mode: UgoHutaoMode): Promise<void> {
switch (mode) {
case "download":
await handleHutaoDownload(uids);
break;
case "upload":
await handleHutaoUpload(uids);
break;
case "delete":
await handleHutaoDelete(uids);
break;
}
}
async function handleHutaoUpload(uids: Array<string>): Promise<void> {
@@ -284,7 +308,7 @@ async function handleHutaoUpload(uids: Array<string>): Promise<void> {
QueryType: Number(i.uigfType),
ItemId: Number(i.itemId),
Time: str2timeStr(i.time),
Id: BigInt(i.id),
Id: i.id.toString(),
})),
};
const resp = await hutao.Gacha.upload(accessToken.value!, data);
@@ -298,8 +322,124 @@ async function handleHutaoUpload(uids: Array<string>): Promise<void> {
}
async function handleHutaoDownload(uids: Array<string>): Promise<void> {
console.log(uids);
// TODO:implement download gacha logs
if (uids.length === 0) {
showSnackbar.warn("没有选中的UID");
return;
}
if (!isLoginHutao.value) {
showSnackbar.warn("未登录胡桃云账号");
return;
}
if (!userInfo.value) {
await hutaoStore.tryRefreshInfo();
if (!userInfo.value) {
showSnackbar.warn("未检测到胡桃云用户信息");
return;
}
}
await showLoading.start("正在下载胡桃云祈愿记录...", "正在刷新Token");
for (const u of uids) {
await showLoading.start(`正在下载UID:${u}的祈愿记录`, "正在获取EndIds");
const endIdResp = await hutao.Gacha.endIds(accessToken.value!, u);
if ("retcode" in endIdResp) {
showSnackbar.warn(`[${endIdResp.retcode}] ${endIdResp.message}`);
continue;
}
for (const [p, i] of Object.entries(endIdResp)) {
if (i === 0) continue;
let endId: string | undefined = undefined;
let flag = true;
const pageSize = 200;
await showLoading.start(`正在下载卡池 ${p}`);
const uigfList: Array<TGApp.Plugins.UIGF.GachaItem> = [];
while (flag) {
await showLoading.update(`EndId:${endId ?? "无"}`);
await hutaoStore.tryRefreshToken();
const gachaResp = await hutao.Gacha.logs(accessToken.value!, u, Number(p), pageSize, endId);
if (gachaResp.retcode !== 0) {
showSnackbar.warn(`[${gachaResp.retcode}] ${gachaResp.message}`);
break;
}
const data: TGApp.Plugins.Hutao.Gacha.GachaLogRes = gachaResp.data ?? [];
if (data.length === pageSize) {
endId = data[data.length - 1].Id.toString();
} else flag = false;
for (const item of data) {
const tempItem: TGApp.Plugins.UIGF.GachaItem = {
gacha_type: item.GachaType.toString(),
item_id: item.ItemId.toString(),
count: "1",
time: item.Time,
name: "",
item_type: "",
rank_type: "",
id: BigInt(item.Id).toString(),
uigf_gacha_type: item.QueryType.toString(),
};
const find = AppCalendarData.find((i) => i.id.toString() === item.ItemId.toString());
if (find) {
tempItem.name = find.name;
tempItem.item_type = find.itemType;
tempItem.rank_type = find.star.toString();
} else {
if (hakushiData.value.length === 0) {
await showLoading.update(
`未查找到 ${tempItem.item_id} 的 信息,正在获取 Hakushi 数据`,
);
await loadHakushi();
}
const findH = hakushiData.value.find((i) => i.id.toString() === item.ItemId.toString());
if (findH) {
tempItem.name = findH.name;
tempItem.item_type = findH.type;
tempItem.rank_type = findH.star.toString();
} else {
showSnackbar.warn(`无法搜索到 ${item.ItemId} 的信息,请等待元数据更新`);
continue;
}
}
uigfList.push(tempItem);
}
}
await showLoading.start(`正在写入卡池 ${p}-${uigfList.length}`);
await TSUserGacha.mergeUIGF(u, uigfList, true);
}
}
await showLoading.end();
showSnackbar.success("成功下载,即将刷新页面");
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
window.location.reload();
}
async function handleHutaoDelete(uids: Array<string>): Promise<void> {
if (uids.length === 0) {
showSnackbar.warn("没有选中的UID");
return;
}
if (!isLoginHutao.value) {
showSnackbar.warn("未登录胡桃云账号");
return;
}
if (!userInfo.value) {
await hutaoStore.tryRefreshInfo();
if (!userInfo.value) {
showSnackbar.warn("未检测到胡桃云用户信息");
return;
}
}
const check = await showDialog.check("确定删除?", uids.join("、"));
if (!check) return;
await showLoading.start("正在删除胡桃云祈愿记录");
for (const u of uids) {
await showLoading.update(`UID:${u}`);
const deleteResp = await hutao.Gacha.delete(accessToken.value!, u);
if (deleteResp.retcode === 0) {
showSnackbar.success(`删除记录成功:${deleteResp.message}`);
} else {
showSnackbar.warn(`[${deleteResp.retcode}] ${deleteResp.message}`);
}
}
await showLoading.end();
}
async function reloadUid(): Promise<void> {

View File

@@ -14,7 +14,13 @@ import {
} from "./request/abyssReq.js";
import { getUserInfo, loginPassport, refreshToken } from "./request/accountReq.js";
import { getCombatStatistic, uploadCombatData } from "./request/combatReq.js";
import { getEndIds, getEntries, getGachaLogs, uploadGachaLogs } from "./request/gachaReq.js";
import {
deleteGachaLogs,
getEndIds,
getEntries,
getGachaLogs,
uploadGachaLogs,
} from "./request/gachaReq.js";
import { transAbyssAvatars, transAbyssLocal } from "./utils/abyssUtil.js";
import { transCombatLocal } from "./utils/combatUtil.js";
@@ -62,7 +68,7 @@ const Hutao = {
endIds: getEndIds,
logs: getGachaLogs,
upload: uploadGachaLogs,
delete: _,
delete: deleteGachaLogs,
},
};

View File

@@ -41,6 +41,7 @@ export async function getEndIds(
method: "GET",
headers: header,
query: { Uid: uid },
hasBigInt: true,
});
if (resp.retcode !== 0) return <TGApp.Plugins.Hutao.Base.Resp>resp;
return <TGApp.Plugins.Hutao.Gacha.EndIdRes>resp.data;
@@ -59,24 +60,23 @@ export async function getGachaLogs(
tk: string,
uid: string,
gType: number,
endId: number,
count: number,
): Promise<TGApp.Plugins.Hutao.Gacha.GachaLogRes | TGApp.Plugins.Hutao.Base.Resp> {
endId: string | undefined = undefined,
): Promise<TGApp.Plugins.Hutao.Gacha.GachaLogResp> {
const url = `${GachaUrl}LimitedRetrieve`;
const header = await getReqHeader(tk);
const params = {
const params: Record<string, string | number> = {
uid: uid,
configType: gType,
endId: endId,
count: count,
};
const resp = await TGHttp<TGApp.Plugins.Hutao.Gacha.GachaLogResp>(url, {
if (endId) params.endId = endId;
return await TGHttp<TGApp.Plugins.Hutao.Gacha.GachaLogResp>(url, {
method: "GET",
headers: header,
query: params,
hasBigInt: true,
});
if (resp.retcode !== 0) return <TGApp.Plugins.Hutao.Base.Resp>resp;
return <TGApp.Plugins.Hutao.Gacha.GachaLogRes>resp.data;
}
/**
@@ -89,12 +89,32 @@ export async function getGachaLogs(
export async function uploadGachaLogs(
tk: string,
data: TGApp.Plugins.Hutao.Gacha.UploadData,
): Promise<TGApp.Plugins.Hutao.Gacha.UploadResp | TGApp.Plugins.Hutao.Base.Resp> {
): Promise<TGApp.Plugins.Hutao.Gacha.UploadResp> {
const url = `${GachaUrl}Upload`;
const header = await getReqHeader(tk);
return await TGHttp<TGApp.Plugins.Hutao.Gacha.UploadResp>(url, {
method: "POST",
headers: header,
body: JSON.stringify(data, (_, v) => (typeof v === "bigint" ? v.toString() : v)),
body: JSON.stringify(data),
});
}
/**
* 删除祈愿记录
* @since Beta v0.9.1
* @param tk - token
* @param uid - uid
* @returns 删除结果
*/
export async function deleteGachaLogs(
tk: string,
uid: string,
): Promise<TGApp.Plugins.Hutao.Gacha.DeleteResp> {
const url = `${GachaUrl}Delete`;
const header = await getReqHeader(tk);
return await TGHttp<TGApp.Plugins.Hutao.Gacha.DeleteResp>(url, {
method: "GET",
headers: header,
query: { Uid: uid },
});
}

View File

@@ -83,7 +83,7 @@ declare namespace TGApp.Plugins.Hutao.Gacha {
*/
Time: string;
/** Id */
Id: number | bigint;
Id: string | bigint;
};
/**
@@ -102,4 +102,10 @@ declare namespace TGApp.Plugins.Hutao.Gacha {
/** 数据 */
Items: Array<GachaLog>;
};
/**
* 删除响应
* @since Beta v0.9.1
*/
type DeleteResp = TGApp.Plugins.Hutao.Base.Resp<string>;
}

View File

@@ -79,9 +79,7 @@ const useHutaoStore = defineStore(
}
async function tryRefreshInfo(): Promise<void> {
if (!checkIsValid()) {
await tryRefreshToken();
}
await tryRefreshToken();
const resp = await hutao.Account.info(accessToken.value!);
if ("retcode" in resp) {
showSnackbar.warn(`刷新用户信息失败:${resp.retcode}-${resp.message}`);
@@ -96,6 +94,7 @@ const useHutaoStore = defineStore(
showSnackbar.warn("未找到胡桃云RefreshToken");
return;
}
if (checkIsValid()) return;
try {
const resp = await hutao.Token.refresh(refreshToken.value);
if ("retcode" in resp) {

View File

@@ -82,11 +82,16 @@ declare namespace TGApp.Plugins.Hakushi {
/**
* 转换后的数据
* @since Beta v0.9.0
* @since Beta v0.9.1
*/
type ConvertData = {
/** ID */
id: string;
/** 名称 */
name: string;
/** 类型 */
type: "武器" | "角色";
/** 星级 */
star: number;
};
}

View File

@@ -30,17 +30,17 @@ async function fetchWeapon(): Promise<TGApp.Plugins.Hakushi.WeaponResp> {
/**
* 转换数据
* @since Beta v0.9.0
* @since Beta v0.9.1
*/
async function fetchJson(): Promise<Array<TGApp.Plugins.Hakushi.ConvertData>> {
const jsonW = await fetchWeapon();
const jsonA = await fetchAvatar();
const res: Array<TGApp.Plugins.Hakushi.ConvertData> = [];
for (const [id, data] of Object.entries(jsonW)) {
res.push({ id: id.toString(), name: data.CHS, type: "武器" });
res.push({ id: id.toString(), name: data.CHS, type: "武器", star: data.rank });
}
for (const [id, data] of Object.entries(jsonA)) {
res.push({ id: id.toString(), name: data.CHS, type: "角色" });
res.push({ id: id.toString(), name: data.CHS, type: "角色", star: data.rank });
}
return res;
}

View File

@@ -4,6 +4,7 @@
*/
import { type ClientOptions, fetch } from "@tauri-apps/plugin-http";
import JSONBig from "json-bigint";
import TGLogger from "./TGLogger.js";
@@ -22,6 +23,8 @@ type TGHttpParams = {
body?: string;
/** 是否是Blob */
isBlob?: boolean;
/** 是否有BigInt */
hasBigInt?: boolean;
};
/**
@@ -65,7 +68,11 @@ async function TGHttp<T>(
return await fetch(url, fetchOptions)
.then(async (res) => {
if (res.ok) {
const data = options.isBlob ? await res.arrayBuffer() : await res.json();
const data = options.isBlob
? await res.arrayBuffer()
: options.hasBigInt
? JSONBig.parse(await res.text())
: await res.json();
if (fullResponse) return { data, resp: res };
return data;
}