init
This commit is contained in:
65
src/views/Profile/Index.vue
Normal file
65
src/views/Profile/Index.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<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" class="profile-tabs" style="height: 400px" tab-position="top">
|
||||
<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 v-model:activeName="activeName" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components'
|
||||
|
||||
const { t } = useI18n()
|
||||
defineOptions({ name: 'Profile' })
|
||||
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>
|
||||
88
src/views/Profile/components/BasicInfo.vue
Normal file
88
src/views/Profile/components/BasicInfo.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="py-10px">
|
||||
<avue-form
|
||||
v-model="formData"
|
||||
:option="formOption"
|
||||
@submit="submit"
|
||||
@reset-change="init"
|
||||
v-loading="loading"
|
||||
>
|
||||
</avue-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { getUserProfile, updateUserProfile } from '@/api/system/user/profile'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'BasicInfo' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage() // 消息弹窗
|
||||
const userStore = useUserStore()
|
||||
|
||||
const formData = ref<any>({})
|
||||
const formOption = ref({
|
||||
labelWidth: 120,
|
||||
span: 24,
|
||||
submitText: t('common.save'),
|
||||
emptyText: t('common.reset'),
|
||||
column: {
|
||||
nickname: {
|
||||
label: t('profile.user.nickname'),
|
||||
rules: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }]
|
||||
},
|
||||
mobile: {
|
||||
label: t('profile.user.mobile'),
|
||||
rules: [
|
||||
{ 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'
|
||||
}
|
||||
]
|
||||
},
|
||||
email: {
|
||||
label: t('profile.user.email'),
|
||||
rules: [
|
||||
{ required: true, message: t('profile.rules.mail'), trigger: 'blur' },
|
||||
{
|
||||
type: 'email',
|
||||
message: t('profile.rules.truemail'),
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
},
|
||||
sex: {
|
||||
label: t('profile.user.sex'),
|
||||
type: 'radio',
|
||||
dicData: [
|
||||
{ label: t('profile.user.man'), value: 1 },
|
||||
{ label: t('profile.user.woman'), value: 2 }
|
||||
],
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
const submit = async (form, done) => {
|
||||
loading.value = true
|
||||
done()
|
||||
await updateUserProfile(form)
|
||||
message.success(t('common.updateSuccess'))
|
||||
const profile = await init()
|
||||
userStore.setUserNicknameAction(profile.nickname)
|
||||
userStore.setUserMobilAction(profile.mobile)
|
||||
}
|
||||
const init = async () => {
|
||||
loading.value = true
|
||||
const res = await getUserProfile()
|
||||
formData.value = res
|
||||
loading.value = false
|
||||
return res
|
||||
}
|
||||
onMounted(async () => {
|
||||
await init()
|
||||
})
|
||||
</script>
|
||||
128
src/views/Profile/components/ProfileUser.vue
Normal file
128
src/views/Profile/components/ProfileUser.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<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 flex justify-between">
|
||||
<div class="flex-basis-100px flex-shrink-0">
|
||||
<Icon class="mr-5px" icon="carbon:tree-view-alt" />
|
||||
{{ t('profile.user.dept') }}
|
||||
</div>
|
||||
<div class="flex-1 flex flex-wrap justify-end gap-5px">
|
||||
<div v-for="dept in userInfo.deptInfoList" :key="dept.deptId">
|
||||
<el-popover placement="right" width="240" trigger="hover">
|
||||
<template #reference>
|
||||
<el-tag type="primary" class="cursor-pointer">{{ dept.deptName }}</el-tag>
|
||||
</template>
|
||||
<div class="flex flex-col gap-y-4px">
|
||||
<template v-for="type in keyList" :key="type.value">
|
||||
<div class="flex" v-if="dept[type.key]">
|
||||
<span class="flex-basis-42px flex-shrink-0 c-#909399">{{ type.label }}:</span>
|
||||
<span class="flex-1">{{ dept[type.key] }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" v-if="userInfo.rankInfoList?.length">
|
||||
<Icon class="mr-5px" icon="ep:suitcase" />
|
||||
{{ t('profile.user.rank') }}
|
||||
<div v-if="userInfo?.rankInfoList" class="pull-right">
|
||||
{{ userInfo?.rankInfoList.map((rank) => rank.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({} as ProfileVO)
|
||||
|
||||
const keyList = [
|
||||
{ label: '部门', value: 'deptName', key: 'deptName' },
|
||||
{ label: '角色', value: 'roleInfoList', key: 'role' },
|
||||
{ label: '职务', value: 'dutyInfoList', key: 'duty' },
|
||||
{ label: '职位', value: 'positionInfoList', key: 'position' },
|
||||
{ label: '岗位', value: 'postInfoList', key: 'post' }
|
||||
]
|
||||
|
||||
const getUserInfo = async () => {
|
||||
const users = await getUserProfile()
|
||||
users.deptInfoList = users.deptInfoList.map((dept) => {
|
||||
keyList.forEach(({ value, key }) => {
|
||||
if (value === 'deptName') return
|
||||
if (dept[value]?.length) dept[key] = dept[value].map((info) => info.name).join('、')
|
||||
else dept[key] = ''
|
||||
})
|
||||
return dept
|
||||
})
|
||||
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>
|
||||
72
src/views/Profile/components/ResetPwd.vue
Normal file
72
src/views/Profile/components/ResetPwd.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="py-10px">
|
||||
<avue-form v-model="formData" :option="formOption" @submit="submit" v-loading="loading">
|
||||
<template #oldPassword>
|
||||
<InputPassword v-model="formData.oldPassword" />
|
||||
</template>
|
||||
<template #newPassword>
|
||||
<InputPassword v-model="formData.newPassword" strength />
|
||||
</template>
|
||||
<template #confirmPassword>
|
||||
<InputPassword v-model="formData.confirmPassword" strength />
|
||||
</template>
|
||||
</avue-form>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { updateUserPassword } from '@/api/system/user/profile'
|
||||
|
||||
defineOptions({ name: 'ResetPwd' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
const equalToPassword = (_rule, value, callback) => {
|
||||
if (formData.value.newPassword !== value) {
|
||||
callback(new Error(t('profile.password.diffPwd')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const loading = ref(false)
|
||||
const formData = ref<any>({})
|
||||
const formOption = ref({
|
||||
labelWidth: 120,
|
||||
span: 24,
|
||||
submitText: t('common.save'),
|
||||
emptyText: t('common.reset'),
|
||||
column: {
|
||||
oldPassword: {
|
||||
label: t('profile.password.oldPassword'),
|
||||
rules: [
|
||||
{ required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
newPassword: {
|
||||
label: t('profile.password.newPassword'),
|
||||
rules: [
|
||||
{ required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: t('profile.password.pwdRules'), trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
confirmPassword: {
|
||||
label: t('profile.password.confirmPassword'),
|
||||
rules: [
|
||||
{ required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
|
||||
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const submit = async (form, done) => {
|
||||
done()
|
||||
loading.value = true
|
||||
await updateUserPassword(form.oldPassword, form.newPassword).finally(
|
||||
() => (loading.value = false)
|
||||
)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
</script>
|
||||
45
src/views/Profile/components/UserAvatar.vue
Normal file
45
src/views/Profile/components/UserAvatar.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<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'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
|
||||
defineOptions({ name: 'UserAvatar' })
|
||||
|
||||
defineProps({
|
||||
img: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
|
||||
const cropperRef = ref()
|
||||
const handelUpload = async ({ file }) => {
|
||||
const res = await uploadAvatar({ updateSupport: 0, file })
|
||||
cropperRef.value.close()
|
||||
userStore.setUserAvatarAction(res.data.fileUrl)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.change-avatar {
|
||||
img {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
src/views/Profile/components/UserSocial.vue
Normal file
107
src/views/Profile/components/UserSocial.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<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' })
|
||||
defineProps<{
|
||||
activeName: string
|
||||
}>()
|
||||
const message = useMessage()
|
||||
const socialUsers = ref<any[]>([])
|
||||
const userInfo = ref<ProfileVO>()
|
||||
|
||||
const initSocial = async () => {
|
||||
socialUsers.value = [] // 重置避免无限增长
|
||||
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 emit = defineEmits<{
|
||||
(e: 'update:activeName', v: string): void
|
||||
}>()
|
||||
const bindSocial = () => {
|
||||
// 社交绑定
|
||||
const type = getUrlValue('type')
|
||||
const code = route.query.code
|
||||
const state = route.query.state
|
||||
if (!code) {
|
||||
return
|
||||
}
|
||||
socialBind(type, code, state).then(() => {
|
||||
message.success('绑定成功')
|
||||
emit('update:activeName', 'userSocial')
|
||||
})
|
||||
}
|
||||
|
||||
// 双层 encode 需要在回调后进行 decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href))
|
||||
return url.searchParams.get(key) ?? ''
|
||||
}
|
||||
|
||||
const bind = (row) => {
|
||||
// 双层 encode 解决钉钉回调 type 参数丢失的问题
|
||||
const redirectUri = location.origin + '/user/profile?' + encodeURIComponent(`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,
|
||||
() => {
|
||||
bindSocial()
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
7
src/views/Profile/components/index.ts
Normal file
7
src/views/Profile/components/index.ts
Normal file
@@ -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 }
|
||||
Reference in New Issue
Block a user