【新增】前端代码第一次提交

This commit is contained in:
YOHO\20373
2025-04-17 16:42:02 +08:00
parent e0f13c705b
commit 865ebd0a4d
1635 changed files with 237638 additions and 23 deletions

View 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>

View File

@@ -0,0 +1,96 @@
<template>
<Form ref="formRef" :labelWidth="200" :rules="rules" :schema="schema">
<template #sex="form">
<el-radio-group v-model="form['sex']">
<el-radio :value="1">{{ t('profile.user.man') }}</el-radio>
<el-radio :value="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'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'BasicInfo' })
const { t } = useI18n()
const message = useMessage() // 消息弹窗
const userStore = useUserStore()
// 表单校验
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'))
const profile = await init()
userStore.setUserNicknameAction(profile.nickname)
}
})
}
const init = async () => {
const res = await getUserProfile()
unref(formRef)?.setValues(res)
return res
}
onMounted(async () => {
await init()
})
</script>

View File

@@ -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({} as 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>

View File

@@ -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>

View 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 ({ data }) => {
const res = await uploadAvatar({ avatarFile: data })
cropperRef.value.close()
userStore.setUserAvatarAction(res.data)
}
</script>
<style lang="scss" scoped>
.change-avatar {
img {
display: block;
margin-bottom: 15px;
border-radius: 50%;
}
}
</style>

View 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>

View 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 }