Files
TeyvatGuide/src/components/userScripts/tus-mission.vue
2026-04-15 20:04:39 +08:00

512 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 米游币任务 -->
<template>
<div ref="tusmRef" class="tusm-box">
<div class="tusm-top">
<div class="tusm-title" @click="genShare()">
米游币任务({{ todayPoints }}/{{ totalPoints }})
</div>
<div class="tusm-acts" data-html2canvas-ignore>
<v-btn :loading="loadState" class="tusm-btn" @click="tryRefresh()">刷新</v-btn>
<v-btn :loading="loadMission" class="tusm-btn" @click="tryAuto()">执行</v-btn>
</div>
</div>
<div class="tusm-mid">
<div class="tusm-total">
<span>持有米游币</span>
<span>{{ userPoints }}</span>
</div>
<div class="tusm-switch-box" data-html2canvas-ignore>
<span>{{ cancelLike ? "点赞后取消" : "直接点赞" }}</span>
<v-switch v-model="cancelLike" class="tusm-switch" color="var(--tgc-od-red)" />
</div>
</div>
<div class="tusm-content">
<div v-for="mission in parseMissions" :key="mission.id" class="mission-item">
<div class="left">
<v-icon v-if="!mission.status" color="var(--tgc-od-white)">mdi-circle</v-icon>
<v-icon v-else color="var(--tgc-od-green)">mdi-checkbox-marked-circle-outline</v-icon>
<span>{{ mission.name }} - {{ mission.reward }}米游币</span>
<span v-if="mission.cycleTimes"> - Day{{ mission.cycleTimes }}</span>
</div>
<div class="right">
<span>
<v-progress-linear
:model-value="(mission.process / mission.total) * 100"
color="var(--tgc-od-blue)"
height="8"
rounded
/>
</span>
<span>{{ mission.process }}/{{ mission.total }}</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import apiHubReq from "@req/apiHubReq.js";
import miscReq from "@req/miscReq.js";
import painterReq from "@req/painterReq.js";
import postReq from "@req/postReq.js";
import useAppStore from "@store/app.js";
import TGLogger from "@utils/TGLogger.js";
import TGNotify from "@utils/TGNotify.js";
import { postDetailRateLimiter } from "@utils/rateLimiter.js";
import { storeToRefs } from "pinia";
import { ref, shallowRef, useTemplateRef, watch } from "vue";
import TGHttps from "@utils/TGHttps.js";
import { generateShareImg } from "@utils/TGShare.js";
/** 用于渲染的任务项 */
type ParseMission = {
/** 任务ID */
id: number;
/** 任务Key */
key: string;
/** 任务名称 */
name: string;
/** 任务进度 */
process: number;
/** 任务总进度 */
total: number;
/** 是否完成任务 */
status: boolean;
/** 米游币奖励 */
reward: number;
/** 完成次数 */
cycleTimes?: number;
};
/** 任务组件参数 */
type TusMissionProps = {
/** 米社账号 */
acCur: TGApp.App.Account.User | undefined;
};
const { cancelLike } = storeToRefs(useAppStore());
const loadScript = defineModel<boolean>();
const props = defineProps<TusMissionProps>();
const todayPoints = ref<number>(0);
const totalPoints = ref<number>(0);
const userPoints = ref<number>(0);
const loadState = ref<boolean>(false);
const loadMission = ref<boolean>(false);
const parseMissions = shallowRef<Array<ParseMission>>([]);
const missionList = shallowRef<Array<TGApp.BBS.Mission.MissionItem>>([]);
const tusmEl = useTemplateRef<HTMLDivElement>("tusmRef");
defineExpose({ tryAuto });
watch(
() => props.acCur,
() => {
todayPoints.value = 0;
totalPoints.value = 0;
userPoints.value = 0;
parseMissions.value = [];
missionList.value = [];
},
);
function mergeMission(
list: Array<TGApp.BBS.Mission.MissionItem>,
state: Array<TGApp.BBS.Mission.StateItem>,
): void {
const res: Array<ParseMission> = [];
for (const item of list) {
const stateFind = state.find((i) => i.mission_id === item.id);
if (!stateFind) {
res.push({
id: item.id,
key: item.mission_key,
name: item.name,
process: 0,
total: item.threshold,
status: false,
reward: item.points,
cycleTimes: item.continuous_cycle_times === 0 ? undefined : item.continuous_cycle_times,
});
continue;
}
res.push({
id: item.id,
key: item.mission_key,
name: item.name,
total: item.threshold,
process: stateFind.happened_times,
status: stateFind.process === 1,
reward: item.points,
cycleTimes: item.continuous_cycle_times === 0 ? undefined : item.continuous_cycle_times,
});
}
res.sort((a, b) => a.id - b.id);
parseMissions.value = res;
}
async function tryRefresh(): Promise<void> {
if (!props.acCur) {
showSnackbar.warn("未检测到当前账号数据");
return;
}
if (loadScript.value) {
showSnackbar.warn("任务正在执行中,请稍后再试");
return;
}
loadScript.value = true;
loadState.value = true;
await TGLogger.ScriptSep("米游币任务");
await TGLogger.Script("[米游币任务]刷新任务状态");
await refreshState(props.acCur.cookie);
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadState.value = false;
}
async function tryAuto(skip: boolean = false): Promise<void> {
if (!props.acCur) {
showSnackbar.warn("未检测到当前账号数据");
return;
}
if (loadScript.value) {
showSnackbar.warn("任务正在执行中,请稍后再试");
return;
}
loadScript.value = true;
loadMission.value = true;
await TGLogger.ScriptSep("米游币任务");
await TGLogger.Script("[米游币任务]开始执行任务");
await refreshState(props.acCur.cookie);
if (parseMissions.value.length === 0 || missionList.value.length === 0) {
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadMission.value = false;
return;
}
await autoSign(props.acCur.cookie, skip);
const postFilter = parseMissions.value.filter((i) => i.key !== "continuous_sign");
if (postFilter.every((i) => i.status)) {
await TGLogger.Script("[米游币任务]所有任务已完成");
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadMission.value = false;
return;
}
let isShare = false;
let likeCnt = 0;
let viewCnt = 0;
const shareFind = postFilter.find((i) => i.key === "share_post_0");
if (shareFind) isShare = shareFind.status;
const likeFind = postFilter.find((i) => i.key === "post_up_0");
if (likeFind) likeCnt = likeFind.process;
const viewFind = postFilter.find((i) => i.key === "view_post_0");
if (viewFind) viewCnt = viewFind.process;
// 获取帖子列表
await TGLogger.Script("[米游币任务]获取帖子列表");
let listResp: TGApp.BBS.Forum.PostForumResp | undefined;
try {
listResp = await painterReq.forum.recent(26, 2, 2, undefined, 20);
if (listResp.retcode !== 0) {
showSnackbar.error(`[${listResp.retcode}] ${listResp.message}`);
await TGLogger.Script(`获取帖子列表失败: [${listResp.retcode}] ${listResp.message}`, "warn");
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadMission.value = false;
return;
}
} catch (e) {
const errMsg = TGHttps.getErrMsg(e);
showSnackbar.error(`获取帖子列表失败:${errMsg}`);
await TGLogger.Script(`获取帖子列表失败`, "error");
await TGLogger.Error(`[tus-mission][tryAuto] ${e}`);
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadMission.value = false;
return;
}
if (!listResp) return;
// 执行操作
const ckShare = {
stoken: props.acCur.cookie.stoken,
stuid: props.acCur.cookie.stuid,
mid: props.acCur.cookie.mid,
};
const ckPost = { ltoken: props.acCur.cookie.ltoken, ltuid: props.acCur.cookie.ltuid };
for (const post of listResp.data.list) {
if (!isShare) {
await TGLogger.Script(`[米游币任务]正在分享帖子${post.post.post_id}`);
try {
const shareResp = await apiHubReq.post.share(post.post.post_id, ckShare);
if (shareResp.retcode === 0) {
await TGLogger.Script("[米游币任务]分享成功");
isShare = true;
} else {
await TGLogger.Script(`[米游币任务]分享失败:${shareResp.retcode} ${shareResp.message}`);
}
} catch (e) {
await TGLogger.Script(`[米游币任务]分享异常:${TGHttps.getErrMsg(e)}`, "error");
}
}
if (likeCnt < 5 || viewCnt < 3) {
const currentCount = postDetailRateLimiter.getRequestCount();
await TGLogger.Script(
`[米游币任务]正在浏览帖子${post.post.post_id} (当前 1 分钟内请求数:${currentCount}/10)`,
);
let detailResp: TGApp.BBS.Post.FullResp | undefined;
try {
detailResp = await postDetailRateLimiter.execute(() =>
postReq.post(post.post.post_id, ckPost),
);
if (detailResp.retcode !== 0) {
await TGLogger.Script(
`[米游币任务]获取帖子${post.post.post_id}失败:${detailResp.retcode} ${detailResp.message}`,
"warn",
);
continue;
}
} catch (e) {
const errMsg = TGHttps.getErrMsg(e);
await TGLogger.Script(`[米游币任务]获取帖子${post.post.post_id}异常:${errMsg}`, "error");
continue;
}
viewCnt++;
if (likeCnt < 5) {
const isLike = (detailResp.data.post.self_operation?.upvote_type ?? 0) > 0;
if (isLike) {
await TGLogger.Script(`[米游币任务]帖子${post.post.post_id}已点赞,跳过`);
continue;
}
await TGLogger.Script(`[米游币任务]正在点赞帖子${post.post.post_id}`);
try {
const likeResp = await apiHubReq.post.like(post.post.post_id, ckPost);
if (likeResp.retcode === 0) {
await TGLogger.Script("[米游币任务]点赞成功");
likeCnt++;
} else {
await TGLogger.Script(`[米游币任务]点赞失败:${likeResp.retcode} ${likeResp.message}`);
continue;
}
} catch (e) {
await TGLogger.Script(`[米游币任务]点赞异常:${TGHttps.getErrMsg(e)}`, "error");
continue;
}
if (cancelLike.value) {
await TGLogger.Script(`[米游币任务]正在取消点赞帖子${post.post.post_id}`);
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
try {
const unlikeResp = await apiHubReq.post.like(post.post.post_id, ckPost, true);
if (unlikeResp.retcode === 0) {
await TGLogger.Script("[米游币任务]取消点赞成功");
} else {
await TGLogger.Script(
`[米游币任务]取消点赞失败:${unlikeResp.retcode} ${unlikeResp.message}`,
);
}
} catch (e) {
await TGLogger.Script(`[米游币任务]取消点赞异常:${TGHttps.getErrMsg(e)}`, "error");
}
}
}
}
if (isShare && likeCnt >= 5 && viewCnt >= 3) {
await TGLogger.Script("[米游币任务]所有任务已完成");
break;
}
}
await TGLogger.Script("[米游币任务]任务执行完毕,即将刷新任务状态");
await refreshState(props.acCur.cookie);
await TGLogger.ScriptSep("米游币任务", false);
loadScript.value = false;
loadMission.value = false;
}
async function refreshState(ck: TGApp.App.Account.Cookie): Promise<void> {
const ckState = { cookie_token: ck.cookie_token, account_id: ck.account_id };
await TGLogger.Script("[米游币任务]刷新任务状态");
if (missionList.value.length === 0) {
await TGLogger.Script("[米游币任务]未检测到任务列表,正在获取");
try {
const listResp = await apiHubReq.mission.list(ckState);
console.log("米游币任务列表", listResp);
if (listResp.retcode !== 0) {
await TGLogger.Script(
`[米游币任务]获取任务列表失败:${listResp.retcode} ${listResp.message}`,
"warn",
);
showSnackbar.error(`[${listResp.retcode}] ${listResp.message}`);
return;
}
missionList.value = listResp.data.missions;
await TGLogger.Script("[米游币任务]获取任务列表成功");
} catch (e) {
await TGLogger.Script(`[米游币任务]获取任务列表异常:${TGHttps.getErrMsg(e)}`, "error");
return;
}
}
await TGLogger.Script("[米游币任务]正在获取任务状态");
try {
const stateResp = await apiHubReq.mission.state(ckState);
console.log("米游币任务状态", stateResp);
if (stateResp.retcode !== 0) {
await TGLogger.Script(
`[米游币任务]获取任务状态失败:${stateResp.retcode} ${stateResp.message}`,
"warn",
);
showSnackbar.error(`[${stateResp.retcode}] ${stateResp.message}`);
return;
}
await TGLogger.Script("[米游币任务]获取任务状态成功");
todayPoints.value = stateResp.data.already_received_points;
totalPoints.value = stateResp.data.today_total_points;
userPoints.value = stateResp.data.total_points;
await TGLogger.Script("[米游币任务]合并任务数据");
mergeMission(missionList.value, stateResp.data.states);
await TGLogger.Script("[米游币任务]任务数据合并完成");
} catch (e) {
await TGLogger.Script(`[米游币任务]获取任务状态异常:${TGHttps.getErrMsg(e)}`, "error");
}
}
async function autoSign(ck: TGApp.App.Account.Cookie, skip: boolean, ch?: string): Promise<void> {
const signFind = parseMissions.value.find((i) => i.key === "continuous_sign");
if (!signFind) {
await TGLogger.Script("[米游币任务]未找到打卡任务");
return;
}
if (signFind.status) {
await TGLogger.Script("[米游币任务]今日已打卡");
return;
}
await TGLogger.Script("[米游币任务]正在执行打卡");
const ckSign = { stoken: ck.stoken, stuid: ck.stuid, mid: ck.mid };
let resp: TGApp.BBS.Response.Base;
try {
resp = await apiHubReq.sign(ckSign, 2, ch);
console.log("打卡情况", resp);
} catch (e) {
await TGLogger.Script(`[米游币任务]打卡异常:${TGHttps.getErrMsg(e)}`, "error");
return;
}
if (resp.retcode !== 0) {
if (resp.retcode !== 1034) {
await TGLogger.Script(`[米游币任务]打卡失败:${resp.retcode} ${resp.message}`);
showSnackbar.error(`[${resp.retcode}] ${resp.message}`);
return;
}
if (skip) {
await TGLogger.Script("已设置跳过验证,打卡失败");
await TGNotify.normal("自动打卡触发验证", `UID:${ck.stuid}`);
return;
}
await TGLogger.Script(`[米游币任务]社区签到触发验证码,正在尝试验证`);
const challenge = await miscReq.challenge(ckSign);
if (challenge === false) {
await TGLogger.Script(`[米游币任务]验证失败`);
return;
}
await autoSign(ck, skip, challenge);
return;
}
await TGLogger.Script("[米游币任务]打卡成功");
}
async function genShare(): Promise<void> {
if (!tusmEl.value) return;
await generateShareImg(`MiTasks_${props.acCur?.uid ?? "unknown"}`, tusmEl.value, 2);
}
</script>
<style lang="scss" scoped>
.tusm-box {
position: relative;
display: flex;
width: 100%;
box-sizing: border-box;
flex-direction: column;
padding: 12px;
border: 1px solid var(--common-shadow-2);
border-radius: 4px;
background: var(--box-bg-1);
color: var(--box-text-1);
}
.tusm-top,
.tusm-mid {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
}
.tusm-title {
cursor: pointer;
font-family: var(--font-title);
font-size: 18px;
}
.tusm-acts {
display: flex;
align-items: center;
gap: 8px;
}
.tusm-switch-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 8px;
}
.tusm-switch {
display: flex;
height: 36px;
align-items: center;
justify-content: center;
margin-right: 4px;
}
.tusm-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
}
.tusm-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.mission-item {
position: relative;
display: flex;
width: 100%;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
padding: 10px;
border-radius: 4px;
background: var(--box-bg-2);
color: var(--box-text-2);
.left {
display: flex;
align-items: center;
gap: 4px;
}
.right {
position: relative;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
:first-child {
width: 100px;
}
}
}
</style>