重置胡桃云密码

This commit is contained in:
BTMuli
2026-01-12 23:26:38 +08:00
parent 2a9c0ab552
commit 44dd81463b
8 changed files with 464 additions and 26 deletions

View File

@@ -32,18 +32,24 @@
variant="outlined"
@click="hutaoStore.tryRefreshInfo()"
/>
<v-btn icon="mdi-lock-reset" title="重置密码" variant="outlined" @click="showVerify = true" />
</template>
</v-card>
<TcoHutaoVerify v-model="showVerify" />
</template>
<script lang="ts" setup>
import showDialog from "@comp/func/dialog.js";
import TcoHutaoVerify from "@comp/pageConfig/tco-hutaoVerify.vue";
import useHutaoStore from "@store/hutao.js";
import { timeStr2str } from "@utils/toolFunc.js";
import { storeToRefs } from "pinia";
import { ref } from "vue";
const hutaoStore = useHutaoStore();
const { userName, userInfo, isLogin } = storeToRefs(useHutaoStore());
const showVerify = ref<boolean>(false);
function getAccountDesc(): string {
if (!isLogin.value) return "未登录";
if (userInfo.value) {
@@ -80,7 +86,7 @@ async function tryLogin(): Promise<void> {
.logo {
position: absolute;
z-index: 1;
z-index: 0;
right: -20px;
bottom: -20px;
width: 160px;

View File

@@ -0,0 +1,258 @@
<!-- 重置密码 TODO:其他类似请求通用&UI调整 -->
<template>
<TOverlay v-model="visible" :outer-close="false" blur-val="4px">
<v-card
:disabled="formDisabled"
class="tco-hutao-verify-container"
density="compact"
title="重置胡桃云密码"
>
<v-form ref="formEl" class="thvc-mid">
<v-text-field
ref="usernameInput"
v-model="username"
:disabled="codeDisabled"
:rules="usernameRules"
clearable
color="var(--tgc-od-blue)"
density="compact"
label="用户名(邮箱)"
/>
<v-text-field
v-model="verifyCode"
:rules="[(v) => !!v || '请填写验证码']"
clearable
color="var(--tgc-od-blue)"
density="compact"
label="验证码"
>
<template #append>
<v-btn
:disabled="codeDisabled"
:loading="codeLoad"
color="var(--tgc-od-blue)"
variant="flat"
@click="tryGetCode()"
>
<template v-if="!codeDisabled">获取验证码</template>
<template v-else>{{ codeRest }}s</template>
</v-btn>
</template>
</v-text-field>
<v-text-field
v-model="pwd"
:rules="[(v) => !!v || '请填写新密码']"
:type="showPwd ? 'text' : 'password'"
clearable
color="var(--tgc-od-blue)"
density="compact"
label="新密码"
>
<template #append-inner>
<v-icon @click="showPwd = !showPwd">{{ showPwd ? "mdi-eye" : "mdi-eye-off" }}</v-icon>
</template>
</v-text-field>
</v-form>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="var(--tgc-od-white)" @click="onCancel()">
<v-icon icon="mdi-chevron-right" start></v-icon>
<span>取消</span>
</v-btn>
<v-btn :loading="formLoad" color="success" @click="onSubmit()">
<v-icon icon="mdi-chevron-right" start></v-icon>
<span>确认重置</span>
</v-btn>
</v-card-actions>
</v-card>
</TOverlay>
</template>
<script lang="ts" setup>
import TOverlay from "@comp/app/t-overlay.vue";
import showSnackbar from "@comp/func/snackbar.js";
import hutao from "@Hutao/index.js";
import useHutaoStore from "@store/hutao.js";
import { validEmail } from "@utils/toolFunc.js";
import { onUnmounted, ref, shallowRef, useTemplateRef } from "vue";
import { VForm, VTextField } from "vuetify/components";
type VuetifyRules = VTextField["rules"];
const hutaoStore = useHutaoStore();
// eslint-disable-next-line no-undef
let codeTimer: NodeJS.Timeout | null = null;
const visible = defineModel<boolean>({ default: false });
const formRef = useTemplateRef<VForm>("formEl");
const formDisabled = ref<boolean>(false);
const formLoad = ref<boolean>(false);
const username = ref<string>();
const usernameRef = useTemplateRef<VTextField>("usernameInput");
const usernameRules = shallowRef<VuetifyRules>([
(v) => !!v || "请填写用户名",
(v) => (v && validEmail(v)) || "请填写符合格式的邮箱地址",
]);
const verifyCode = ref<string>();
const codeDisabled = ref<boolean>(false);
const pwd = ref<string>();
const showPwd = ref<boolean>(false);
const codeLoad = ref<boolean>(false);
const codeRest = ref<number>(0);
async function tryGetCode(): Promise<void> {
if (!usernameRef.value) return;
const check = await usernameRef.value.validate();
if (check.length > 0) return;
codeLoad.value = true;
const resp = await hutao.Account.verify.pwd(username.value!);
if (resp.retcode !== 0) {
showSnackbar.warn(`[${resp.retcode}] ${resp.message}`);
} else {
showSnackbar.success(`${resp.message}`);
}
codeLoad.value = false;
codeDisabled.value = true;
codeRest.value = 60;
codeTimer = setInterval(countdownCode, 1000);
}
function countdownCode(): void {
if (codeRest.value <= 0) {
if (codeTimer !== null) clearInterval(codeTimer);
codeTimer = null;
codeRest.value = 0;
codeDisabled.value = false;
return;
}
codeRest.value -= 1;
}
function onCancel(): void {
visible.value = false;
}
async function onSubmit(): Promise<void> {
if (!formRef.value) return;
const check = await formRef.value.validate();
if (!check.valid) return;
formDisabled.value = true;
formLoad.value = true;
const resp = await hutao.Account.reset.pwd(username.value!, verifyCode.value!, pwd.value!);
formLoad.value = false;
if (resp.retcode !== 0) {
showSnackbar.warn(`[${resp.retcode}] ${resp.message}`);
formDisabled.value = false;
return;
}
showSnackbar.success(`${resp.message}`);
await hutaoStore.autoLogin(username.value!, pwd.value!);
formDisabled.value = false;
visible.value = false;
}
onUnmounted(() => {
if (codeTimer !== null) {
clearInterval(codeTimer);
codeTimer = null;
}
});
</script>
<style lang="scss" scoped>
.tco-hutao-verify-container {
position: relative;
width: 400px;
padding: 8px;
border-radius: 4px;
background-color: var(--box-bg-1);
}
.thvc-mid {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
font-size: 10px;
row-gap: 8px;
}
.thvc-form-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
column-gap: 8px;
}
.thvc-fi-label {
position: relative;
width: 60px;
flex-shrink: 0;
font-family: var(--font-title);
text-align: right;
}
.thvc-fi-input {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
column-gap: 4px;
input {
width: 100%;
height: 24px;
padding: 2px 8px;
border: 1px solid var(--common-shadow-1);
border-radius: 4px;
background: var(--box-bg-2);
font-size: 12px;
line-height: 16px;
&:focus-visible {
border: unset;
outline: 1px solid var(--tgc-od-blue);
}
}
.icon {
color: var(--tgc-od-white);
cursor: pointer;
}
}
.thvc-fii-append {
flex-shrink: 0;
&.code {
display: flex;
width: 80px;
height: 24px;
box-sizing: border-box;
align-items: center;
justify-content: center;
border-radius: 4px;
background: var(--tgc-od-blue);
color: var(--tgc-white-1);
cursor: pointer;
font-size: 12px;
line-height: 24px;
&:hover {
filter: brightness(1.2);
}
&.disabled {
cursor: not-allowed;
}
}
}
</style>

View File

@@ -16,31 +16,17 @@
<div class="btn-list">
<v-btn class="test-btn" @click="test()">测试</v-btn>
</div>
<TcoHutaoVerify v-model="show" />
</div>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import hutao from "@Hutao/index.js";
import useHutaoStore from "@store/hutao.js";
import { storeToRefs } from "pinia";
import TcoHutaoVerify from "@comp/pageConfig/tco-hutaoVerify.vue";
import { ref } from "vue";
const hutaoStore = useHutaoStore();
const { accessToken } = storeToRefs(hutaoStore);
const show = ref<boolean>(false);
async function test() {
if (!hutaoStore.checkIsValid()) {
await hutaoStore.tryRefreshToken();
}
const resp = await hutao.Gacha.entry(accessToken.value!);
if ("retcode" in resp) {
showSnackbar.warn(`${resp.retcode}-${resp.message}`);
return;
}
console.log(resp);
// userName.value = inputN;
// accessToken.value = resp.AccessToken;
// refreshToken.value = resp.RefreshToken;
// showSnackbar.success("登录成功!");
show.value = true;
}
</script>
<style lang="css" scoped>

View File

@@ -12,7 +12,13 @@ import {
getTeamCollect,
uploadAbyssData,
} from "./request/abyssReq.js";
import { getUserInfo, loginPassport, refreshToken } from "./request/accountReq.js";
import {
getResetPwdCode,
getUserInfo,
loginPassport,
refreshToken,
resetPwd,
} from "./request/accountReq.js";
import { getCombatStatistic, uploadCombatData } from "./request/combatReq.js";
import {
deleteGachaLogs,
@@ -50,11 +56,16 @@ const Hutao = {
Account: {
register: _,
login: loginPassport,
verify: _,
verify: {
username: _,
usernameNew: _,
pwd: getResetPwdCode,
cancel: _,
},
cancel: _,
reset: {
username: _,
password: _,
pwd: resetPwd,
},
info: getUserInfo,
},

View File

@@ -14,7 +14,7 @@ const PassportUrl = "https://homa.gentle.house/Passport/v2/";
* @since Beta v0.9.1
* @param username - 用户名(邮箱)
* @param password - 密码
* @returns
* @returns 登录返回
*/
export async function loginPassport(
username: string,
@@ -38,6 +38,7 @@ export async function loginPassport(
/**
* 刷新访问令牌
* @since Beta v0.9.1
* @returns 令牌返回
*/
export async function refreshToken(token: string) {
const url = `${PassportUrl}RefreshToken`;
@@ -55,6 +56,7 @@ export async function refreshToken(token: string) {
/**
* 获取用户信息
* @since Beta v0.9.1
* @returns 用户信息返回
*/
export async function getUserInfo(
token: string,
@@ -68,3 +70,61 @@ export async function getUserInfo(
if (resp.retcode !== 0) return <TGApp.Plugins.Hutao.Base.Resp>resp;
return <TGApp.Plugins.Hutao.Account.InfoRes>resp.data;
}
/**
* 获取充值密码验证码
* @since Beta v0.9.1
* @param username - 用户
* @returns 验证码返回
*/
export async function getResetPwdCode(username: string): Promise<TGApp.Plugins.Hutao.Base.Resp> {
const url = `${PassportUrl}Verify`;
const header = await getReqHeader();
const data: TGApp.Plugins.Hutao.Account.ResetPwdCodeParam = {
UserName: rsaEncrypt(username),
IsCancelRegistration: false,
IsResetPassword: true,
IsResetUserName: false,
IsResetUserNameNew: false,
};
const resp = await TGHttp<TGApp.Plugins.Hutao.Base.Resp>(url, {
method: "POST",
headers: header,
body: JSON.stringify(data),
});
console.log(resp);
return resp;
}
/**
* 重置密码
* @since Beta v0.9.1
* @param username - 用户
* @param code - 验证码
* @param pwd - 密码
* @returns 重置密码返回
*/
export async function resetPwd(
username: string,
code: string,
pwd: string,
): Promise<TGApp.Plugins.Hutao.Base.Resp> {
const url = `${PassportUrl}ResetPassword`;
const header = await getReqHeader();
const data: TGApp.Plugins.Hutao.Account.ResetPwdParams = {
UserName: rsaEncrypt(username),
Password: rsaEncrypt(pwd),
VerifyCode: rsaEncrypt(code),
IsCancelRegistration: false,
IsResetPassword: true,
IsResetUserName: false,
IsResetUserNameNew: false,
};
const resp = await TGHttp<TGApp.Plugins.Hutao.Base.Resp>(url, {
method: "POST",
headers: header,
body: JSON.stringify(data),
});
console.log(resp);
return resp;
}

View File

@@ -98,4 +98,82 @@ declare namespace TGApp.Plugins.Hutao.Account {
/** 用户名 */
UserName: string;
};
/**
* Verify接口请求参数
* @since Beta v0.9.1
* @remarks
* - 同一邮箱同一类型验证码在 60 秒内只能申请一次,请在客户端进行节流处理。
* - 若需要重置用户名,请分别提交旧邮箱与新邮箱两次请求以获取成对验证码。
*/
type VerifyParams =
| ResetPwdCodeParam
| ResetUserNameCodeParam
| ResetUserNameNewCodeParam
| CancelRegCodeParam;
/**
* Verify接口Flag值
* @since Beta v0.9.1
*/
type VerifyFlags =
| "IsResetPassword"
| "IsResetUserName"
| "IsResetUserNameNew"
| "IsCancelRegistration";
/**
* Verify Flag 生成
* @since Beta v0.9.1
*/
type VerifyFlagGen<T extends VerifyFlags> = {
[K in VerifyFlags]: K extends T ? true : false;
};
/**
* 获取重设密码验证码参数
* @since Beta v0.9.1
*/
type ResetPwdCodeParam = {
/** 用户名 */
UserName: string;
} & VerifyFlagGen<"IsResetPassword">;
/**
* 重设密码参数
* @since Beta v0.9.1
*/
type ResetPwdParams = ResetPwdCodeParam & {
/** 密码 */
Password: string;
/** 验证码 */
VerifyCode: string;
};
/**
* 获取重设用户名验证码参数
* @since Beta v0.9.1
*/
type ResetUserNameCodeParam = {
/** 用户名 */
UserName: string;
} & VerifyFlagGen<"IsResetUserName">;
/**
* 获取重设用户名-二次验证码参数
* @since Beta v0.9.1
*/
type ResetUserNameNewCodeParam = {
/** 用户名 */
UserName: string;
} & VerifyFlagGen<"IsResetUserNameNew">;
/**
* 获取取消注册验证码参数
* @since Beta v0.9.1
*/
type CancelRegCodeParam = {
/** 用户名 */
UserName: string;
} & VerifyFlagGen<"IsCancelRegistration">;
}

View File

@@ -7,6 +7,7 @@ import showDialog from "@comp/func/dialog.js";
import showLoading from "@comp/func/loading.js";
import showSnackbar from "@comp/func/snackbar.js";
import hutao from "@Hutao/index.js";
import { validEmail } from "@utils/toolFunc.js";
import { defineStore } from "pinia";
import { ref } from "vue";
@@ -42,8 +43,7 @@ const useHutaoStore = defineStore(
showSnackbar.cancel("已取消胡桃云账号输入");
return;
}
const mailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
if (!mailReg.test(inputN.trim())) {
if (!validEmail(inputN.trim())) {
showSnackbar.warn("请输入合法邮箱地址");
return;
}
@@ -78,6 +78,33 @@ const useHutaoStore = defineStore(
}
}
async function autoLogin(username: string, pwd: string): Promise<void> {
await showLoading.start("正在登录胡桃云", username);
try {
const resp = await hutao.Account.login(username, pwd);
if ("retcode" in resp) {
showSnackbar.warn(`[${resp.retcode}] ${resp.message}`);
console.error(resp);
await showLoading.end();
return;
}
isLogin.value = true;
userName.value = username;
accessToken.value = resp.AccessToken;
refreshToken.value = resp.RefreshToken;
accessExpire.value = Date.now() + resp.ExpiresIn * 1000;
showSnackbar.success("成功登录胡桃云");
} catch (err) {
console.error(err);
showSnackbar.error("登录胡桃云失败");
} finally {
await showLoading.end();
}
if (isLogin.value) {
await tryRefreshInfo();
}
}
async function tryRefreshInfo(): Promise<void> {
await tryRefreshToken();
const resp = await hutao.Account.info(accessToken.value!);
@@ -128,6 +155,7 @@ const useHutaoStore = defineStore(
userInfo,
checkIsValid,
tryLogin,
autoLogin,
tryRefreshToken,
tryRefreshInfo,
checkGachaExpire,

View File

@@ -394,3 +394,14 @@ export function str2timeStr(str: string): string {
// 输出为 UTC 的 ISO 字符串
return format(d, "yyyy-MM-dd'T'HH:mm:ss.SSSX", { in: tz("UTC") });
}
/**
* 验证邮箱
* @since Beta v0.9.1
* @param email - 邮箱
* @returns 验证结果
*/
export function validEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}