陈薪名
10 months ago
7 changed files with 468 additions and 0 deletions
@ -0,0 +1,92 @@ |
|||||
|
<template> |
||||
|
<Form ref="formRef" :labelWidth="200" :rules="rules" :schema="schema"> |
||||
|
<template #sex="form"> |
||||
|
<el-radio-group v-model="form['sex']"> |
||||
|
<el-radio :label="1">{{ t('profile.user.man') }}</el-radio> |
||||
|
<el-radio :label="2">{{ t('profile.user.woman') }}</el-radio> |
||||
|
</el-radio-group> |
||||
|
</template> |
||||
|
</Form> |
||||
|
<div style="text-align: center"> |
||||
|
<XButton :title="t('common.save')" type="primary" @click="submit()" /> |
||||
|
<XButton :title="t('common.reset')" type="danger" @click="init()" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import type { FormRules } from 'element-plus' |
||||
|
import { FormSchema } from '@/types/form' |
||||
|
import type { FormExpose } from '@/components/Form' |
||||
|
import { |
||||
|
getUserProfile, |
||||
|
updateUserProfile, |
||||
|
UserProfileUpdateReqVO |
||||
|
} from '@/api/system/user/profile' |
||||
|
|
||||
|
defineOptions({ name: 'BasicInfo' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const message = useMessage() // 消息弹窗 |
||||
|
// 表单校验 |
||||
|
const rules = reactive<FormRules>({ |
||||
|
nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }], |
||||
|
email: [ |
||||
|
{ required: true, message: t('profile.rules.mail'), trigger: 'blur' }, |
||||
|
{ |
||||
|
type: 'email', |
||||
|
message: t('profile.rules.truemail'), |
||||
|
trigger: ['blur', 'change'] |
||||
|
} |
||||
|
], |
||||
|
mobile: [ |
||||
|
{ required: true, message: t('profile.rules.phone'), trigger: 'blur' }, |
||||
|
{ |
||||
|
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, |
||||
|
message: t('profile.rules.truephone'), |
||||
|
trigger: 'blur' |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
const schema = reactive<FormSchema[]>([ |
||||
|
{ |
||||
|
field: 'nickname', |
||||
|
label: t('profile.user.nickname'), |
||||
|
component: 'Input' |
||||
|
}, |
||||
|
{ |
||||
|
field: 'mobile', |
||||
|
label: t('profile.user.mobile'), |
||||
|
component: 'Input' |
||||
|
}, |
||||
|
{ |
||||
|
field: 'email', |
||||
|
label: t('profile.user.email'), |
||||
|
component: 'Input' |
||||
|
}, |
||||
|
{ |
||||
|
field: 'sex', |
||||
|
label: t('profile.user.sex'), |
||||
|
component: 'InputNumber', |
||||
|
value: 0 |
||||
|
} |
||||
|
]) |
||||
|
const formRef = ref<FormExpose>() // 表单 Ref |
||||
|
const submit = () => { |
||||
|
const elForm = unref(formRef)?.getElFormRef() |
||||
|
if (!elForm) return |
||||
|
elForm.validate(async (valid) => { |
||||
|
if (valid) { |
||||
|
const data = unref(formRef)?.formModel as UserProfileUpdateReqVO |
||||
|
await updateUserProfile(data) |
||||
|
message.success(t('common.updateSuccess')) |
||||
|
await init() |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
const init = async () => { |
||||
|
const res = await getUserProfile() |
||||
|
unref(formRef)?.setValues(res) |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await init() |
||||
|
}) |
||||
|
</script> |
@ -0,0 +1,99 @@ |
|||||
|
<template> |
||||
|
<div> |
||||
|
<div class="text-center"> |
||||
|
<UserAvatar :img="userInfo?.avatar" /> |
||||
|
</div> |
||||
|
<ul class="list-group list-group-striped"> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:user" /> |
||||
|
{{ t('profile.user.username') }} |
||||
|
<div class="pull-right">{{ userInfo?.username }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:phone" /> |
||||
|
{{ t('profile.user.mobile') }} |
||||
|
<div class="pull-right">{{ userInfo?.mobile }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="fontisto:email" /> |
||||
|
{{ t('profile.user.email') }} |
||||
|
<div class="pull-right">{{ userInfo?.email }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="carbon:tree-view-alt" /> |
||||
|
{{ t('profile.user.dept') }} |
||||
|
<div v-if="userInfo?.dept" class="pull-right">{{ userInfo?.dept.name }}</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:suitcase" /> |
||||
|
{{ t('profile.user.posts') }} |
||||
|
<div v-if="userInfo?.posts" class="pull-right"> |
||||
|
{{ userInfo?.posts.map((post) => post.name).join(',') }} |
||||
|
</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="icon-park-outline:peoples" /> |
||||
|
{{ t('profile.user.roles') }} |
||||
|
<div v-if="userInfo?.roles" class="pull-right"> |
||||
|
{{ userInfo?.roles.map((role) => role.name).join(',') }} |
||||
|
</div> |
||||
|
</li> |
||||
|
<li class="list-group-item"> |
||||
|
<Icon class="mr-5px" icon="ep:calendar" /> |
||||
|
{{ t('profile.user.createTime') }} |
||||
|
<div class="pull-right">{{ formatDate(userInfo?.createTime) }}</div> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { formatDate } from '@/utils/formatTime' |
||||
|
import UserAvatar from './UserAvatar.vue' |
||||
|
|
||||
|
import { getUserProfile, ProfileVO } from '@/api/system/user/profile' |
||||
|
|
||||
|
defineOptions({ name: 'ProfileUser' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const userInfo = ref<ProfileVO>() |
||||
|
const getUserInfo = async () => { |
||||
|
const users = await getUserProfile() |
||||
|
userInfo.value = users |
||||
|
} |
||||
|
onMounted(async () => { |
||||
|
await getUserInfo() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.text-center { |
||||
|
position: relative; |
||||
|
height: 120px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.list-group-striped > .list-group-item { |
||||
|
padding-right: 0; |
||||
|
padding-left: 0; |
||||
|
border-right: 0; |
||||
|
border-left: 0; |
||||
|
border-radius: 0; |
||||
|
} |
||||
|
|
||||
|
.list-group { |
||||
|
padding-left: 0; |
||||
|
list-style: none; |
||||
|
} |
||||
|
|
||||
|
.list-group-item { |
||||
|
padding: 11px 0; |
||||
|
margin-bottom: -1px; |
||||
|
font-size: 13px; |
||||
|
border-top: 1px solid #e7eaec; |
||||
|
border-bottom: 1px solid #e7eaec; |
||||
|
} |
||||
|
|
||||
|
.pull-right { |
||||
|
float: right !important; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,73 @@ |
|||||
|
<template> |
||||
|
<el-form ref="formRef" :model="password" :rules="rules" :label-width="200"> |
||||
|
<el-form-item :label="t('profile.password.oldPassword')" prop="oldPassword"> |
||||
|
<InputPassword v-model="password.oldPassword" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item :label="t('profile.password.newPassword')" prop="newPassword"> |
||||
|
<InputPassword v-model="password.newPassword" strength /> |
||||
|
</el-form-item> |
||||
|
<el-form-item :label="t('profile.password.confirmPassword')" prop="confirmPassword"> |
||||
|
<InputPassword v-model="password.confirmPassword" strength /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<XButton :title="t('common.save')" type="primary" @click="submit(formRef)" /> |
||||
|
<XButton :title="t('common.reset')" type="danger" @click="reset(formRef)" /> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import type { FormInstance, FormRules } from 'element-plus' |
||||
|
|
||||
|
import { InputPassword } from '@/components/InputPassword' |
||||
|
import { updateUserPassword } from '@/api/system/user/profile' |
||||
|
|
||||
|
defineOptions({ name: 'ResetPwd' }) |
||||
|
|
||||
|
const { t } = useI18n() |
||||
|
const message = useMessage() |
||||
|
const formRef = ref<FormInstance>() |
||||
|
const password = reactive({ |
||||
|
oldPassword: '', |
||||
|
newPassword: '', |
||||
|
confirmPassword: '' |
||||
|
}) |
||||
|
|
||||
|
// 表单校验 |
||||
|
const equalToPassword = (_rule, value, callback) => { |
||||
|
if (password.newPassword !== value) { |
||||
|
callback(new Error(t('profile.password.diffPwd'))) |
||||
|
} else { |
||||
|
callback() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const rules = reactive<FormRules>({ |
||||
|
oldPassword: [ |
||||
|
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' }, |
||||
|
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' } |
||||
|
], |
||||
|
newPassword: [ |
||||
|
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' }, |
||||
|
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' } |
||||
|
], |
||||
|
confirmPassword: [ |
||||
|
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' }, |
||||
|
{ required: true, validator: equalToPassword, trigger: 'blur' } |
||||
|
] |
||||
|
}) |
||||
|
|
||||
|
const submit = (formEl: FormInstance | undefined) => { |
||||
|
if (!formEl) return |
||||
|
formEl.validate(async (valid) => { |
||||
|
if (valid) { |
||||
|
await updateUserPassword(password.oldPassword, password.newPassword) |
||||
|
message.success(t('common.updateSuccess')) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const reset = (formEl: FormInstance | undefined) => { |
||||
|
if (!formEl) return |
||||
|
formEl.resetFields() |
||||
|
} |
||||
|
</script> |
@ -0,0 +1,39 @@ |
|||||
|
<template> |
||||
|
<div class="change-avatar"> |
||||
|
<CropperAvatar |
||||
|
ref="cropperRef" |
||||
|
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }" |
||||
|
:showBtn="false" |
||||
|
:value="img" |
||||
|
width="120px" |
||||
|
@change="handelUpload" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { propTypes } from '@/utils/propTypes' |
||||
|
import { uploadAvatar } from '@/api/system/user/profile' |
||||
|
import { CropperAvatar } from '@/components/Cropper' |
||||
|
|
||||
|
defineOptions({ name: 'UserAvatar' }) |
||||
|
|
||||
|
defineProps({ |
||||
|
img: propTypes.string.def('') |
||||
|
}) |
||||
|
|
||||
|
const cropperRef = ref() |
||||
|
const handelUpload = async ({ data }) => { |
||||
|
await uploadAvatar({ avatarFile: data }) |
||||
|
cropperRef.value.close() |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.change-avatar { |
||||
|
img { |
||||
|
display: block; |
||||
|
margin-bottom: 15px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,94 @@ |
|||||
|
<template> |
||||
|
<el-table :data="socialUsers" :show-header="false"> |
||||
|
<el-table-column fixed="left" title="序号" type="seq" width="60" /> |
||||
|
<el-table-column align="left" label="社交平台" width="120"> |
||||
|
<template #default="{ row }"> |
||||
|
<img :src="row.img" alt="" class="h-5 align-middle" /> |
||||
|
<p class="mr-5">{{ row.title }}</p> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column align="center" label="操作"> |
||||
|
<template #default="{ row }"> |
||||
|
<template v-if="row.openid"> |
||||
|
已绑定 |
||||
|
<XTextButton class="mr-5" title="(解绑)" type="primary" @click="unbind(row)" /> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
未绑定 |
||||
|
<XTextButton class="mr-5" title="(绑定)" type="primary" @click="bind(row)" /> |
||||
|
</template> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
</template> |
||||
|
<script lang="ts" setup> |
||||
|
import { SystemUserSocialTypeEnum } from '@/utils/constants' |
||||
|
import { getUserProfile, ProfileVO } from '@/api/system/user/profile' |
||||
|
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser' |
||||
|
|
||||
|
defineOptions({ name: 'UserSocial' }) |
||||
|
|
||||
|
const message = useMessage() |
||||
|
const socialUsers = ref<any[]>([]) |
||||
|
const userInfo = ref<ProfileVO>() |
||||
|
|
||||
|
const initSocial = async () => { |
||||
|
const res = await getUserProfile() |
||||
|
userInfo.value = res |
||||
|
for (const i in SystemUserSocialTypeEnum) { |
||||
|
const socialUser = { ...SystemUserSocialTypeEnum[i] } |
||||
|
socialUsers.value.push(socialUser) |
||||
|
if (userInfo.value?.socialUsers) { |
||||
|
for (const j in userInfo.value.socialUsers) { |
||||
|
if (socialUser.type === userInfo.value.socialUsers[j].type) { |
||||
|
socialUser.openid = userInfo.value.socialUsers[j].openid |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
const route = useRoute() |
||||
|
const bindSocial = () => { |
||||
|
// 社交绑定 |
||||
|
const type = route.query.type |
||||
|
const code = route.query.code |
||||
|
const state = route.query.state |
||||
|
if (!code) { |
||||
|
return |
||||
|
} |
||||
|
socialBind(type, code, state).then(() => { |
||||
|
message.success('绑定成功') |
||||
|
initSocial() |
||||
|
}) |
||||
|
} |
||||
|
const bind = (row) => { |
||||
|
const redirectUri = location.origin + '/user/profile?type=' + row.type |
||||
|
// 进行跳转 |
||||
|
socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => { |
||||
|
window.location.href = res |
||||
|
}) |
||||
|
} |
||||
|
const unbind = async (row) => { |
||||
|
const res = await socialUnbind(row.type, row.openid) |
||||
|
if (res) { |
||||
|
row.openid = undefined |
||||
|
} |
||||
|
message.success('解绑成功') |
||||
|
} |
||||
|
|
||||
|
onMounted(async () => { |
||||
|
await initSocial() |
||||
|
}) |
||||
|
|
||||
|
watch( |
||||
|
() => route, |
||||
|
(newRoute) => { |
||||
|
bindSocial() |
||||
|
console.log(newRoute) |
||||
|
}, |
||||
|
{ |
||||
|
immediate: true |
||||
|
} |
||||
|
) |
||||
|
</script> |
@ -0,0 +1,7 @@ |
|||||
|
import BasicInfo from './BasicInfo.vue' |
||||
|
import ProfileUser from './ProfileUser.vue' |
||||
|
import ResetPwd from './ResetPwd.vue' |
||||
|
import UserAvatarVue from './UserAvatar.vue' |
||||
|
import UserSocial from './UserSocial.vue' |
||||
|
|
||||
|
export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial } |
@ -0,0 +1,64 @@ |
|||||
|
<template> |
||||
|
<div class="flex"> |
||||
|
<el-card class="user w-1/3" shadow="hover"> |
||||
|
<template #header> |
||||
|
<div class="card-header"> |
||||
|
<span>{{ t('profile.user.title') }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<ProfileUser /> |
||||
|
</el-card> |
||||
|
<el-card class="user ml-3 w-2/3" shadow="hover"> |
||||
|
<template #header> |
||||
|
<div class="card-header"> |
||||
|
<span>{{ t('profile.info.title') }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<div> |
||||
|
<el-tabs v-model="activeName" tab-position="top" style="height: 400px" class="profile-tabs"> |
||||
|
<el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo"> |
||||
|
<BasicInfo /> |
||||
|
</el-tab-pane> |
||||
|
<el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd"> |
||||
|
<ResetPwd /> |
||||
|
</el-tab-pane> |
||||
|
<!-- <el-tab-pane :label="t('profile.info.userSocial')" name="userSocial"> |
||||
|
<UserSocial /> |
||||
|
</el-tab-pane> --> |
||||
|
</el-tabs> |
||||
|
</div> |
||||
|
</el-card> |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup lang="ts" name="Profile"> |
||||
|
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components/' |
||||
|
const { t } = useI18n() |
||||
|
|
||||
|
const activeName = ref('basicInfo') |
||||
|
</script> |
||||
|
<style scoped> |
||||
|
.user { |
||||
|
max-height: 960px; |
||||
|
padding: 15px 20px 20px; |
||||
|
} |
||||
|
|
||||
|
.card-header { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
:deep(.el-card .el-card__header, .el-card .el-card__body) { |
||||
|
padding: 15px !important; |
||||
|
} |
||||
|
|
||||
|
.profile-tabs > .el-tabs__content { |
||||
|
padding: 32px; |
||||
|
font-weight: 600; |
||||
|
color: #6b778c; |
||||
|
} |
||||
|
|
||||
|
.el-tabs--left .el-tabs__content { |
||||
|
height: 100%; |
||||
|
} |
||||
|
</style> |
Loading…
Reference in new issue