first commit

This commit is contained in:
2026-03-05 16:52:12 +08:00
commit 8ca2e6d52f
1899 changed files with 321565 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
<template>
<div class="mobile-profile">
<!-- 头像区域 -->
<div class="mobile-profile__avatar-section">
<el-avatar :size="72" :src="userInfo.avatar || defaultAvatar" class="mobile-profile__avatar" />
<div class="mobile-profile__name">{{ userInfo.nickname || '用户' }}</div>
<div class="mobile-profile__username">{{ userInfo.username || '' }}</div>
</div>
<!-- 基本信息 -->
<div class="mobile-profile__section">
<div class="mobile-profile__section-title">基本信息</div>
<div class="mobile-profile__card">
<div class="mobile-profile__item">
<span class="mobile-profile__label">昵称</span>
<span class="mobile-profile__value">{{ userInfo.nickname || '-' }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">手机号</span>
<span class="mobile-profile__value">{{ userInfo.mobile || '-' }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">邮箱</span>
<span class="mobile-profile__value">{{ userInfo.email || '-' }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">性别</span>
<span class="mobile-profile__value">{{ getSexText(userInfo.sex) }}</span>
</div>
</div>
</div>
<!-- 组织信息 -->
<div class="mobile-profile__section">
<div class="mobile-profile__section-title">组织信息</div>
<div class="mobile-profile__card">
<div class="mobile-profile__item">
<span class="mobile-profile__label">部门</span>
<span class="mobile-profile__value">{{ userInfo.dept?.name || '-' }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">岗位</span>
<span class="mobile-profile__value">{{ userInfo.posts?.map(p => p.name).join('、') || '-' }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">角色</span>
<span class="mobile-profile__value">{{ userInfo.roles?.map(r => r.name).join('、') || '-' }}</span>
</div>
</div>
</div>
<!-- 账号信息 -->
<div class="mobile-profile__section">
<div class="mobile-profile__section-title">账号信息</div>
<div class="mobile-profile__card">
<div class="mobile-profile__item">
<span class="mobile-profile__label">用户名</span>
<span class="mobile-profile__value">{{ userInfo.username || '-' }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">最近登录IP</span>
<span class="mobile-profile__value">{{ userInfo.loginIp || '-' }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">最近登录时间</span>
<span class="mobile-profile__value">{{ formatDate2(userInfo.loginDate) }}</span>
</div>
<div class="mobile-profile__item">
<span class="mobile-profile__label">注册时间</span>
<span class="mobile-profile__value">{{ formatDate2(userInfo.createTime) }}</span>
</div>
</div>
</div>
<!-- 编辑按钮 -->
<div class="mobile-profile__actions">
<el-button type="primary" round class="mobile-profile__edit-btn" @click="editDialogVisible = true">
编辑个人信息
</el-button>
</div>
<!-- 编辑对话框 -->
<el-dialog v-model="editDialogVisible" title="编辑个人信息" width="90%" center>
<el-form ref="editFormRef" :model="editForm" :rules="editRules" label-position="top">
<el-form-item label="昵称" prop="nickname">
<el-input v-model="editForm.nickname" placeholder="请输入昵称" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="editForm.mobile" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="editForm.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-radio-group v-model="editForm.sex">
<el-radio :value="1"></el-radio>
<el-radio :value="2"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitEdit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { ArrowLeft } from '@element-plus/icons-vue'
import { formatDate } from '@/utils/formatTime'
import {
getUserProfile,
updateUserProfile,
ProfileVO,
UserProfileUpdateReqVO
} from '@/api/system/user/profile'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'ProfileMobile' })
const router = useRouter()
const message = useMessage()
const userStore = useUserStore()
const defaultAvatar = 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
const userInfo = ref<Partial<ProfileVO>>({})
const editDialogVisible = ref(false)
const editFormRef = ref()
const editForm = reactive<UserProfileUpdateReqVO>({
nickname: '',
mobile: '',
email: '',
sex: 1
})
const editRules = {
nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
],
mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
]
}
const formatDate2 = (date: any) => {
if (!date) return '-'
return formatDate(new Date(date), 'YYYY-MM-DD HH:mm')
}
const getSexText = (sex?: number) => {
if (sex === 1) return '男'
if (sex === 2) return '女'
return '未设置'
}
const goBack = () => {
router.back()
}
/** 获取用户信息 */
const loadUserInfo = async () => {
try {
userInfo.value = await getUserProfile()
} catch (e) {
console.error('获取用户信息失败', e)
}
}
/** 打开编辑时填充表单 */
watch(editDialogVisible, (val) => {
if (val && userInfo.value) {
editForm.nickname = userInfo.value.nickname || ''
editForm.mobile = userInfo.value.mobile || ''
editForm.email = userInfo.value.email || ''
editForm.sex = userInfo.value.sex || 1
}
})
/** 提交编辑 */
const submitEdit = async () => {
const valid = await editFormRef.value?.validate()
if (!valid) return
try {
await updateUserProfile(editForm)
message.success('修改成功')
editDialogVisible.value = false
await loadUserInfo()
userStore.setUserNicknameAction(editForm.nickname || '')
} catch (e) {
console.error('修改个人信息失败', e)
}
}
onMounted(() => {
loadUserInfo()
})
</script>
<style lang="scss" scoped>
.mobile-profile {
min-height: 100vh;
background: #f5f7fa;
padding-bottom: 30px;
}
.mobile-profile__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
z-index: 10;
}
.mobile-profile__back {
font-size: 20px;
color: #333;
cursor: pointer;
width: 32px;
}
.mobile-profile__title {
font-size: 17px;
font-weight: 600;
color: #303133;
}
.mobile-profile__placeholder {
width: 32px;
}
.mobile-profile__avatar-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 20px 20px;
background: linear-gradient(180deg, #fff 0%, #f5f7fa 100%);
}
.mobile-profile__avatar {
border: 3px solid #409eff;
}
.mobile-profile__name {
margin-top: 10px;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.mobile-profile__username {
margin-top: 4px;
font-size: 13px;
color: #909399;
}
.mobile-profile__section {
margin: 12px 16px 0;
}
.mobile-profile__section-title {
font-size: 13px;
font-weight: 500;
color: #909399;
margin-bottom: 8px;
padding-left: 4px;
}
.mobile-profile__card {
background: #fff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.mobile-profile__item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
&:not(:last-child) {
border-bottom: 1px solid #f0f0f0;
}
}
.mobile-profile__label {
font-size: 14px;
color: #606266;
flex-shrink: 0;
}
.mobile-profile__value {
font-size: 14px;
color: #303133;
text-align: right;
word-break: break-all;
margin-left: 16px;
}
.mobile-profile__actions {
margin: 24px 16px 0;
}
.mobile-profile__edit-btn {
width: 100%;
height: 44px;
font-size: 16px;
}
</style>

View File

@@ -0,0 +1,442 @@
<template>
<div class="user-center">
<!-- 顶部导航 -->
<div class="header">
<span class="header-title">用户中心</span>
<el-icon class="header-icon" @click="handleMessage">
<ChatDotSquare />
</el-icon>
</div>
<!-- 用户信息卡片 -->
<div class="user-card">
<div class="avatar-wrapper">
<el-avatar :size="80" :src="userInfo.avatar || defaultAvatar" class="user-avatar" />
</div>
<div class="user-name">{{ userInfo.nickname || '用户' }}</div>
<el-tag type="primary" round class="company-tag">
{{ userInfo.dept?.name || '未设置部门' }}
</el-tag>
</div>
<!-- 菜单列表 -->
<div class="menu-list">
<div class="menu-item" @click="handleProfile">
<div class="menu-item-left">
<el-icon class="menu-icon"><UserFilled /></el-icon>
<span>个人中心</span>
</div>
<el-icon class="menu-arrow"><ArrowRight /></el-icon>
</div>
<div class="menu-item" @click="handleVersionInfo">
<div class="menu-item-left">
<el-icon class="menu-icon"><InfoFilled /></el-icon>
<span>版本信息</span>
</div>
<el-icon class="menu-arrow"><ArrowRight /></el-icon>
</div>
<div class="menu-item" @click="handleChangePassword">
<div class="menu-item-left">
<el-icon class="menu-icon"><Lock /></el-icon>
<span>修改密码</span>
</div>
<el-icon class="menu-arrow"><ArrowRight /></el-icon>
</div>
<!-- <div class="menu-item" @click="handleWechatAuth">-->
<!-- <div class="menu-item-left">-->
<!-- <el-icon class="menu-icon"><CircleCheck /></el-icon>-->
<!-- <span>公众号授权</span>-->
<!-- </div>-->
<!-- <el-icon class="menu-arrow"><ArrowRight /></el-icon>-->
<!-- </div>-->
<!-- <div class="menu-item" @click="handleCheckIn">-->
<!-- <div class="menu-item-left">-->
<!-- <el-icon class="menu-icon"><Calendar /></el-icon>-->
<!-- <span>签到打卡</span>-->
<!-- </div>-->
<!-- <el-icon class="menu-arrow"><ArrowRight /></el-icon>-->
<!-- </div>-->
<div class="menu-item" @click="handleLogout">
<div class="menu-item-left">
<el-icon class="menu-icon"><SwitchButton /></el-icon>
<span>退出登录</span>
</div>
<el-icon class="menu-arrow"><ArrowRight /></el-icon>
</div>
<div class="menu-item" @click="handleLanguage">
<div class="menu-item-left">
<el-icon class="menu-icon"><Setting /></el-icon>
<span>语言</span>
</div>
<el-icon class="menu-arrow"><ArrowRight /></el-icon>
</div>
</div>
<!-- 修改密码对话框 -->
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="400px" center>
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="passwordForm.oldPassword" type="password" show-password placeholder="请输入旧密码" />
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input v-model="passwordForm.newPassword" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="passwordForm.confirmPassword" type="password" show-password placeholder="请确认新密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="passwordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPassword">确定</el-button>
</template>
</el-dialog>
<!-- 版本信息对话框 -->
<el-dialog v-model="versionDialogVisible" title="版本信息" width="350px" center>
<div class="version-info">
<p>应用名称MOM管理系统</p>
<p>当前版本v1.0.0</p>
</div>
<template #footer>
<el-button type="primary" @click="versionDialogVisible = false">确定</el-button>
</template>
</el-dialog>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-item" @click="handleNavClick('home')">
<el-icon :size="22"><HomeFilled /></el-icon>
<span>首页</span>
</div>
<div class="nav-item" @click="handleNavClick('workspace')">
<el-icon :size="22"><TrendCharts /></el-icon>
<span>工作台</span>
</div>
<div class="nav-item" @click="handleNavClick('function')">
<el-icon :size="22"><Grid /></el-icon>
<span>功能</span>
</div>
<div class="nav-item active" @click="handleNavClick('mine')">
<el-icon :size="22"><User /></el-icon>
<span>我的</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getUserProfile, updateUserPassword } from '@/api/system/user/profile'
import { useUserStore } from '@/store/modules/user'
import {
ChatDotSquare,
InfoFilled,
Lock,
CircleCheck,
Calendar,
SwitchButton,
Setting,
ArrowRight,
HomeFilled,
TrendCharts,
Grid,
User,
UserFilled
} from '@element-plus/icons-vue'
defineOptions({ name: 'UserCenter' })
const message = useMessage()
const router = useRouter()
const userStore = useUserStore()
const defaultAvatar = 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif'
const userInfo = ref<any>({})
const passwordDialogVisible = ref(false)
const versionDialogVisible = ref(false)
const passwordFormRef = ref()
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
if (value !== passwordForm.newPassword) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const passwordRules = {
oldPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在6到20个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认新密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
/** 获取用户信息 */
const getUserInfo = async () => {
try {
const data = await getUserProfile()
userInfo.value = data
} catch (e) {
console.error(e)
}
}
/** 个人中心 */
const handleProfile = () => {
router.push('/user/profile-mobile')
}
/** 版本信息 */
const handleVersionInfo = () => {
versionDialogVisible.value = true
}
/** 修改密码 */
const handleChangePassword = () => {
passwordForm.oldPassword = ''
passwordForm.newPassword = ''
passwordForm.confirmPassword = ''
passwordDialogVisible.value = true
}
/** 提交密码修改 */
const submitPassword = async () => {
const valid = await passwordFormRef.value?.validate()
if (!valid) return
try {
await updateUserPassword(passwordForm.oldPassword, passwordForm.newPassword)
message.success('密码修改成功')
passwordDialogVisible.value = false
} catch (e) {
console.error(e)
}
}
/** 公众号授权 */
const handleWechatAuth = () => {
message.info('公众号授权功能开发中')
}
/** 签到打卡 */
const handleCheckIn = () => {
message.info('签到打卡功能开发中')
}
/** 退出登录 */
const handleLogout = async () => {
try {
await message.confirm('确定要退出登录吗?')
await userStore.loginOut()
router.push('/login')
} catch {
// 取消退出
}
}
/** 语言 */
const handleLanguage = () => {
message.info('语言切换功能开发中')
}
/** 消息 */
const handleMessage = () => {
router.push('/user/notify-message')
}
/** 底部导航 */
const handleNavClick = (tab: string) => {
switch (tab) {
case 'home':
router.push('/')
break
case 'workspace':
message.info('工作台功能开发中')
break
case 'function':
message.info('功能页面开发中')
break
case 'mine':
break
}
}
onMounted(() => {
getUserInfo()
})
</script>
<style lang="scss" scoped>
.user-center {
min-height: 100vh;
background: #f5f7fa;
padding-bottom: 70px;
max-width: 450px;
margin: 0 auto;
position: relative;
}
/* 顶部导航 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fff;
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-icon {
font-size: 22px;
color: #333;
cursor: pointer;
}
}
/* 用户信息卡片 */
.user-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px 24px;
background: linear-gradient(180deg, #fff 0%, #f5f7fa 100%);
.avatar-wrapper {
.user-avatar {
border: 3px solid #409eff;
}
}
.user-name {
margin-top: 12px;
font-size: 18px;
font-weight: 600;
color: #333;
}
.company-tag {
margin-top: 8px;
font-size: 13px;
padding: 4px 16px;
border-radius: 20px;
}
}
/* 菜单列表 */
.menu-list {
margin: 12px 16px;
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
cursor: pointer;
transition: background-color 0.2s;
&:not(:last-child) {
border-bottom: 1px solid #f0f0f0;
}
&:hover {
background-color: #f9fafb;
}
&:active {
background-color: #f0f2f5;
}
.menu-item-left {
display: flex;
align-items: center;
gap: 12px;
.menu-icon {
font-size: 20px;
color: #666;
}
span {
font-size: 15px;
color: #333;
}
}
.menu-arrow {
font-size: 14px;
color: #ccc;
}
}
}
/* 底部导航栏 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 450px;
display: flex;
justify-content: space-around;
align-items: center;
background: #fff;
padding: 8px 0 12px;
border-top: 1px solid #eee;
z-index: 100;
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
cursor: pointer;
color: #999;
transition: color 0.2s;
span {
font-size: 11px;
}
&.active {
color: #f56c6c;
}
&:hover:not(.active) {
color: #666;
}
}
}
/* 版本信息 */
.version-info {
text-align: center;
line-height: 2;
color: #666;
font-size: 14px;
}
</style>