学员管理增加岗位级别
This commit is contained in:
1 parent
86c04320fd
commit
2c14bf2b41
5 files changed
+319
-3
No files matched your search
-1
@@ -5,7 +5,6 @@ import java.util.regex.Pattern;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.train.domain.TrainTeacher;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.train.domain.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 学员手工修改请求。
|
||||
*
|
||||
* <p>复用 {@link StudentCreateDTO} 的可编辑字段与校验规则,额外要求携带学员主键 sId。
|
||||
* 身份证号沿用「只写不读」策略:未提交时保留库内既有密文,不做清空。</p>
|
||||
*/
|
||||
public class StudentUpdateDTO extends StudentCreateDTO
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "学员ID不能为空")
|
||||
@Size(max = 64, message = "学员ID长度不能超过64个字符")
|
||||
private String sId;
|
||||
|
||||
public String getsId()
|
||||
{
|
||||
return sId;
|
||||
}
|
||||
|
||||
public void setsId(String sId)
|
||||
{
|
||||
this.sId = sId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "StudentUpdateDTO{sId='" + sId + "', sName='" + getsName()
|
||||
+ "', idCardProvided=" + (getIdCard() != null) + "}";
|
||||
}
|
||||
}
|
||||
+1
@@ -101,6 +101,7 @@ public class TrainStudentServiceImpl implements ITrainStudentService
|
||||
student.setsPhone(trimToNull(dto.getsPhone()));
|
||||
student.setsEmail(trimToNull(dto.getsEmail()));
|
||||
student.setAvatar(trimToNull(dto.getAvatar()));
|
||||
student.setDutyLevel(dto.getDutyLevel());
|
||||
|
||||
if (normalizedIdCard != null)
|
||||
{
|
||||
|
||||
@@ -175,10 +175,18 @@
|
||||
<el-input v-model="createForm.sName" placeholder="请输入学员姓名" maxlength="50" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="sGender">
|
||||
<el-radio-group v-model="createForm.sGender">
|
||||
<!-- <el-radio-group v-model="createForm.sGender">
|
||||
<el-radio label="男">男</el-radio>
|
||||
<el-radio label="女">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-radio-group>-->
|
||||
<el-select v-model="createForm.sGender" placeholder="请选择性别">
|
||||
<el-option
|
||||
v-for="dict in dict.type.sys_user_sex"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="出生日期" prop="sBirth">
|
||||
<el-date-picker
|
||||
@@ -193,6 +201,16 @@
|
||||
<!-- <el-input v-model="createForm.orgName" placeholder="请输入所属单位/部门" maxlength="100" />-->
|
||||
<treeselect v-model="createForm.orgName" :options="deptOptions" :normalizer="normalizer" :show-count="true" placeholder="请选择归属部门" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位级别" prop="dutyLevel">
|
||||
<el-select v-model="createForm.dutyLevel" placeholder="请选择岗位级别" clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in dutyLevelOptions"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="sPhone">
|
||||
<el-input v-model="createForm.sPhone" placeholder="请输入联系电话" maxlength="20" />
|
||||
</el-form-item>
|
||||
@@ -240,6 +258,10 @@
|
||||
<el-tag v-else type="info" size="mini">未关联</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属单位"><span>{{ detail.orgName || '-' }}</span></el-form-item>
|
||||
<el-form-item label="岗位级别">
|
||||
<dict-tag v-if="detail.dutyLevel !== null && detail.dutyLevel !== undefined" :options="dict.type.train_duty_level" :value="detail.dutyLevel" />
|
||||
<span v-else>-</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话"><span>{{ detail.sPhone || '-' }}</span></el-form-item>
|
||||
<el-form-item label="邮箱"><span>{{ detail.sEmail || '-' }}</span></el-form-item>
|
||||
<el-form-item label="身份证号"><span>{{ detail.idCardMasked || '-' }}</span></el-form-item>
|
||||
@@ -322,6 +344,8 @@ import {addTeacher, updateTeacher} from "@/api/train/teacher";
|
||||
export default {
|
||||
name: 'Student',
|
||||
components: { studentAvatar, Treeselect },
|
||||
// 岗位级别字典(train_duty_level),注入后可通过 dict.type.train_duty_level 访问
|
||||
dicts: ['train_duty_level', 'sys_user_sex'],
|
||||
data() {
|
||||
const validateIdCard = (rule, value, callback) => {
|
||||
if (!value || value.trim() === '') {
|
||||
@@ -382,6 +406,20 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/*
|
||||
* 岗位级别下拉选项:取自字典 train_duty_level。
|
||||
* 后端 duty_level 为数值型,字典 dictValue 为字符串,
|
||||
* 此处将纯数字的字典值转为 Number,保证下拉回显时类型一致。
|
||||
*/
|
||||
dutyLevelOptions() {
|
||||
const list = (this.dict && this.dict.type && this.dict.type.train_duty_level) || []
|
||||
return list.map(item => ({
|
||||
label: item.label,
|
||||
value: /^\d+$/.test(String(item.value)) ? Number(item.value) : item.value
|
||||
}))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.resetCreate()
|
||||
this.mapDept()
|
||||
@@ -470,6 +508,7 @@ export default {
|
||||
sGender: row.sGender || '男',
|
||||
sBirth: row.sBirth || null,
|
||||
orgName: this.toDeptValue(row.orgName),
|
||||
dutyLevel: row.dutyLevel !== undefined && row.dutyLevel !== null ? this.toDutyLevelValue(row.dutyLevel) : null,
|
||||
sPhone: row.sPhone || null,
|
||||
sEmail: row.sEmail || null,
|
||||
idCard: null,
|
||||
@@ -480,6 +519,13 @@ export default {
|
||||
this.user = { avatar: this.createForm.avatar || '' }
|
||||
this.createOpen = true
|
||||
},
|
||||
/** 岗位级别值归一化:数字字符串转 Number,保证与下拉选项 value 类型一致 */
|
||||
toDutyLevelValue(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return null
|
||||
}
|
||||
return /^\d+$/.test(String(value)) ? Number(value) : value
|
||||
},
|
||||
/** 部门值归一化:数字字符串转 Number,保证与部门树选项 id 类型一致 */
|
||||
toDeptValue(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
@@ -494,6 +540,7 @@ export default {
|
||||
sGender: '男',
|
||||
sBirth: null,
|
||||
orgName: null,
|
||||
dutyLevel: null,
|
||||
sPhone: null,
|
||||
sEmail: null,
|
||||
idCard: null,
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="user-info-head" @click="editCropper()"><img :src="options.img" title="点击上传头像" class="img-circle img-lg" @error="handleAvatarError"></div>
|
||||
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
|
||||
<el-row>
|
||||
<el-col :xs="24" :md="12" :style="{height: '350px'}">
|
||||
<vue-cropper
|
||||
v-if="visible"
|
||||
ref="cropper"
|
||||
:img="options.img"
|
||||
:info="true"
|
||||
:auto-crop="options.autoCrop"
|
||||
:auto-crop-width="options.autoCropWidth"
|
||||
:auto-crop-height="options.autoCropHeight"
|
||||
:fixed-box="options.fixedBox"
|
||||
:output-type="options.outputType"
|
||||
@realTime="realTime"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12" :style="{height: '350px'}">
|
||||
<div class="avatar-upload-preview">
|
||||
<img :src="previews.url" :style="previews.img">
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<br>
|
||||
<el-row>
|
||||
<el-col :lg="2" :sm="3" :xs="3">
|
||||
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
|
||||
<el-button size="small">
|
||||
选择
|
||||
<i class="el-icon-upload el-icon--right" />
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
<el-col :lg="{span: 1, offset: 2}" :sm="2" :xs="2">
|
||||
<el-button icon="el-icon-plus" size="small" @click="changeScale(1)" />
|
||||
</el-col>
|
||||
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
|
||||
<el-button icon="el-icon-minus" size="small" @click="changeScale(-1)" />
|
||||
</el-col>
|
||||
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
|
||||
<el-button icon="el-icon-refresh-left" size="small" @click="rotateLeft()" />
|
||||
</el-col>
|
||||
<el-col :lg="{span: 1, offset: 1}" :sm="2" :xs="2">
|
||||
<el-button icon="el-icon-refresh-right" size="small" @click="rotateRight()" />
|
||||
</el-col>
|
||||
<el-col :lg="{span: 2, offset: 6}" :sm="2" :xs="2">
|
||||
<el-button type="primary" size="small" @click="uploadImg()">提 交</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import request from '@/utils/request'
|
||||
import { debounce } from '@/utils'
|
||||
|
||||
// 默认头像
|
||||
const DEFAULT_AVATAR = require('@/assets/images/profile.jpg')
|
||||
|
||||
export default {
|
||||
components: { VueCropper },
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 是否显示cropper
|
||||
visible: false,
|
||||
// 弹出层标题
|
||||
title: '修改头像',
|
||||
options: {
|
||||
img: DEFAULT_AVATAR, // 裁剪图片的地址
|
||||
autoCrop: true, // 是否默认生成截图框
|
||||
autoCropWidth: 200, // 默认生成截图框宽度
|
||||
autoCropHeight: 200, // 默认生成截图框高度
|
||||
fixedBox: true, // 固定截图框大小 不允许改变
|
||||
outputType: 'png' // 默认生成截图为PNG格式
|
||||
},
|
||||
previews: {},
|
||||
resizeHandler: null,
|
||||
// 当前生效的头像地址(本地上传成功后优先于此值,避免被关闭弹窗重置)
|
||||
currentAvatar: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 跟随传入学员的头像地址变化
|
||||
'user.avatar': {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.currentAvatar = val || ''
|
||||
this.options.img = this.resolveAvatar(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 头像地址解析:相对路径补全 baseApi,空值使用默认头像
|
||||
// withStamp=true 时追加时间戳,强制浏览器重新加载,避免缓存导致图片不刷新
|
||||
resolveAvatar(avatar, withStamp) {
|
||||
if (!avatar) {
|
||||
return DEFAULT_AVATAR
|
||||
}
|
||||
if (avatar.indexOf('http') === 0 || avatar.indexOf('data:') === 0) {
|
||||
return avatar
|
||||
}
|
||||
let url = process.env.VUE_APP_BASE_API + avatar
|
||||
if (withStamp) {
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + 't=' + new Date().getTime()
|
||||
}
|
||||
return url
|
||||
},
|
||||
handleAvatarError() {
|
||||
this.options.img = DEFAULT_AVATAR
|
||||
},
|
||||
// 编辑头像
|
||||
editCropper() {
|
||||
this.open = true
|
||||
},
|
||||
// 打开弹出层结束时的回调
|
||||
modalOpened() {
|
||||
this.visible = true
|
||||
if (!this.resizeHandler) {
|
||||
this.resizeHandler = debounce(() => {
|
||||
this.refresh()
|
||||
}, 100)
|
||||
}
|
||||
window.addEventListener('resize', this.resizeHandler)
|
||||
},
|
||||
// 刷新组件
|
||||
refresh() {
|
||||
this.$refs.cropper.refresh()
|
||||
},
|
||||
// 覆盖默认的上传行为
|
||||
requestUpload() {
|
||||
},
|
||||
// 向左旋转
|
||||
rotateLeft() {
|
||||
this.$refs.cropper.rotateLeft()
|
||||
},
|
||||
// 向右旋转
|
||||
rotateRight() {
|
||||
this.$refs.cropper.rotateRight()
|
||||
},
|
||||
// 图片缩放
|
||||
changeScale(num) {
|
||||
num = num || 1
|
||||
this.$refs.cropper.changeScale(num)
|
||||
},
|
||||
// 上传预处理
|
||||
beforeUpload(file) {
|
||||
if (file.type.indexOf('image/') === -1) {
|
||||
this.$modal.msgError('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')
|
||||
} else {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => {
|
||||
this.options.img = reader.result
|
||||
}
|
||||
}
|
||||
},
|
||||
// 上传图片:走通用上传 /common/upload,只存文件不修改 sys_user.avatar
|
||||
uploadImg() {
|
||||
this.$refs.cropper.getCropBlob(data => {
|
||||
const formData = new FormData()
|
||||
// 使用带文件名的 File,保证后端能识别后缀
|
||||
const uploadFile = new File([data], 'avatar.png', { type: 'image/png' })
|
||||
formData.append('file', uploadFile)
|
||||
request({
|
||||
url: '/common/upload',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
}).then(response => {
|
||||
// 入库用相对路径,如 /profile/upload/2026/09/28/xxx.png
|
||||
const imgUrl = response.fileName || response.url
|
||||
// 记录当前生效头像,防止弹窗关闭时被旧值覆盖
|
||||
this.currentAvatar = imgUrl
|
||||
// 追加时间戳强制刷新,避免浏览器缓存
|
||||
this.options.img = this.resolveAvatar(imgUrl, true)
|
||||
// 先抛给父组件更新 user.avatar,再关闭弹窗
|
||||
this.$emit('success', imgUrl)
|
||||
this.$modal.msgSuccess('上传成功')
|
||||
this.open = false
|
||||
this.visible = false
|
||||
})
|
||||
})
|
||||
},
|
||||
// 实时预览
|
||||
realTime(data) {
|
||||
this.previews = data
|
||||
},
|
||||
// 关闭窗口:优先使用本地已上传的头像,其次取父组件传入的 user.avatar
|
||||
closeDialog() {
|
||||
this.options.img = this.resolveAvatar(this.currentAvatar || (this.user && this.user.avatar))
|
||||
this.visible = false
|
||||
window.removeEventListener('resize', this.resizeHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.user-info-head {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.user-info-head:hover:after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
color: #eee;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
cursor: pointer;
|
||||
line-height: 110px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user