Files
jiapuapp/pages/profile/change-password.vue
T

320 lines
9.6 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>
<view
class="password-page"
:class="{
'password-state--ready': passwordState === 'ready',
'password-state--saving': passwordState === 'saving',
}"
>
<ModulePageBackground module="profile" />
<view class="page-layer"
><PageHeader title="修改密码" custom-back @back="requestBack"
/></view>
<view class="page-content page-layer">
<view class="security-tip"
><text>设置安全密码</text
><text>建议使用 832 位字母与数字组合不要与其他应用共用</text></view
>
<view class="form-panel">
<view
v-for="field in passwordFields"
:key="field.key"
class="field-block"
>
<view class="form-row">
<text>{{ field.label }}</text>
<input
v-model="passwordForm[field.key]"
:password="!passwordVisible[field.key]"
maxlength="32"
:aria-label="field.label"
:aria-invalid="Boolean(errors[field.key])"
:aria-describedby="errors[field.key] ? `${field.key}-password-error` : undefined"
:placeholder="field.placeholder"
@input="errors[field.key] = ''"
/>
<button
class="password-toggle"
role="button"
tabindex="0"
:aria-label="`${passwordVisible[field.key] ? '隐藏' : '显示'}${field.label}`"
:aria-pressed="passwordVisible[field.key]"
@click="togglePassword(field.key)"
>{{ passwordVisible[field.key] ? "隐藏" : "显示" }}</button
>
</view>
<text v-if="errors[field.key]" :id="`${field.key}-password-error`" class="field-error" role="alert">{{
errors[field.key]
}}</text>
</view>
</view>
<AppButton
block
:disabled="passwordState === 'saving'"
:label="passwordState === 'saving' ? '正在提交' : '确认修改密码'"
@click="savePassword"
/>
</view>
<AppToast :visible="toastVisible" :message="toastMessage" />
<AppDialog
:visible="discardVisible"
:close-on-mask="false"
eyebrow="放弃确认"
title="放弃密码填写?"
message="新密码还没有保存。"
confirm-text="确认放弃"
cancel-text="继续填写"
show-cancel
@confirm="confirmDiscard"
@cancel="cancelDiscard"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from "vue";
import { onBackPress, onUnload } from "@dcloudio/uni-app";
import AppButton from "@/components/AppButton.vue";
import AppDialog from "@/components/AppDialog.vue";
import AppToast from "@/components/AppToast.vue";
import ModulePageBackground from "@/components/ModulePageBackground.vue";
import PageHeader from "@/components/PageHeader.vue";
import {
createRequestController,
isRequestCancelled
} from "@/services/api/request-controller.js";
import { authApi } from "@/services/api/auth-service.js";
import { getRequestErrorMessage } from "@/services/api/request-error-message.js";
import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";
import { calcMD5 } from "@/utils/md5.js";
import { createNonIdempotentWriteGuard } from "@/utils/non-idempotent-write-guard.js";
import { goRoot, handleBackPress, runBackGuard } from "@/utils/navigation/gateway.js";
import { session } from "@/utils/session.js";
import {
PASSWORD_POLICY_MESSAGE,
validatePassword,
} from "@/utils/auth/password-policy.js";
const passwordForm = reactive({ current: "", next: "", confirm: "" });
const passwordVisible = reactive({
current: false,
next: false,
confirm: false,
});
const errors = reactive({ current: "", next: "", confirm: "" });
const passwordState = ref("ready");
const discardVisible = ref(false);
const passwordFields = [
{ key: "current", label: "当前密码", placeholder: "请输入当前密码" },
{ key: "next", label: "新密码", placeholder: "832 位字母与数字" },
{ key: "confirm", label: "确认新密码", placeholder: "请再次输入新密码" },
];
const toastVisible = ref(false);
const toastMessage = ref("");
let timer = null;
const passwordChangeRequestController = createRequestController();
const passwordChangeGuard = createNonIdempotentWriteGuard();
let pageActive = true;
const formSnapshot = computed(() => JSON.stringify(passwordForm));
const baseline = ref(formSnapshot.value);
const isDirty = computed(() => formSnapshot.value !== baseline.value);
const discardConfirmation = createDiscardConfirmation((visible) => {
discardVisible.value = visible;
});
const requestDiscardConfirmation = discardConfirmation.request;
const confirmDiscard = discardConfirmation.confirm;
const cancelDiscard = discardConfirmation.cancel;
const showToast = (message) => {
toastMessage.value = message;
toastVisible.value = true;
clearTimeout(timer);
timer = setTimeout(() => (toastVisible.value = false), 1800);
};
const togglePassword = (key) => {
passwordVisible[key] = !passwordVisible[key];
};
const validateForm = () => {
errors.current = passwordForm.current ? "" : "请输入当前密码";
const nextPassword = validatePassword(passwordForm.next);
errors.next = nextPassword.valid ? "" : PASSWORD_POLICY_MESSAGE;
if (!errors.next && passwordForm.next === passwordForm.current)
errors.next = "新密码不能与当前密码相同";
errors.confirm = !passwordForm.confirm
? "请再次输入新密码"
: passwordForm.confirm !== passwordForm.next
? "两次输入的新密码不一致"
: "";
return !Object.values(errors).some(Boolean);
};
const savePassword = async () => {
if (!validateForm() || passwordState.value === "saving") return;
const passwordChangePayload = {
oldPasswordHash: calcMD5(passwordForm.current),
newPasswordHash: calcMD5(passwordForm.next),
};
const passwordChangeAttempt = passwordChangeGuard.begin(passwordChangePayload);
if (passwordChangeAttempt === null) {
showToast(
"上次修改结果暂时无法确认,请重新登录验证新密码,不要重复提交",
);
return;
}
passwordState.value = "saving";
try {
await authApi.changePassword(
passwordChangePayload,
{ requestController: passwordChangeRequestController },
);
if (!pageActive) return;
session.clear();
await goRoot("A01");
if (!pageActive) return;
passwordForm.current = "";
passwordForm.next = "";
passwordForm.confirm = "";
baseline.value = formSnapshot.value;
} catch (error) {
if (!pageActive) return;
if (passwordChangeGuard.recordFailure(passwordChangeAttempt, error)) {
showToast(
"密码修改结果暂时无法确认,请重新登录验证新密码,不要重复提交",
);
return;
}
if (!isRequestCancelled(error))
showToast(
error?.code
? getRequestErrorMessage(error, "密码修改失败,请稍后重试")
: error?.message || "密码修改失败,请稍后重试",
);
} finally {
if (pageActive) passwordState.value = "ready";
}
};
const requestBack = () =>
runBackGuard({
transientOpen: discardVisible.value,
dirty: isDirty.value,
submitting: passwordState.value === "saving",
"close-transient": cancelDiscard,
"block-submitting": () => true,
"confirm-discard": requestDiscardConfirmation,
});
onBackPress((event) => handleBackPress(event, requestBack));
onUnload(() => {
pageActive = false;
passwordChangeRequestController.abort();
clearTimeout(timer);
discardConfirmation.dispose();
});
</script>
<style scoped lang="scss">
@import "../../styles/adaptive-frame-profiles.scss";
.password-page {
display: flex;
min-height: 100vh;
flex-direction: column;
background: $paper;
}
.page-layer {
z-index: 1;
}
.page-content {
flex: 1;
padding: 28rpx 30rpx calc(72rpx + env(safe-area-inset-bottom));
}
.security-tip,
.form-panel {
@include adaptive-profile-content;
}
.security-tip {
min-height: 170rpx;
padding: 38rpx 44rpx;
text-align: center;
}
.security-tip text {
display: block;
}
.security-tip text:first-child {
color: $ink;
font-size: clamp(17px, 32rpx, 22px);
font-weight: 700;
}
.security-tip text:last-child {
margin-top: 10rpx;
color: $ink-muted;
font-size: clamp(14px, 22rpx, 17px);
line-height: 1.55;
}
.form-panel {
margin-top: 20rpx;
padding: 22rpx 34rpx 30rpx;
}
.field-block + .field-block {
margin-top: 6rpx;
}
.form-row {
display: grid;
grid-template-columns: 184rpx minmax(0, 1fr) 74rpx;
min-height: 92rpx;
align-items: center;
gap: 12rpx;
border-bottom: 1px solid rgba(181, 137, 63, 0.42);
}
.form-row > text {
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
font-weight: 700;
white-space: nowrap;
}
.form-row input {
width: auto;
min-width: 0;
min-height: 80rpx;
color: $ink;
font-size: clamp(14px, 23rpx, 17px);
}
.password-toggle {
display: flex;
min-height: 80rpx;
align-items: center;
justify-content: flex-end;
width: auto;
margin: 0;
padding: 0;
border: 0;
background: transparent;
color: $brand-red;
font-size: clamp(13px, 21rpx, 16px);
}
.password-toggle::after {
border: 0;
}
.field-error {
display: block;
padding-top: 7rpx;
color: #b42318;
font-size: clamp(13px, 20rpx, 16px);
line-height: 1.4;
text-align: right;
}
.page-content > .app-button {
margin-top: 28rpx;
}
@media (max-width: 340px) {
.page-content {
padding-right: 22rpx;
padding-left: 22rpx;
}
.form-panel {
padding-right: 26rpx;
padding-left: 26rpx;
}
.form-row {
grid-template-columns: 184rpx minmax(0, 1fr) 64rpx;
gap: 8rpx;
}
}
</style>