mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-15 03:53:16 +08:00
✨ 重置胡桃云密码
This commit is contained in:
@@ -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;
|
||||
|
||||
258
src/components/pageConfig/tco-hutaoVerify.vue
Normal file
258
src/components/pageConfig/tco-hutaoVerify.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
78
src/plugins/Hutao/types/Account.d.ts
vendored
78
src/plugins/Hutao/types/Account.d.ts
vendored
@@ -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">;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user