You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
paopao-ce/web/src/views/Setting.vue

599 lines
19 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<main-nav title="设置" />
<n-card title="基本信息" size="small" class="setting-card">
<div class="base-line avatar">
<n-avatar
class="avatar-img"
:size="80"
:src="store.state.userInfo.avatar"
/>
<n-upload
ref="avatarRef"
:action="uploadGateway"
:headers="{
Authorization: uploadToken,
}"
:data="{
type: uploadType,
}"
@before-upload="beforeUpload"
@finish="finishUpload"
>
<n-button size="small"></n-button>
</n-upload>
</div>
<div class="base-line">
<span class="base-label"></span>
<div v-if="!showNicknameEdit">
{{ store.state.userInfo.nickname }}
</div>
<n-input
ref="inputInstRef"
v-show="showNicknameEdit"
class="nickname-input"
v-model:value="store.state.userInfo.nickname"
type="text"
size="small"
placeholder="请输入昵称"
@blur="handleNicknameChange"
:maxlength="16"
/>
<n-button
quaternary
round
type="success"
size="small"
v-if="!showNicknameEdit"
@click="handleNicknameShow"
>
<template #icon>
<n-icon>
<edit />
</n-icon>
</template>
</n-button>
</div>
<div class="base-line">
<span class="base-label"></span> @{{
store.state.userInfo.username
}}
</div>
</n-card>
<n-card title="手机号" size="small" class="setting-card">
<div
v-if="
store.state.userInfo.phone &&
store.state.userInfo.phone.length > 0
"
>
{{ store.state.userInfo.phone }}
<n-button
quaternary
round
type="success"
v-if="!showPhoneBind"
@click="showPhoneBind = true"
>
</n-button>
</div>
<div v-else>
<n-alert title="手机绑定提示" type="warning">
~
<a
class="hash-link"
@click="showPhoneBind = true"
v-if="!showPhoneBind"
>
</a>
</n-alert>
</div>
<div class="phone-bind-wrap" v-if="showPhoneBind">
<n-form
ref="phoneFormRef"
:model="modelData"
:rules="bindRules"
>
<n-form-item path="phone" label="手机号">
<n-input
:value="modelData.phone"
@update:value="(v: string) => (modelData.phone = v.trim())"
placeholder="请输入中国大陆手机号"
@keydown.enter.prevent
/>
</n-form-item>
<n-form-item path="img_captcha" label="图形验证码">
<div class="captcha-img-wrap">
<n-input
v-model:value="modelData.imgCaptcha"
placeholder="请输入图形验证码后获取验证码"
/>
<div class="captcha-img">
<img
v-if="modelData.b64s"
:src="modelData.b64s"
@click="loadCaptcha"
/>
</div>
</div>
</n-form-item>
<n-form-item path="phone_captcha" label="短信验证码">
<n-input-group>
<n-input
v-model:value="modelData.phone_captcha"
placeholder="请输入收到的短信验证码"
/>
<n-button
type="primary"
ghost
:disabled="smsDisabled"
:loading="sending"
@click="sendPhoneCaptcha"
>
{{
smsCounter > 0 && smsDisabled
? smsCounter + 's'
: ''
}}
</n-button>
</n-input-group>
</n-form-item>
<n-row :gutter="[0, 24]">
<n-col :span="24">
<div class="form-submit-wrap">
<n-button
quaternary
round
@click="showPhoneBind = false"
>
</n-button>
<n-button
secondary
round
type="primary"
:loading="binding"
@click="handlePhoneBind"
>
</n-button>
</div>
</n-col>
</n-row>
</n-form>
</div>
</n-card>
<n-card title="账户安全" size="small" class="setting-card">
<n-button
quaternary
round
type="success"
v-if="!showPasswordSetting"
@click="showPasswordSetting = true"
>
</n-button>
<div class="phone-bind-wrap" v-if="showPasswordSetting">
<n-form ref="formRef" :model="modelData" :rules="passwordRules">
<n-form-item path="old_password" label="旧密码">
<n-input
v-model:value="modelData.old_password"
type="password"
placeholder="请输入当前密码"
@keydown.enter.prevent
/>
</n-form-item>
<n-form-item path="password" label="新密码">
<n-input
v-model:value="modelData.password"
type="password"
placeholder="请输入新密码"
@input="handlePasswordInput"
@keydown.enter.prevent
/>
</n-form-item>
<n-form-item
ref="rPasswordFormItemRef"
first
path="reenteredPassword"
label="重复密码"
>
<n-input
v-model:value="modelData.reenteredPassword"
:disabled="!modelData.password"
type="password"
placeholder="请再次输入密码"
@keydown.enter.prevent
/>
</n-form-item>
<n-row :gutter="[0, 24]">
<n-col :span="24">
<div class="form-submit-wrap">
<n-button
quaternary
round
@click="showPasswordSetting = false"
>
</n-button>
<n-button
secondary
round
type="primary"
:loading="passwordSetting"
@click="handleValidateButtonClick"
>
</n-button>
</div>
</n-col>
</n-row>
</n-form>
</div>
</n-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, reactive } from 'vue';
import { useStore } from 'vuex';
import { Edit } from '@vicons/tabler';
import {
getCaptcha,
sendCaptcha,
bindUserPhone,
changePassword,
changeNickname,
changeAvatar,
} from '@/api/user';
import type {
UploadInst,
FormItemRule,
FormItemInst,
FormInst,
InputInst,
} from 'naive-ui';
const uploadGateway = import.meta.env.VITE_HOST + '/v1/attachment';
const uploadToken = 'Bearer ' + localStorage.getItem('PAOPAO_TOKEN');
const uploadType = ref('public/avatar');
const store = useStore();
const sending = ref(false);
const binding = ref(false);
const avatarRef = ref<UploadInst>();
const inputInstRef = ref<InputInst>();
const showNicknameEdit = ref(false);
const passwordSetting = ref(false);
const showPasswordSetting = ref(false);
const smsDisabled = ref(false);
const smsCounter = ref(60);
const showPhoneBind = ref(false);
const phoneFormRef = ref<FormInst>();
const formRef = ref<FormInst>();
const rPasswordFormItemRef = ref<FormItemInst>();
const modelData = reactive({
id: '',
b64s: '',
imgCaptcha: '',
phone: '',
phone_captcha: '',
password: '',
old_password: '',
reenteredPassword: '',
});
const beforeUpload = async (data: any) => {
// 图片类型校验
if (
uploadType.value === 'public/avatar' &&
!['image/png', 'image/jpg', 'image/jpeg'].includes(data.file.file?.type)
) {
window.$message.warning(' png/jpg ');
return false;
}
if (uploadType.value === 'image' && data.file.file?.size > 1048576) {
window.$message.warning('1MB');
return false;
}
return true;
};
const finishUpload = ({ file, event }: any): any => {
try {
let data = JSON.parse(event.target?.response);
if (data.code === 0) {
if (uploadType.value === 'public/avatar') {
changeAvatar({
avatar: data.data.content,
})
.then((res) => {
window.$message.success('');
avatarRef.value?.clear();
store.commit('updateUserinfo', {
...store.state.userInfo,
avatar: data.data.content,
});
})
.catch((err) => {
console.log(err);
});
}
}
} catch (error) {
window.$message.error('');
}
};
const validatePasswordStartWith = (rule: FormItemRule, value: any) => {
return (
!!modelData.password &&
(modelData.password as any).startsWith(value) &&
(modelData.password as any).length >= value.length
);
};
const validatePasswordSame = (rule: FormItemRule, value: any) => {
return value === modelData.password;
};
const handlePasswordInput = () => {
if (modelData.reenteredPassword) {
rPasswordFormItemRef.value?.validate({ trigger: 'password-input' });
}
};
const handleValidateButtonClick = (e: MouseEvent) => {
e.preventDefault();
formRef.value?.validate((errors) => {
if (!errors) {
passwordSetting.value = true;
changePassword({
password: modelData.password,
old_password: modelData.old_password,
})
.then((res) => {
passwordSetting.value = false;
showPasswordSetting.value = false;
window.$message.success('');
// 用户退出登录
store.commit('userLogout');
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', 'signin');
})
.catch((err) => {
passwordSetting.value = false;
});
}
});
};
const handlePhoneBind = (e: MouseEvent) => {
e.preventDefault();
phoneFormRef.value?.validate((errors) => {
if (!errors) {
binding.value = true;
bindUserPhone({
phone: modelData.phone,
captcha: modelData.phone_captcha,
})
.then((res) => {
binding.value = false;
showPhoneBind.value = false;
window.$message.success('');
store.commit('updateUserinfo', {
...store.state.userInfo,
phone: modelData.phone,
});
modelData.id = '';
modelData.b64s = '';
modelData.imgCaptcha = '';
modelData.phone = '';
modelData.phone_captcha = '';
})
.catch((err) => {
binding.value = false;
});
}
});
};
const loadCaptcha = () => {
getCaptcha()
.then((res) => {
modelData.id = res.id;
modelData.b64s = res.b64s;
})
.catch((err) => {
console.log(err);
});
};
const handleNicknameChange = () => {
changeNickname({
nickname: store.state.userInfo.nickname || '',
})
.then((res) => {
showNicknameEdit.value = false;
window.$message.success('');
})
.catch((err) => {
showNicknameEdit.value = true;
});
};
const sendPhoneCaptcha = () => {
if (smsCounter.value > 0 && smsDisabled.value) {
return;
}
if (modelData.imgCaptcha === '') {
window.$message.warning('');
return;
}
sending.value = true;
sendCaptcha({
phone: modelData.phone,
img_captcha: modelData.imgCaptcha,
img_captcha_id: modelData.id,
})
.then((res) => {
smsDisabled.value = true;
sending.value = false;
window.$message.success('');
let s = setInterval(() => {
smsCounter.value--;
if (smsCounter.value === 0) {
clearInterval(s);
smsCounter.value = 60;
smsDisabled.value = false;
}
}, 1000);
})
.catch((err) => {
sending.value = false;
if (err.code === 20012) {
loadCaptcha();
}
console.log(err);
});
};
const bindRules = {
phone: [
{
required: true,
message: '',
trigger: ['input'],
validator: (rule: FormItemRule, value: any) => {
return /^[1]+[3-9]{1}\d{9}$/.test(value);
},
},
],
phone_captcha: [
{
required: true,
message: '',
},
],
};
const passwordRules = {
password: [
{
required: true,
message: '',
},
],
old_password: [
{
required: true,
message: '',
},
],
reenteredPassword: [
{
required: true,
message: '',
trigger: ['input', 'blur'],
},
{
validator: validatePasswordStartWith,
message: '',
trigger: 'input',
},
{
validator: validatePasswordSame,
message: '',
trigger: ['blur', 'password-input'],
},
],
};
const handleNicknameShow = () => {
showNicknameEdit.value = true;
setTimeout(() => {
inputInstRef.value?.focus();
}, 30);
};
onMounted(() => {
if (store.state.userInfo.id === 0) {
store.commit('triggerAuth', true);
store.commit('triggerAuthKey', 'signin');
}
loadCaptcha();
});
</script>
<style lang="less" scoped>
.setting-card {
margin-top: -1px;
border-radius: 0;
.form-submit-wrap {
display: flex;
justify-content: flex-end;
}
.base-line {
line-height: 2;
display: flex;
align-items: center;
.base-label {
opacity: 0.75;
margin-right: 12px;
}
.nickname-input {
margin-right: 10px;
width: 120px;
}
}
.avatar {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
.avatar-img {
margin-bottom: 10px;
}
}
.hash-link {
margin-left: 12px;
}
.phone-bind-wrap {
margin-top: 20px;
.captcha-img-wrap {
width: 100%;
display: flex;
align-items: center;
}
.captcha-img {
width: 125px;
height: 34px;
border-radius: 3px;
margin-left: 10px;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
}
</style>