Files
tms-erp-web/src/views/system/userinfo.vue
T
gxwebsoft f7eea88945 feat(login): 更换登录验证码为图形字符验证码
- 配置文件 website.js 中将 captchaType 从 'behavior' 改为 'image'
- 修改 userlogin.vue,图形验证码图片由输入框内联改为输入框旁独立显示
- 重写 login.scss 样式,实现验证码图片区域带边框和高亮效果
- 确保验证码图片点击可刷新,且支持从后端接口获取数据
- 登录页仍默认显示 IAM 登录,需切换「账号密码登录」显示验证码区域

feat(contract-manage): 优化所属组织搜索栏为树形下拉选择框

- 引入并应用 organization-search mixin 统一管理所属组织搜索
- contract-manage.js、driver.js、transport-ship.js、transport-vehicle.js 中所属组织搜索改为 searchslot 模式
- contract-manage.vue 中新增 renderOrganizationSearch 方法,渲染基于 ElTreeSelect 的树形下拉组件
- 组织树数据加载时自动排除「外部组织」节点,保持搜索选项清晰
- 所有相关所属组织搜索统一样式与交互,提升用户体验

feat(system): 用户和部门视图样式及操作改进

- user.vue 和 userinfo.vue 中 el-tabs 组件添加 border-card 类型,提升标签页视觉效果
- dept.vue 中添加查看、编辑、删除操作入口,绑定对应方法,通过权限控制显示权限按钮
- dept.vue 菜单宽度、对话框宽度及行为配置优化

style(login): 调整登录样式及验证码布局细节

- 调整 .login-code-wrap 及内部元素的 flex 布局,输入框与验证码图片并排显示
- 设置验证码图片容器尺寸、边框及 hover 高亮样式
- 确保验证码图片撑满容器并禁止用户选中

refactor(mixins): 新增 organization-search 统一搜索 mixin

- 实现组织树数据加载、排除外部节点、归一化及扁平化处理
- 提供 renderOrganizationSearch 方法用于渲染统一树形选择搜索框
- 减少重复代码,方便各页面快速接入和统一维护

chore(vehicle): 车辆页面引入 organization-search mixin

- 在车辆视图 vehicle.vue 中混入 organization-search,增强所属组织搜索功能一致性
2026-08-28 15:41:01 +08:00

487 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<basic-container>
<el-tabs type="border-card" v-model="activeTab" @tab-change="handleTabChange">
<!-- 个人信息 Tab -->
<el-tab-pane label="个人信息" name="info">
<el-form ref="infoForm" :model="infoForm" label-width="auto">
<el-form-item label="头像">
<div class="avatar-upload-container">
<div class="avatar-wrapper" v-if="infoForm.avatar">
<img :src="infoForm.avatar" alt="用户头像" class="avatar" />
<div class="avatar-overlay">
<el-icon class="avatar-action" @click="previewAvatar"><View /></el-icon>
<el-icon class="avatar-action" @click="deleteAvatar"><Delete /></el-icon>
</div>
</div>
<el-upload
v-else
class="avatar-uploader"
action="/api/blade-resource/oss/endpoint/put-file"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
accept=".jpg,.jpeg,.png,.gif"
>
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="el-upload__tip">支持 JPG/PNG/GIF 格式文件大小不超过 1MB</div>
</div>
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="infoForm.realName" placeholder="请输入姓名">
<template #prefix>
<el-icon><Bell /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="用户名">
<el-input v-model="infoForm.name" placeholder="请输入用户名">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="infoForm.phone" placeholder="请输入手机号">
<template #prefix>
<el-icon><Iphone /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="infoForm.email" placeholder="请输入邮箱">
<template #prefix>
<el-icon><Message /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="button-group">
<el-button type="primary" @click="submitInfo" :loading="loading">
<el-icon><Check /></el-icon>
<span>提交</span>
</el-button>
<el-button @click="resetForm">
<el-icon><RefreshRight /></el-icon>
<span>清空</span>
</el-button>
</div>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 修改密码 Tab -->
<el-tab-pane label="修改密码" name="password">
<el-form ref="passwordForm" :model="passwordForm" label-width="auto">
<el-form-item label="原密码">
<el-input
v-model="passwordForm.oldPassword"
type="password"
placeholder="请输入原密码"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="新密码">
<el-input
v-model="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="确认密码">
<el-input
v-model="passwordForm.newPassword1"
type="password"
placeholder="请确认新密码"
show-password
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="button-group">
<el-button type="primary" @click="submitPassword" :loading="loading">
<el-icon><Check /></el-icon>
<span>提交</span>
</el-button>
<el-button @click="resetForm">
<el-icon><RefreshRight /></el-icon>
<span>清空</span>
</el-button>
</div>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</basic-container>
<!-- 头像预览组件 -->
<el-image-viewer
v-if="showAvatarPreview"
:url-list="avatarPreviewList"
:initial-index="0"
:hide-on-click-modal="true"
:teleported="true"
@close="showAvatarPreview = false"
/>
</div>
</template>
<script>
import { getUserInfo, updateInfo, updatePassword } from '@/api/system/user';
import md5 from 'js-md5';
import { sensitive } from '@/utils/sensitive';
import {
Plus,
Bell,
User,
Iphone,
Message,
Lock,
Check,
RefreshRight,
View,
Delete,
} from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus';
import { getUploadHeaders } from '@/utils/upload';
export default {
components: {
ElImageViewer,
Plus,
Bell,
User,
Iphone,
Message,
Lock,
Check,
RefreshRight,
View,
Delete,
},
data() {
return {
activeTab: 'info',
loading: false,
infoForm: {
id: '',
avatar: '',
name: '',
realName: '',
phone: '',
email: '',
},
passwordForm: {
oldPassword: '',
newPassword: '',
newPassword1: '',
},
sensitiveManager: null,
showAvatarPreview: false,
avatarPreviewList: [],
};
},
created() {
// 初始化脱敏工具
this.sensitiveManager = sensitive.create({
fields: ['phone', 'email'],
});
// 初始化时根据默认tab加载数据
if (this.activeTab === 'info') {
this.loadUserInfo();
}
},
computed: {
uploadHeaders() {
return getUploadHeaders();
},
},
methods: {
// Tab切换处理
handleTabChange(name) {
if (name === 'info') {
this.loadUserInfo();
}
},
// 加载用户信息
loadUserInfo() {
getUserInfo().then(res => {
const user = res.data.data;
this.infoForm = {
id: user.id,
avatar: user.avatar,
name: user.name,
realName: user.realName,
phone: user.phone,
email: user.email,
};
// 保存初始脱敏数据
this.sensitiveManager.saveInitialData(this.infoForm);
});
},
// 提交个人信息
submitInfo() {
this.loading = true;
const submitData = this.sensitiveManager.getSubmitData(this.infoForm);
updateInfo(submitData).then(
res => {
if (res.data.success) {
this.$message({
type: 'success',
message: '修改信息成功!',
});
} else {
this.$message({
type: 'error',
message: res.data.msg,
});
}
this.loading = false;
},
error => {
window.console.log(error);
this.loading = false;
}
);
},
// 提交密码修改
submitPassword() {
this.loading = true;
updatePassword(
md5(this.passwordForm.oldPassword),
md5(this.passwordForm.newPassword),
md5(this.passwordForm.newPassword1)
).then(
res => {
if (res.data.success) {
this.$message({
type: 'success',
message: '修改密码成功!',
});
// 清空密码表单
this.passwordForm = {
oldPassword: '',
newPassword: '',
newPassword1: '',
};
} else {
this.$message({
type: 'error',
message: res.data.msg,
});
}
this.loading = false;
},
error => {
window.console.log(error);
this.loading = false;
}
);
},
// 头像上传成功处理
handleAvatarSuccess(response) {
if (response && response.data) {
this.infoForm.avatar = response.data.link || response.data;
}
},
// 头像上传前验证
beforeAvatarUpload(file) {
const isValidType = ['image/jpeg', 'image/png', 'image/gif'].includes(file.type);
const isLt1M = file.size / 1024 < 1024;
if (!isValidType) {
this.$message.error('上传头像只支持 JPG/PNG/GIF 格式!');
}
if (!isLt1M) {
this.$message.error('上传头像大小不能超过 1MB!');
}
return isValidType && isLt1M;
},
// 重置表单
resetForm() {
if (this.activeTab === 'info') {
// 清空个人信息表单,但保留id
const id = this.infoForm.id;
this.infoForm = {
id: id,
avatar: '',
name: '',
realName: '',
phone: '',
email: '',
};
} else if (this.activeTab === 'password') {
// 清空密码表单
this.passwordForm = {
oldPassword: '',
newPassword: '',
newPassword1: '',
};
}
},
// 预览头像
previewAvatar() {
if (this.infoForm.avatar) {
this.avatarPreviewList = [this.infoForm.avatar];
this.showAvatarPreview = true;
} else {
this.$message.warning('暂无头像可预览');
}
},
// 删除头像
deleteAvatar() {
this.$confirm('确定要删除头像吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.infoForm.avatar = '';
this.$message.success('头像已删除');
})
.catch(() => {});
},
},
};
</script>
<style scoped>
.avatar-upload-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.avatar-wrapper {
position: relative;
width: 200px;
height: 200px;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.avatar-wrapper:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.avatar-wrapper .avatar {
width: 200px;
height: 200px;
display: block;
border-radius: 12px;
object-fit: cover;
}
.avatar-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.6) 100%);
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
opacity: 0;
transition: opacity 0.3s ease;
backdrop-filter: blur(2px);
}
.avatar-wrapper:hover .avatar-overlay {
opacity: 1;
}
.avatar-action {
font-size: 20px;
color: #fff;
cursor: pointer;
padding: 8px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-action:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.avatar-uploader :deep(.el-upload) {
border: 2px dashed #d9d9d9;
border-radius: 12px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
width: 200px;
height: 200px;
background-color: #fafafa;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-uploader :deep(.el-upload:hover) {
border-color: var(--el-color-primary);
background-color: #f5f5f5;
}
.avatar-uploader-icon {
font-size: 40px;
color: #8c939d;
transition: color 0.3s ease;
}
.avatar-uploader :deep(.el-upload:hover) .avatar-uploader-icon {
color: var(--el-color-primary);
}
.el-upload__tip {
font-size: 12px;
color: #969799;
margin-top: 10px;
line-height: 1.5;
padding-left: 4px;
}
.button-group {
display: flex;
justify-content: center;
gap: 16px;
width: 100%;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>