陈薪名
9 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