This commit is contained in:
guochunsi
2026-01-06 19:23:18 +08:00
parent 8af3aaa9b6
commit e1cb334fbf
33 changed files with 685 additions and 329 deletions

View File

@@ -0,0 +1,871 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 搜索表单 -->
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="handleFilter">
<el-form-item label="班级名称" prop="companyId">
<el-select
v-model="state.queryForm.companyId"
filterable
clearable
placeholder="请选择班级"
style="width: 200px"
>
<el-option
v-for="item in companyList"
:key="item.id"
:label="item.companyName"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="学员编号" prop="employeeNo">
<el-input
v-model="state.queryForm.employeeNo"
placeholder="请输入学员编号"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input
v-model="state.queryForm.realName"
placeholder="请输入姓名"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="身份证" prop="idCard">
<el-input
v-model="state.queryForm.idCard"
placeholder="请输入身份证"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input
v-model="state.queryForm.mobile"
placeholder="请输入手机"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="允许进出" prop="inoutFlag">
<el-select
v-model="state.queryForm.inoutFlag"
clearable
placeholder="请选择"
style="width: 200px"
>
<el-option
v-for="item in yesNoDict"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="handleFilter">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<!-- 操作按钮 -->
<el-row>
<div class="mb15" style="width: 100%;">
<el-button
type="primary"
icon="FolderAdd"
@click="handleAdd"
v-if="permissions.professional_outercompanyemployee_add">
</el-button>
<el-button
type="primary"
@click="handleExportIn"
v-if="permission.scope == '1'"
>
</el-button>
<el-button
type="warning"
@click="handleExportScore"
:loading="exportLoading"
icon="Download">导出
</el-button>
<el-button
type="primary"
@click="batchDelect">批量删除
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<!-- 表格 -->
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="companyName" label="班级名称" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="employeeNo" label="学员编号" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="realName" label="姓名" min-width="100" align="center" show-overflow-tooltip />
<el-table-column prop="idCard" label="身份证" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="mobile" label="手机" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="inoutFlag" label="允许进出" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.inoutFlag">{{ getDictLabel(scope.row.inoutFlag) }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="头像" width="100" align="center">
<template #default="scope">
<img
width="50px"
height="50px"
@click="handlePictureCardPreview(scope.row.employeeNo)"
:src="getImageView(scope.row.employeeNo)"
@error="handleImageError"
style="cursor: pointer; object-fit: cover; border-radius: 4px;"
/>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" width="180" align="center" />
<el-table-column label="操作" width="250" align="center" fixed="right">
<template #default="scope">
<el-button
v-if="permissions.professional_outercompanyemployee_edit"
icon="edit-pen"
link
type="primary"
@click="handleEdit(scope.row)">修改
</el-button>
<el-button
v-if="permissions.professional_outercompanyemployee_reset_pw"
icon="RefreshLeft"
link
type="primary"
style="margin-left: 12px"
@click="resetPassword(scope.row)">重置密码
</el-button>
<el-button
v-if="permissions.professional_outercompanyemployee_del"
icon="delete"
link
type="primary"
style="margin-left: 12px"
@click="handleDel(scope.row)">删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-bind="state.pagination"
@current-change="currentChangeHandle"
@size-change="sizeChangeHandle"
/>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="form.id ? '编辑' : '新增'"
width="800px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="120px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="班级名称" prop="companyId">
<el-select
v-model="form.companyId"
filterable
clearable
placeholder="请选择班级"
style="width: 100%"
>
<el-option
v-for="item in companyList"
:key="item.id"
:label="item.companyName"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学员编号" prop="employeeNo">
<el-input
v-model="form.employeeNo"
placeholder="系统自动生成"
disabled
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="realName">
<el-input
v-model="form.realName"
placeholder="请输入姓名"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="身份证" prop="idCard">
<el-input
v-model="form.idCard"
placeholder="请输入身份证号"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机" prop="mobile">
<el-input
v-model="form.mobile"
placeholder="请输入手机号"
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="允许进出" prop="inoutFlag">
<el-select
v-model="form.inoutFlag"
clearable
placeholder="请选择"
style="width: 100%"
>
<el-option
v-for="item in yesNoDict"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remarks">
<el-input
v-model="form.remarks"
type="textarea"
:rows="3"
placeholder="请输入备注"
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 头像预览对话框 -->
<el-dialog v-model="dialogUploadVisible" title="头像预览" append-to-body>
<img width="100%" :src="dialogImageUrl" alt="" style="max-height: 600px; object-fit: contain;" />
</el-dialog>
<!-- 上传头像对话框 -->
<el-dialog v-model="dialogAvatarVisible" title="上传头像" width="50%" append-to-body>
<el-upload
action="/basic/basicstudent/uploadAvatarBase64"
list-type="picture-card"
name="file"
:headers="headers"
:limit="1"
:data="uploadData"
:file-list="fileList"
:before-upload="beforeUpload"
:on-remove="removeHandler"
:http-request="httpRequest"
:on-success="uploadSuccess"
>
<el-icon><Plus /></el-icon>
<template #tip>
<div class="el-upload__tip">上传头像人脸识别用</div>
</template>
</el-upload>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog v-model="dialogViewVisible" title="导入文件" append-to-body>
<el-upload
class="upload-container"
ref="uploadFormRef"
action="doUpload"
:limit="1"
:file-list="filesList"
:before-upload="fileUpload"
:auto-upload="false"
>
<template #trigger>
<el-button type="primary">选取文件</el-button>
</template>
<a href="outercomanyemployee.xlsx" rel="external nofollow" download="模板" style="margin-left: 20px">
<el-button type="success">下载模板</el-button>
</a>
<template #tip>
<div class="el-upload__tip">只能上传excel文件且不超过5MB</div>
<div class="el-upload__tip">{{ fileName }}</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogViewVisible = false">取消</el-button>
<el-button type="primary" @click="submitUpload">导入</el-button>
</div>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserInfo } from '/@/stores/userInfo'
import { BasicTableProps, useTable } from '/@/hooks/table'
import { useMessage } from '/@/hooks/message'
import { useMessageBox } from '/@/hooks/message'
import { useDict } from '/@/hooks/dict'
import { Session } from '/@/utils/storage'
import { validateNull } from '/@/utils/validate'
import axios from 'axios'
import request from '/@/utils/request'
import { Plus } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
import {
fetchList,
getObj,
saveSecond,
putObj,
delObj,
batchDel,
resetPassWord
} from '/@/api/professional/stayschool/outercompanyemployee'
import { getList as getCompanyList } from '/@/api/professional/stayschool/outercompany'
// 使用 Pinia store
const userInfoStore = useUserInfo()
const { userInfos } = storeToRefs(userInfoStore)
// 创建权限对象
const permissions = computed(() => {
const perms: Record<string, boolean> = {}
userInfos.value.authBtnList.forEach((perm: string) => {
perms[perm] = true
})
return perms
})
// 消息提示 hooks
const message = useMessage()
const messageBox = useMessageBox()
// 字典数据
const { yes_no: yesNoDict } = useDict('yes_no')
// 获取字典标签的辅助函数
const getDictLabel = (value: string | number) => {
const item = yesNoDict.value.find((i: any) => i.value === value)
return item ? item.label : ''
}
// 表格引用
const tableRef = ref()
const formRef = ref()
const queryRef = ref()
const uploadFormRef = ref()
// 搜索显示
const showSearch = ref(true)
// 弹窗状态
const dialogVisible = ref(false)
const dialogUploadVisible = ref(false)
const dialogAvatarVisible = ref(false)
const dialogViewVisible = ref(false)
const submitLoading = ref(false)
const exportLoading = ref(false)
// 选中的行数据
const selectList = ref<any[]>([])
const permission = reactive({
hasPermission: "0",
scope: "0"
})
// 单位列表
const companyList = ref<any[]>([])
// 表单数据
const form = reactive({
id: '',
companyId: '',
companyName: '',
employeeNo: '',
realName: '',
idCard: '',
mobile: '',
inoutFlag: '',
remarks: ''
})
// 表单验证规则 - 培训单位没有必填验证
const formRules = {
companyId: [
{ required: true, message: '请选择班级', trigger: 'change' }
]
}
// 头像相关
const dialogImageUrl = ref('')
const fileList = ref<any[]>([])
const rowData = ref<any>({})
const fileReader = ref<FileReader | null>(null)
// 上传相关
const uploadData = reactive({
bucketName: "base",
module: "basic",
username: ""
})
// 导入相关
const fileName = ref('')
const filesList = ref<any[]>([])
let files: File | null = null
// 请求头
const headers = computed(() => {
return {
"Authorization": 'Bearer ' + Session.getToken()
}
})
// 配置 useTable - 注意这个 API 返回的数据结构特殊
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: async (params: any) => {
const response = await fetchList({
...params,
companyType: '1' // 培训单位
})
// 特殊处理API 返回的是 response.data.data.dataList.records
const dataList = response.data?.data?.dataList || response.data?.dataList || {}
permission.hasPermission = response.data?.data?.permission?.hasPermission || "0"
permission.scope = response.data?.data?.permission?.scope || "0"
return {
data: {
records: dataList.records || [],
total: dataList.total || 0
}
}
},
queryForm: {
companyId: '',
employeeNo: '',
realName: '',
idCard: '',
mobile: '',
inoutFlag: ''
}
})
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state)
// 获取单位列表
const loadCompanyList = async () => {
try {
const response = await getCompanyList({ companyType: '1' })
companyList.value = response.data || []
} catch (error) {
// 获取单位列表失败
}
}
// 重置查询
const resetQuery = () => {
queryRef.value?.resetFields()
getDataList()
}
// 处理搜索
const handleFilter = () => {
getDataList()
}
// 多选变化
const handleSelectionChange = (selection: any[]) => {
selectList.value = selection
}
// 打开新增窗口
const handleAdd = () => {
Object.assign(form, {
id: '',
companyId: '',
companyName: '',
employeeNo: '',
realName: '',
idCard: '',
mobile: '',
inoutFlag: '',
remarks: ''
})
dialogVisible.value = true
}
// 打开编辑窗口
const handleEdit = async (row: any) => {
try {
const response = await getObj(row.id)
const data = response.data
Object.assign(form, {
id: data.id,
companyId: data.companyId || '',
companyName: data.companyName || '',
employeeNo: data.employeeNo || '',
realName: data.realName || '',
idCard: data.idCard || '',
mobile: data.mobile || '',
inoutFlag: data.inoutFlag || '',
remarks: data.remarks || ''
})
dialogVisible.value = true
} catch (error) {
// 获取详情失败
}
}
// 删除
const handleDel = (row: any) => {
messageBox.confirm('是否确认删除该条记录').then(async () => {
await delObj(row.id)
message.success('删除成功')
// 如果当前页只剩一条数据,且不是第一页,则跳转到上一页
if (state.pagination && state.dataList && state.dataList.length === 1 && state.pagination.current && state.pagination.current > 1) {
state.pagination.current = state.pagination.current - 1
}
getDataList()
}).catch(() => {})
}
// 批量删除
const batchDelect = () => {
if (selectList.value.length === 0) {
message.warning('请至少选择一条数据')
return
}
messageBox.confirm('是否确认删除').then(async () => {
await batchDel(selectList.value)
message.success('删除成功')
selectList.value = []
getDataList()
}).catch(() => {})
}
// 重置密码
const resetPassword = (row: any) => {
messageBox.confirm('是否确定重置密码?').then(async () => {
try {
const response = await resetPassWord(row)
const pw = response.data?.data
if (!validateNull(pw)) {
ElMessageBox.alert('重置后密码为: ' + pw, '重置密码')
} else {
ElMessageBox.alert('系统繁忙,请重试', '重置密码')
}
} catch (error) {
// 重置密码失败
}
}).catch(() => {})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid: boolean) => {
if (valid) {
submitLoading.value = true
try {
// 设置单位名称
const selectedCompany = companyList.value.find(item => item.id === form.companyId)
if (selectedCompany) {
form.companyName = selectedCompany.companyName
}
if (form.id) {
await putObj(form)
message.success('修改成功')
} else {
await saveSecond(form) // 使用 saveSecond API
message.success('添加成功')
}
dialogVisible.value = false
getDataList()
} catch (error: any) {
message.error(error?.msg || '操作失败')
} finally {
submitLoading.value = false
}
}
})
}
// 获取图片 URL
const getImageView = (employeeNo: string) => {
const timestamp = Date.parse(new Date().toString())
const baseUrl = import.meta.env.VITE_API_URL || ''
return `${baseUrl}/admin/user/photo/${employeeNo}?${timestamp}`
}
// 图片加载错误处理
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.src = '/img/default/no_pic.png'
}
// 预览头像
const handlePictureCardPreview = (employeeNo: string) => {
dialogImageUrl.value = getImageView(employeeNo)
dialogUploadVisible.value = true
}
// 上传前验证
const beforeUpload = (file: File) => {
const isLt5M = file.size < 1 * 1024 * 1024
if (fileList.value.length >= 1) {
message.warning('只能上传一张头像')
return false
}
if (!isLt5M) {
message.warning('文件大小不能超过1M')
return false
}
return true
}
// 自定义上传
const httpRequest = (options: any) => {
const file = options.file
if (!fileReader.value) {
fileReader.value = new FileReader()
}
if (file) {
fileReader.value.readAsDataURL(file)
}
fileReader.value.onload = () => {
const base64Str = fileReader.value?.result as string
const config = {
url: '/basic/basicstudent/uploadAvatarBase64',
method: 'post',
data: {
base64Str: base64Str.split(',')[1],
username: rowData.value.employeeNo
},
timeout: 10000,
onUploadProgress: (progressEvent: any) => {
progressEvent.percent = progressEvent.loaded / progressEvent.total * 100
options.onProgress(progressEvent, file)
}
}
axios(config)
.then(res => {
options.onSuccess(res, file)
getDataList()
})
.catch(err => {
options.onError(err)
})
}
}
// 移除文件
const removeHandler = (file: any) => {
const index = fileList.value.findIndex(f => f.uid === file.uid)
if (index !== -1) {
fileList.value.splice(index, 1)
}
}
// 上传成功
const uploadSuccess = (res: any, file: any) => {
if (res.data) {
const data = res.data
file.key = data.key
fileList.value.push(file)
message.success('上传成功')
dialogAvatarVisible.value = false
getDataList()
}
}
// 导入
const handleExportIn = () => {
fileName.value = ""
filesList.value = []
files = null
dialogViewVisible.value = true
}
// 文件上传验证
const fileUpload = (file: File) => {
const fileLast = file.name.split('.')
const extension = fileLast[fileLast.length - 1] === 'xls'
const extension2 = fileLast[fileLast.length - 1] === 'xlsx'
const isLt2M = file.size / 1024 / 1024 < 5
if (!extension && !extension2) {
message.warning('上传模板只能是 xls、xlsx格式!')
return false
}
if (!isLt2M) {
message.warning('上传模板大小不能超过 5MB!')
return false
}
fileName.value = file.name
files = file
return false // 返回false不会自动上传
}
// 提交导入
const submitUpload = async () => {
if (!fileName.value || !files) {
message.warning('请选择要上传的文件!')
return
}
const fileFormData = new FormData()
fileFormData.append('file', files, fileName.value)
try {
const response = await request({
url: `/professional/file/exportOuterCompanyEmployee?companyType=1`,
method: 'post',
data: fileFormData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.code === 0) {
message.success('操作成功')
dialogViewVisible.value = false
getDataList()
} else {
message.error(response.msg || '导入失败')
}
} catch (error: any) {
message.error(error?.msg || '导入失败')
}
}
// 导出
const handleExportScore = async () => {
if (!state.queryForm || Object.keys(state.queryForm).length === 0) {
message.warning('请选择导出条件')
return
}
exportLoading.value = true
try {
const params = {
...state.queryForm,
companyType: "1"
}
const response = await axios({
method: 'post',
url: '/professional/outercompanyemployee/export',
data: params,
responseType: 'blob',
headers: {
'Content-Type': 'application/json'
}
})
const blob = new Blob([response.data])
const fileName = '培训单位人员导出表.xls'
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
message.success('导出成功')
} catch (error: any) {
message.error(error?.msg || '导出失败')
} finally {
exportLoading.value = false
}
}
// 初始化
onMounted(() => {
if (!window.FileReader) {
// eslint-disable-next-line no-console
console.error('Your browser does not support FileReader API!')
} else {
fileReader.value = new FileReader()
}
loadCompanyList()
})
</script>
<style lang="scss" scoped>
.upload-container {
:deep(.el-upload) {
width: 100%;
}
}
</style>