Commit 0924d371 authored by tyyin lan's avatar tyyin lan

chore(个人设置): 邮箱绑定&密码修改

parent 22d26331
......@@ -23,6 +23,7 @@
"@vueuse/core": "^10.11.1",
"axios": "^1.7.7",
"clipboardy": "^4.0.0",
"cropperjs": "^1.6.2",
"dayjs": "^1.11.13",
"dompurify": "^3.2.0",
"github-markdown-css": "^5.7.0",
......
......@@ -29,6 +29,9 @@ importers:
clipboardy:
specifier: ^4.0.0
version: 4.0.0
cropperjs:
specifier: ^1.6.2
version: 1.6.2
dayjs:
specifier: ^1.11.13
version: 1.11.13
......@@ -1526,6 +1529,9 @@ packages:
typescript:
optional: true
cropperjs@1.6.2:
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
......@@ -4672,6 +4678,8 @@ snapshots:
optionalDependencies:
typescript: 5.6.2
cropperjs@1.6.2: {}
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
......
......@@ -24,3 +24,17 @@ export function fetchUserDetailInfo<T>() {
export function fetchGetMemberInfoById<T>(memberId: number) {
return request.post<T>(`/bizMemberInfoRest/getMemberNickName.json?memberId=${memberId}`)
}
export function fetchUserInfoUpdate<T>(userInfo: object) {
return request.post<T>('/bizMemberInfoRest/updateMemberInfo.json', userInfo)
}
export function fetchVerifyCode<T>(account: string, code: string) {
return request.post<T>('/judgeCodeRest/judgeCodeReturnAuthCode.json', null, { params: { account, code } })
}
export function fetchUserPasswordUpdate<T>(authCode: string, password: string) {
return request.post<T>('/bizMemberInfoRest/changeMemberPassword.json', null, {
params: { authCode, password },
})
}
<script setup lang="ts">
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { nextTick, ref, shallowRef, useTemplateRef } from 'vue'
const imageRef = useTemplateRef('imageRef')
// const rectanglePreview = useTemplateRef('rectanglePreview')
const roundPreview = useTemplateRef('roundPreview')
const isShowImageCropperModal = ref(false)
const cropperIns = shallowRef<Cropper | null>(null)
const isInitCropper = ref(false)
const confirmBtnLoading = ref(false)
const currentEditImageUrl = ref('')
let cropImageResolve: (value: any) => void = () => {}
let cropImageReject: (value: any) => void = () => {}
function initCropper() {
return new Promise((resolve) => {
nextTick(() => {
if (imageRef.value) {
cropperIns.value = new Cropper(imageRef.value, {
viewMode: 3,
dragMode: 'move',
aspectRatio: 1,
// cropBoxMovable: false, // 可通过拖动移动裁剪框
// cropBoxResizable: false, // 可通过拖动调整裁剪框的大小
minCropBoxWidth: 50, // 裁剪框的最小宽度
minCropBoxHeight: 50, // 裁剪框的最小高度
// autoCropArea: 1,
// preview: previewRef.value! || [],
preview: [roundPreview.value!],
// rotatable: false, // 旋转
// scalable: false, // 可伸缩
// zoomable: false, // 可缩放
// zoomOnTouch: false, // 缩放触摸
ready: () => {
if (!isInitCropper.value) {
isInitCropper.value = true
resolve('加载成功')
}
},
})
}
})
})
}
function handleCropConfirm() {
if (cropperIns.value) {
confirmBtnLoading.value = true
cropperIns.value.getCroppedCanvas().toBlob((blob) => {
if (blob) {
const fd = new FormData()
const file = new File([blob], `image.${blob.type.split('/')[1]}`, { type: blob.type })
fd.append('file', file)
cropImageResolve(file)
confirmBtnLoading.value = false
isShowImageCropperModal.value = false
}
})
}
}
function handleCropCancel() {
isShowImageCropperModal.value = false
cropImageReject(new Error('Cancel'))
}
function cropImage(url: string): Promise<File> {
currentEditImageUrl.value = url
isShowImageCropperModal.value = true
return initCropper().then(() => {
return new Promise((resolve, reject) => {
cropImageResolve = resolve
cropImageReject = reject
})
})
}
function onModalAfterLeave() {
if (cropperIns.value) {
cropperIns.value.destroy()
cropperIns.value = null
isInitCropper.value = false
}
}
defineExpose({
cropImage,
})
</script>
<template>
<n-modal v-model:show="isShowImageCropperModal" :on-after-leave="onModalAfterLeave">
<n-card class="!w-[800px]" title="图片裁切" :bordered="false" size="huge" role="dialog" aria-modal="true">
<div class="relative flex">
<div class="absolute inset-0">
<n-skeleton height="400px" width="400px" />
</div>
<div
class="h-[400px] w-[400px] transition-[opacity] duration-300 ease-in-out"
:class="{ 'opacity-0': !isInitCropper }"
>
<img ref="imageRef" class="block h-full w-full" alt="Picture" :src="currentEditImageUrl" />
</div>
<div class="ml-[40px]">
<div class="mb-[10px] text-[16px]">图片预览:</div>
<!-- <div ref="rectanglePreview" class="h-[180px] w-[180px] overflow-hidden rounded-[6px] bg-[#f3f3f3]"></div> -->
<div ref="roundPreview" class="mt-[20px] h-[180px] w-[180px] overflow-hidden rounded-full bg-[#f3f3f3]"></div>
</div>
</div>
<template #footer>
<div class="text-end">
<n-button class="!mr-[20px]" @click="handleCropCancel">取消</n-button>
<n-button type="primary" :loading="confirmBtnLoading" @click="handleCropConfirm">确定</n-button>
</div>
</template>
</n-card>
</n-modal>
</template>
<style lang="scss" scoped>
// :global(.cropper-view-box) {
// .cropper-view-box,
// .cropper-face {
// border-radius: 50%;
// }
// .cropper-view-box {
// outline: 0;
// box-shadow: 0 0 0 1px #39f;
// }
// }
// :global(.cropper-container .cropper-crop-box) {
// outline: 1px solid #000dff;
// .cropper-line {
// background-color: #000dff !important;
// }
// .cropper-point {
// background-color: #000dff !important;
// }
// }
// .cropper-container .cropper-crop-box {
// outline: 1px solid #000dff;
// .cropper-line {
// background-color: #000dff !important;
// }
// .cropper-point {
// background-color: #000dff !important;
// }
// }
</style>
......@@ -89,6 +89,8 @@ common_module:
language: 'Language'
change: 'Change'
bind: 'Bind'
sms: 'Short message'
verificationCode: 'Verification code'
dialogue_module:
continue_question_message: 'You can keep asking questions'
......@@ -452,3 +454,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: 'Verify that the new password is inconsistent with the new password'
please_enter_the_account_nickname: 'Please enter the account nickname'
please_enter_a_personal_profile: 'Please enter a personal profile'
please_enter_the_correct_verification_code: 'Please enter the correct verification code'
binding_successful: 'Binding successful'
obtaining_the_verification_code: 'Obtaining the verification code'
......@@ -88,6 +88,8 @@ common_module:
language: '语言'
change: '更换'
bind: '绑定'
sms: '短信'
verificationCode: '验证码'
dialogue_module:
continue_question_message: '你可以继续提问'
......@@ -450,3 +452,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: '确认新密码与新密码不一致'
please_enter_the_account_nickname: '请输入账号昵称'
please_enter_a_personal_profile: '请输入个人简介'
please_enter_the_correct_verification_code: '请输入正确验证码'
binding_successful: '绑定成功'
obtaining_the_verification_code: '获取验证码方式'
......@@ -88,6 +88,8 @@ common_module:
language: '語言'
change: '更換'
bind: '綁定'
sms: '短信'
verificationCode: '驗證碼'
dialogue_module:
continue_question_message: '你可以繼續提問'
......@@ -450,3 +452,6 @@ personal_settings_module:
verify_that_the_new_password_is_inconsistent_with_the_new_password: '確認新密碼與新密碼不一致'
please_enter_the_account_nickname: '請輸入賬號昵稱'
please_enter_a_personal_profile: '請輸入個人簡介'
please_enter_the_correct_verification_code: '請輸入正確驗證碼'
binding_successful: '綁定成功'
obtaining_the_verification_code: '獲取驗證碼方式'
......@@ -47,7 +47,7 @@ export const useUserStore = defineStore('user-store', {
ss.set(UserStoreStorageKeyEnum.userInfo, userInfo)
},
fetchUpdateUserInfo() {
fetchUserDetailInfo<UserInfo>().then((res) => {
return fetchUserDetailInfo<UserInfo>().then((res) => {
this.userInfo = res.data
})
},
......
<script setup lang="ts">
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
import { onMounted, ref, shallowReadonly, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/modules/user'
import isEmail from 'validator/es/lib/isEmail'
import { fetchEmailCode, fetchUserInfoUpdate, fetchVerifyCode } from '@/apis/user'
import { ss } from '@/utils/storage'
const { t } = useI18n()
const userStore = useUserStore()
const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const isShowMailboxBindingModal = defineModel<boolean>('isShowMailboxBindingModal', { default: false })
const mailboxBindingSubmitBtnLoading = ref(false)
const emailInfoForm = ref({
email: '',
verifyCode: '',
})
const isShowCountdown = ref(false)
const countdownDuration = ref<number>(60000)
const countdownActive = ref(true)
const emailInfoFormRules = shallowReadonly({
email: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_your_email_address'))
} else if (!isEmail(value)) {
return new Error(t('login_module.please_enter_the_correct_email_address'))
}
return
},
},
verifyCode: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_the_verification_code'))
} else if (value.length < 6) {
return new Error(t('personal_settings_module.please_enter_the_correct_verification_code'))
}
return
},
},
})
onMounted(() => {
let timeStringDraft = ss.get('PASSWORD_CHANGE_CODE')
if (timeStringDraft) {
const time = Math.floor(Date.now() - parseInt(timeStringDraft))
if (time < 60000) {
countdownDuration.value = 60000 - time
countdownRef.value?.reset()
isShowCountdown.value = true
}
}
})
function onModalAfterLeave() {
emailInfoForm.value.email = ''
}
function handleMailboxBindingSubmit() {
emailInfoFormRef.value?.validate((errors) => {
if (errors) return ''
mailboxBindingSubmitBtnLoading.value = true
fetchVerifyCode<string>(emailInfoForm.value.email, emailInfoForm.value.verifyCode)
.then((res) => {
if (res.code !== 0) return ''
fetchUserInfoUpdate({
email: emailInfoForm.value.email,
})
.then(() => {
userStore.fetchUpdateUserInfo()
window.$message.success(t('personal_settings_module.binding_successful'))
})
.finally(() => {
isShowMailboxBindingModal.value = false
mailboxBindingSubmitBtnLoading.value = false
isShowCountdown.value = false
})
})
.catch(() => {
mailboxBindingSubmitBtnLoading.value = false
})
})
}
function countdownRender({ seconds, minutes }: { seconds: number; minutes: number }) {
if (minutes && minutes === 1) {
return '60 s'
}
return `${seconds} s`
}
function onCountdownFinish() {
isShowCountdown.value = false
}
function handleSMSCodeGain() {
emailInfoFormRef.value?.validate(
(errors) => {
if (errors) return ''
countdownDuration.value = 60000
ss.set('MAILBOX_BINDING_CODE', Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
fetchEmailCode(encodeURIComponent(emailInfoForm.value.email)).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
},
(rule) => {
return rule.key === 'email'
},
)
}
</script>
<template>
<n-modal v-model:show="isShowMailboxBindingModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:title="t('personal_settings_module.email_binding')"
:bordered="false"
size="medium"
closable
@close="() => (isShowMailboxBindingModal = false)"
>
<n-form
ref="emailInfoFormRef"
label-placement="left"
label-width="auto"
:model="emailInfoForm"
:rules="emailInfoFormRules"
>
<n-form-item :label="t('common_module.email')" path="email">
<n-input v-model:value="emailInfoForm.email" :placeholder="t('login_module.please_enter_your_email_address')">
<template #suffix>
<span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span>
<span
v-show="!isShowCountdown"
class="text-theme-color cursor-pointer text-[12px]"
@click="handleSMSCodeGain"
>
{{ t('login_module.get_verification_code') }}
</span>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item :label="t('common_module.verificationCode')" path="verifyCode">
<n-input
v-model:value="emailInfoForm.verifyCode"
:placeholder="t('login_module.please_enter_the_verification_code')"
:maxlength="6"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowMailboxBindingModal = false)">
{{ t('common_module.cancel_btn_text') }}
</n-button>
<n-button type="primary" :loading="mailboxBindingSubmitBtnLoading" @click="handleMailboxBindingSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
</template>
<script setup lang="ts">
import { fetchEmailCode, fetchSMSCode, fetchUserPasswordUpdate, fetchVerifyCode } from '@/apis/user'
import { useUserStore } from '@/store/modules/user'
import { ss } from '@/utils/storage'
import type { CountdownInst, FormInst, FormItemRule } from 'naive-ui'
import SparkMD5 from 'spark-md5'
import { onMounted, ref, shallowReadonly, useTemplateRef, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const countdownRef = useTemplateRef<CountdownInst>('countdownRef')
const userStore = useUserStore()
const isShowPasswordChangeModal = defineModel<boolean>('isShowPasswordChangeModal', { default: false })
const passwordChangeSubmitBtnLoading = ref(false)
const passwordInfoForm = ref<{
verificationMethod: 'email' | 'sms'
verifyCode: string
password: string
confirmPassword: string
}>({
verificationMethod: 'email',
verifyCode: '',
password: '',
confirmPassword: '',
})
const isShowCountdown = ref(false)
const countdownDuration = ref<number>(60000)
const countdownActive = ref(true)
const passwordFormRules = shallowReadonly({
verifyCode: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_the_verification_code'))
} else if (value.length < 6) {
return new Error(t('personal_settings_module.please_enter_the_correct_verification_code'))
}
return
},
},
password: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_your_new_password'))
} else if (value.length <= 6) {
return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters'))
}
return
},
},
confirmPassword: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_confirm_new_password'))
} else if (value !== passwordInfoForm.value.password) {
return new Error(
t('personal_settings_module.verify_that_the_new_password_is_inconsistent_with_the_new_password'),
)
}
return
},
},
})
const verificationMethodDisable = ref({
email: false,
sms: false,
})
watchEffect(() => {
if (userStore.userInfo) {
!userStore.userInfo.email
? (verificationMethodDisable.value.email = true)
: (verificationMethodDisable.value.email = false)
!userStore.userInfo.mobilePhone
? (verificationMethodDisable.value.sms = true)
: (verificationMethodDisable.value.sms = false)
if (userStore.userInfo.email) {
passwordInfoForm.value.verificationMethod = 'email'
} else if (userStore.userInfo.mobilePhone) {
passwordInfoForm.value.verificationMethod = 'sms'
}
}
})
onMounted(() => {
let timeStringDraft = ss.get('PASSWORD_CHANGE_CODE')
if (timeStringDraft) {
const time = Math.floor(Date.now() - parseInt(timeStringDraft))
if (time < 60000) {
countdownDuration.value = 60000 - time
countdownRef.value?.reset()
isShowCountdown.value = true
}
}
})
function countdownRender({ seconds, minutes }: { seconds: number; minutes: number }) {
if (minutes && minutes === 1) {
return '60 s'
}
return `${seconds} s`
}
function onModalAfterLeave() {
passwordInfoForm.value = {
password: '',
confirmPassword: '',
verificationMethod: 'email',
verifyCode: '',
}
}
function handlePasswordChangeSubmit() {
passwordInfoFormRef.value?.validate((errors) => {
if (errors) return ''
passwordChangeSubmitBtnLoading.value = true
fetchVerifyCode<string>(
passwordInfoForm.value.verificationMethod === 'email' ? userStore.userInfo.email : userStore.userInfo.mobilePhone,
passwordInfoForm.value.verifyCode,
)
.then((res) => {
if (res.code !== 0) return ''
fetchUserPasswordUpdate(res.data, SparkMD5.hash(passwordInfoForm.value.confirmPassword)).then(() => {
window.$message.success(t('common_module.successful_update'))
isShowPasswordChangeModal.value = false
passwordChangeSubmitBtnLoading.value = false
isShowCountdown.value = false
})
})
.catch(() => {
passwordChangeSubmitBtnLoading.value = false
})
})
}
function onCountdownFinish() {
isShowCountdown.value = false
}
function handleSMSCodeGain() {
countdownDuration.value = 60000
ss.set('PASSWORD_CHANGE_CODE', Date.now())
countdownRef.value?.reset()
isShowCountdown.value = true
if (passwordInfoForm.value.verificationMethod === 'sms') {
fetchSMSCode(userStore.userInfo.mobilePhone).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
} else if (passwordInfoForm.value.verificationMethod === 'email') {
fetchEmailCode(encodeURIComponent(userStore.userInfo.email)).then((res) => {
if (res.code !== 0) return ''
window.$message.success(t('login_module.successful'))
})
}
}
</script>
<template>
<n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:title="t('personal_settings_module.password_change')"
:bordered="false"
size="medium"
closable
@close="() => (isShowPasswordChangeModal = false)"
>
<n-form
ref="passwordInfoFormRef"
label-placement="left"
label-width="auto"
:model="passwordInfoForm"
:rules="passwordFormRules"
>
<n-form-item :label="t('personal_settings_module.obtaining_the_verification_code')">
<div>
<n-radio-group v-model:value="passwordInfoForm.verificationMethod" name="verificationMethod" size="small">
<n-radio-button :disabled="verificationMethodDisable.email" value="email">
{{ t('common_module.email') }}
</n-radio-button>
<n-radio-button :disabled="verificationMethodDisable.sms" value="sms">
{{ t('common_module.sms') }}
</n-radio-button>
</n-radio-group>
</div>
</n-form-item>
<n-form-item :label="t('common_module.verificationCode')" path="verifyCode">
<n-input
v-model:value="passwordInfoForm.verifyCode"
:placeholder="t('login_module.please_enter_the_verification_code')"
:maxlength="6"
>
<template #suffix>
<span class="mx-[10px] inline-block h-[50%] w-[2px] bg-[#e0e0e6]"></span>
<span
v-show="!isShowCountdown"
class="text-theme-color cursor-pointer text-[12px]"
@click="handleSMSCodeGain"
>
{{ t('login_module.get_verification_code') }}
</span>
<div v-show="isShowCountdown" class="inline-block w-[50px] text-center">
<n-countdown
ref="countdownRef"
:duration="countdownDuration"
:active="countdownActive"
:render="countdownRender"
:on-finish="onCountdownFinish"
/>
</div>
</template>
</n-input>
</n-form-item>
<n-form-item :label="t('personal_settings_module.new_password')" path="password">
<n-input
v-model:value="passwordInfoForm.password"
type="password"
show-password-on="click"
class="font-sans"
:placeholder="t('personal_settings_module.please_enter_your_new_password')"
/>
</n-form-item>
<n-form-item :label="t('personal_settings_module.confirm_new_password')" path="confirmPassword">
<n-input
v-model:value="passwordInfoForm.confirmPassword"
type="password"
show-password-on="click"
class="font-sans"
:placeholder="t('personal_settings_module.please_enter_confirm_new_password')"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowPasswordChangeModal = false)">
{{ t('common_module.cancel_btn_text') }}
</n-button>
<n-button type="primary" :loading="passwordChangeSubmitBtnLoading" @click="handlePasswordChangeSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import { computed, reactive, ref, shallowReadonly, useTemplateRef, nextTick } from 'vue'
import type { FormItemRule, FormInst, InputInst } from 'naive-ui'
import isEmail from 'validator/es/lib/isEmail'
import { fetchUserInfoUpdate } from '@/apis/user'
import ImageCropper from '@/components/image-cropper/image-cropper.vue'
import { useSystemLanguageStore } from '@/store/modules/system-language'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/store/modules/user'
import type { InputInst } from 'naive-ui'
import { computed, nextTick, reactive, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import PasswordChangeModal from './components/password-change-modal.vue'
import MailboxBindingModal from './components/mailbox-binding-modal.vue'
import { fetchUpload } from '@/apis/upload'
const userStore = useUserStore()
const systemLanguageStore = useSystemLanguageStore()
......@@ -14,11 +18,10 @@ const { t } = useI18n()
const inputRefs = useTemplateRef<InputInst | InputInst[]>('inputRefs')
const avatarInputRef = useTemplateRef('avatarInputRef')
const emailInfoFormRef = useTemplateRef<FormInst>('emailInfoFormRef')
const passwordInfoFormRef = useTemplateRef<FormInst>('passwordInfoFormRef')
const imageCropperRef = useTemplateRef<InstanceType<typeof ImageCropper> | null>('imageCropperRef')
const userInfoForm = reactive({
nickName: userStore.userInfo.nickName,
const userInfoForm = ref({
nickName: '',
remark: userStore.userInfo.remark,
})
......@@ -27,70 +30,12 @@ const userInfoFormItemEdit = reactive({
remark: false,
})
/* 邮箱绑定 */
const isShowMailboxBindingModal = ref(false)
const mailboxBindingSubmitBtnLoading = ref(false)
const emailInfoForm = ref({
email: '',
})
const emailInfoFormRules = shallowReadonly({
email: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('login_module.please_enter_your_email_address'))
} else if (!isEmail(value)) {
return new Error(t('login_module.please_enter_the_correct_email_address'))
}
return
},
},
})
/* 密码修改 */
const isShowPasswordChangeModal = ref(false)
const passwordChangeSubmitBtnLoading = ref(false)
const passwordInfoForm = ref({
password: '',
confirmPassword: '',
})
const passwordFormRules = shallowReadonly({
password: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_your_new_password'))
} else if (value.length <= 6) {
return new Error(t('personal_settings_module.the_password_contains_a_maximum_of_6_characters'))
}
return
},
},
confirmPassword: {
required: true,
trigger: 'blur',
validator: (_rule: FormItemRule, value: string) => {
if (!value) {
return new Error(t('personal_settings_module.please_enter_confirm_new_password'))
} else if (value !== passwordInfoForm.value.password) {
return new Error(
t('personal_settings_module.verify_that_the_new_password_is_inconsistent_with_the_new_password'),
)
}
return
},
},
})
const isShowMailboxBindingModal = ref(false)
const listItemLabelWidth = ref(systemLanguageStore.currentLanguageInfo.key === 'en' ? '168px' : '100px')
// const confirmBtnLoading = ref(true)
const saveBtnLoading = ref(false)
const currentLanguage = computed({
get() {
......@@ -109,6 +54,18 @@ const languageOptions = computed(() => {
return systemLanguageStore.languageOptions.map((item) => ({ value: item.key, label: item.label }))
})
function userInfoUpdate(userInfo: {
nickName?: string
avatarUrl?: string
mobilePhone?: string
email?: string
remark?: string
}) {
return fetchUserInfoUpdate(userInfo).then(() => {
userStore.fetchUpdateUserInfo()
})
}
function handleAvatarUpdate() {
avatarInputRef.value && avatarInputRef.value.click()
}
......@@ -117,50 +74,64 @@ function handleAvatarUpload(e: Event) {
const target = e.target as HTMLInputElement
const file = target.files && target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
const result = e.target?.result
console.log('🟰🟰🟰🟰🟰🟰test🟰🟰🟰🟰🟰🟰')
console.log(result)
}
if (imageCropperRef.value && file) {
imageCropperRef.value.cropImage(URL.createObjectURL(file)).then((file) => {
const formData = new FormData()
formData.append('file', file)
const loadingCtl = window.$message.loading(t('common_module.uploading'), { duration: 0 })
fetchUpload<string>(formData)
.then((res) => {
userInfoUpdate({
avatarUrl: res.data,
})
.then(() => {
window.$message.success(t('common_module.save_success_message'))
userStore.fetchUpdateUserInfo()
})
.catch(() => {
window.$message.loading(t('common_module.save_fail_message'))
})
.finally(() => {
loadingCtl.destroy()
})
})
.catch(() => {
window.$message.loading(t('common_module.save_fail_message'))
})
})
}
}
function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit, status: boolean) {
userInfoFormItemEdit[key] = status
function handleUserInfoFormItemEditUpdate(key: keyof typeof userInfoFormItemEdit, isSave = false) {
userInfoForm.value[key] = userInfo.value[key]
nextTick(() => {
if (inputRefs.value && !Array.isArray(inputRefs.value)) {
inputRefs.value.focus()
}
})
}
userInfoFormItemEdit[key] = !userInfoFormItemEdit[key]
function onModalAfterLeave() {
emailInfoForm.value.email = ''
}
function handleMailboxBindingSubmit() {
emailInfoFormRef.value?.validate((errors) => {
if (errors) return ''
console.log('🟰🟰🟰🟰🟰🟰提交🟰🟰🟰🟰🟰🟰')
})
}
function handlePasswordChangeSubmit() {
passwordInfoFormRef.value?.validate((errors) => {
if (errors) return ''
if (userInfoFormItemEdit[key]) {
nextTick(() => {
if (inputRefs.value && !Array.isArray(inputRefs.value)) {
inputRefs.value.focus()
}
})
}
console.log('🟰🟰🟰🟰🟰🟰提交🟰🟰🟰🟰🟰🟰')
})
if (isSave) {
userInfoUpdate({
[key]: userInfoForm.value[key],
}).then(() => {
window.$message.success(t('common_module.successful_update'))
})
}
}
</script>
<template>
<div class="h-full min-h-fit flex-col rounded-[20px] bg-white p-6 shadow-[0_2px_2px_#0000000a]">
<ImageCropper ref="imageCropperRef" />
<div class="ml-[56px] pt-[6px]">
<ul>
<li class="font-600 flex items-center text-[18px]">
......@@ -204,10 +175,15 @@ function handlePasswordChangeSubmit() {
</div>
<div class="ml-[20px] flex items-center">
<n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('nickName', false)">
<n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('nickName')">
<i class="iconfont icon-close"></i>
</n-button>
<n-button type="primary" size="tiny" @click="handleUserInfoFormItemEditUpdate('nickName', true)">
<n-button
type="primary"
size="tiny"
:loading="saveBtnLoading"
@click="handleUserInfoFormItemEditUpdate('nickName', true)"
>
<i class="iconfont icon-queren"></i>
</n-button>
</div>
......@@ -218,7 +194,7 @@ function handlePasswordChangeSubmit() {
<i
class="iconfont icon-edit1 cursor-pointer px-[5px]"
@click="handleUserInfoFormItemEditUpdate('nickName', true)"
@click="handleUserInfoFormItemEditUpdate('nickName')"
></i>
</div>
</li>
......@@ -241,10 +217,15 @@ function handlePasswordChangeSubmit() {
</div>
<div class="ml-[20px] flex items-center">
<n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('remark', false)">
<n-button class="!mr-[6px]" size="tiny" @click="handleUserInfoFormItemEditUpdate('remark')">
<i class="iconfont icon-close"></i>
</n-button>
<n-button type="primary" size="tiny" @click="handleUserInfoFormItemEditUpdate('remark', true)">
<n-button
type="primary"
size="tiny"
:loading="saveBtnLoading"
@click="handleUserInfoFormItemEditUpdate('remark', true)"
>
<i class="iconfont icon-queren"></i>
</n-button>
</div>
......@@ -255,7 +236,7 @@ function handlePasswordChangeSubmit() {
<i
class="iconfont icon-edit1 cursor-pointer px-[5px]"
@click="handleUserInfoFormItemEditUpdate('remark', true)"
@click="handleUserInfoFormItemEditUpdate('remark')"
></i>
</div>
</li>
......@@ -309,96 +290,14 @@ function handlePasswordChangeSubmit() {
</ul>
</div>
<n-modal v-model:show="isShowMailboxBindingModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:title="t('personal_settings_module.email_binding')"
:bordered="false"
size="medium"
closable
@close="() => (isShowMailboxBindingModal = false)"
>
<n-form
ref="emailInfoFormRef"
label-placement="left"
label-width="auto"
:model="emailInfoForm"
:rules="emailInfoFormRules"
>
<n-form-item :label="t('common_module.email')" path="email">
<n-input
v-model:value="emailInfoForm.email"
:placeholder="t('login_module.please_enter_your_email_address')"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowMailboxBindingModal = false)">{{
t('common_module.cancel_btn_text')
}}</n-button>
<n-button type="primary" :loading="mailboxBindingSubmitBtnLoading" @click="handleMailboxBindingSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
<n-modal v-model:show="isShowPasswordChangeModal" :mask-closable="false" :on-after-leave="onModalAfterLeave">
<n-card
class="!w-[600px]"
:title="t('personal_settings_module.password_change')"
:bordered="false"
size="medium"
closable
@close="() => (isShowPasswordChangeModal = false)"
>
<n-form
ref="passwordInfoFormRef"
label-placement="left"
label-width="auto"
:model="passwordInfoForm"
:rules="passwordFormRules"
>
<n-form-item :label="t('personal_settings_module.new_password')" path="password">
<n-input
v-model:value="passwordInfoForm.password"
:placeholder="t('personal_settings_module.please_enter_your_new_password')"
/>
</n-form-item>
<n-form-item :label="t('personal_settings_module.confirm_new_password')" path="confirmPassword">
<n-input
v-model:value="passwordInfoForm.confirmPassword"
:placeholder="t('personal_settings_module.please_enter_confirm_new_password')"
/>
</n-form-item>
</n-form>
<template #footer>
<div class="text-end">
<n-space justify="end">
<n-button @click="() => (isShowPasswordChangeModal = false)">{{
t('common_module.cancel_btn_text')
}}</n-button>
<n-button type="primary" :loading="passwordChangeSubmitBtnLoading" @click="handlePasswordChangeSubmit">
{{ t('common_module.confirm_btn_text') }}
</n-button>
</n-space>
</div>
</template>
</n-card>
</n-modal>
<PasswordChangeModal v-model:is-show-password-change-modal="isShowPasswordChangeModal" />
<MailboxBindingModal v-model:is-show-mailbox-binding-modal="isShowMailboxBindingModal" />
</div>
</template>
<style lang="scss" scoped>
.list-item-label {
/* stylelint-disable-next-line value-keyword-case */
width: v-bind(listItemLabelWidth);
width: v-bind('listItemLabelWidth');
text-align: end;
}
</style>
......@@ -89,6 +89,8 @@ declare namespace I18n {
language: string
change: string
bind: string
sms: string
verificationCode: string
dialogue_module: {
continue_question_message: string
......@@ -466,6 +468,9 @@ declare namespace I18n {
verify_that_the_new_password_is_inconsistent_with_the_new_password: string
please_enter_the_account_nickname: string
please_enter_a_personal_profile: string
please_enter_the_correct_verification_code: string
binding_successful: string
obtaining_the_verification_code: string
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment