整合前端

This commit is contained in:
砂糖
2026-04-13 17:04:38 +08:00
parent 69609a2cb1
commit 5d4794c9bd
915 changed files with 144259 additions and 0 deletions

View File

@@ -0,0 +1,478 @@
<template>
<div class="employee-page">
<div class="main-layout">
<!-- Left: Department Tree for Filtering -->
<el-card shadow="never" class="dept-tree-card">
<div slot="header" class="card-header">
<span class="header-title">部门列表</span>
<el-button size="small" icon="el-icon-refresh" @click="loadDeptTree">刷新</el-button>
</div>
<div class="tree-wrapper">
<el-tree v-loading="deptLoading" :data="deptTree" :props="{ label: 'deptName', children: 'children' }"
node-key="deptId" default-expand-all highlight-current :expand-on-click-node="false"
@node-click="handleDeptNodeClick">
<span slot-scope="{ node }" class="tree-node-label">
<span>
{{ node.label }}
</span>
<span v-if="node.data.leader" class="leader-tag">
({{ node.data.leader }})
</span>
<span v-else class="leader-tag" style="color: red;" @click="goDept">
请配置负责人
</span>
</span>
</el-tree>
</div>
</el-card>
<!-- Right: Employee List -->
<el-card shadow="never" class="employee-list-card">
<div slot="header" class="card-header">
<span class="header-title">员工档案</span>
<div class="header-actions">
<el-input v-model="query.empName" placeholder="姓名/工号" size="small" clearable style="width: 180px;"
@keyup.enter.native="loadEmployeeList" />
<el-select v-model="query.postId" size="small" placeholder="岗位" clearable filterable style="width: 160px;"
@change="loadEmployeeList">
<el-option v-for="post in postOptions" :key="post.postId" :label="post.postName" :value="post.postId" />
</el-select>
<el-select v-model="query.status" size="small" placeholder="状态" clearable style="width: 120px;"
@change="loadEmployeeList">
<el-option label="在职" value="onboard" />
<el-option label="离职" value="leave" />
</el-select>
<el-button size="small" type="primary" icon="el-icon-search" @click="loadEmployeeList">查询</el-button>
<el-button size="small" type="primary" icon="el-icon-plus" @click="handleAdd">新增员工</el-button>
</div>
</div>
<el-table :data="employeeList" v-loading="empLoading" stripe height="calc(100vh - 220px)">
<el-table-column prop="empNo" label="工号" min-width="110" />
<el-table-column prop="empName" label="姓名" min-width="120" />
<el-table-column prop="deptName" label="部门" min-width="140" />
<el-table-column prop="postName" label="岗位" min-width="140" />
<el-table-column prop="mobile" label="手机号" min-width="130" />
<el-table-column prop="status" label="状态" min-width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'onboard' ? 'success' : 'info'" size="small">{{ scope.row.status ===
'onboard' ? '在职' : '离职' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="mini" icon="el-icon-edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="mini" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination :current-page="query.pageNum" :page-sizes="[10, 20, 50, 100]" :page-size="query.pageSize"
:total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handlePageChange" />
</div>
</el-card>
</div>
<!-- Add/Edit Dialog -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="700px" append-to-body>
<el-form ref="empFormRef" :model="empForm" :rules="empRules" label-width="100px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="姓名" prop="empName">
<el-input v-model="empForm.empName" placeholder="请输入员工姓名" @blur="handleEmpNameBlur" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工号" prop="empNo">
<el-input v-model="empForm.empNo" placeholder="请输入工号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="主部门" prop="deptId">
<el-cascader v-model="empForm.deptId" :options="deptTree"
:props="{ label: 'deptName', value: 'deptId', children: 'children', emitPath: false, checkStrictly: true }"
clearable filterable placeholder="请选择主部门" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主岗位" prop="postId">
<el-select v-model="empForm.postId" placeholder="请选择主岗位" filterable style="width: 100%">
<el-option v-for="post in postOptions" :key="post.postId" :label="post.postName" :value="post.postId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="手机号" prop="mobile">
<el-input v-model="empForm.mobile" placeholder="请输入手机号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="empForm.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="empForm.status">
<el-radio label="onboard">在职</el-radio>
<el-radio label="leave">离职</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="关联系统用户">
<el-switch v-model="empForm.linkUser" @change="handleLinkUserChange" />
<span class="form-hint" style="margin-left: 8px;">是否关联 sys_user该员工也是系统用户</span>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16" v-if="empForm.linkUser">
<el-col :span="24">
<el-form-item label="选择用户" prop="userId"
:rules="empForm.linkUser ? [{ required: true, message: '请选择系统用户', trigger: 'change' }] : []">
<el-select v-model="empForm.userId" placeholder="请选择系统用户" filterable remote :remote-method="searchUsers"
:loading="userLoading" style="width: 100%" clearable>
<el-option v-for="user in userOptions" :key="user.userId"
:label="`${user.userName} (${user.nickName || ''})`" :value="user.userId" />
</el-select>
<div class="form-hint" v-if="!userSearchHint">
<span v-if="empForm.empName">已根据姓名"{{ empForm.empName }}"自动搜索</span>
输入用户名或昵称进行搜索
</div>
<div class="form-hint" v-else style="color: #909399;">
{{ userSearchHint }}
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDept } from '@/api/system/dept'
import { listPost } from '@/api/system/post'
import { selectUser } from '@/api/system/user'
import { listEmployee, addEmployee, updateEmployee, delEmployee } from '@/api/hrm/employee'
export default {
name: 'HrmEmployee',
data() {
return {
deptLoading: false,
empLoading: false,
deptTree: [],
postOptions: [],
userOptions: [],
userLoading: false,
userSearchHint: '',
employeeList: [],
total: 0,
query: {
pageNum: 1,
pageSize: 10,
empName: '',
deptId: null,
postId: null,
status: 'onboard'
},
dialogVisible: false,
dialogTitle: '新增员工',
submitting: false,
empForm: {},
empRules: {
empName: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }],
empNo: [{ required: true, message: '请输入工号', trigger: 'blur' }],
deptId: [{ required: true, message: '请选择主部门', trigger: 'change' }],
postId: [{ required: true, message: '请选择主岗位', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
}
},
created() {
this.loadDeptTree()
this.loadPostOptions()
this.loadEmployeeList()
},
methods: {
loadDeptTree() {
this.deptLoading = true
listDept({}).then(res => {
this.deptTree = this.buildTree(res.data || [])
}).finally(() => {
this.deptLoading = false
})
},
goDept() {
this.$router.push('/renshi/dept')
},
buildTree(list) {
const map = {}
const roots = []
list.forEach(item => { map[item.deptId] = { ...item, children: [] } })
list.forEach(item => {
const node = map[item.deptId]
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(node)
} else {
roots.push(node)
}
})
return roots
},
loadPostOptions() {
listPost({ pageNum: 1, pageSize: 1000 }).then(res => {
this.postOptions = res.rows || []
})
},
loadEmployeeList() {
this.empLoading = true
listEmployee(this.query).then(res => {
this.employeeList = res.rows || []
this.total = res.total || 0
}).finally(() => {
this.empLoading = false
})
},
handleDeptNodeClick(data) {
this.query.deptId = data.deptId
this.query.pageNum = 1
this.loadEmployeeList()
},
handleSizeChange(val) {
this.query.pageSize = val
this.loadEmployeeList()
},
handlePageChange(val) {
this.query.pageNum = val
this.loadEmployeeList()
},
handleAdd() {
this.dialogTitle = '新增员工'
this.empForm = {
empName: '',
empNo: '',
deptId: this.query.deptId || undefined,
postId: undefined,
status: 'onboard',
linkUser: false,
userId: undefined
}
this.userOptions = []
this.userSearchHint = ''
this.dialogVisible = true
this.$nextTick(() => { this.$refs.empFormRef?.clearValidate() })
},
handleEdit(row) {
this.dialogTitle = '编辑员工'
this.empForm = {
...row,
linkUser: !!row.userId,
userId: row.userId || undefined
}
this.userSearchHint = ''
if (this.empForm.linkUser && this.empForm.userId) {
this.searchUsers('')
}
this.dialogVisible = true
this.$nextTick(() => { this.$refs.empFormRef?.clearValidate() })
},
handleDelete(row) {
this.$confirm(`确认删除员工 "${row.empName}" 吗?`, '提示', { type: 'warning' }).then(() => {
delEmployee(row.empId).then(() => {
this.$message.success('删除成功')
this.loadEmployeeList()
})
})
},
handleLinkUserChange(val) {
if (!val) {
this.empForm.userId = undefined
this.userOptions = []
this.userSearchHint = ''
} else if (this.empForm.empName) {
// 如果开启了关联用户且已有姓名,自动搜索
this.handleEmpNameBlur()
}
this.$nextTick(() => {
this.$refs.empFormRef?.clearValidate()
})
},
handleEmpNameBlur() {
// 当姓名输入完成且开启了关联用户时,自动用姓名搜索用户
if (this.empForm.linkUser && this.empForm.empName && !this.empForm.userId) {
this.searchUsersByNickName(this.empForm.empName)
}
},
searchUsersByNickName(nickName) {
if (!nickName) return
this.userLoading = true
this.userSearchHint = ''
selectUser({ nickName: nickName, pageNum: 1, pageSize: 10 }).then(res => {
const users = res.rows || []
if (users.length > 0) {
// 精确匹配优先
const exactMatch = users.find(u => u.nickName === nickName)
if (exactMatch) {
this.userOptions = [exactMatch]
this.empForm.userId = exactMatch.userId
this.userSearchHint = `已自动匹配到用户:${exactMatch.userName} (${exactMatch.nickName})`
} else {
// 模糊匹配
this.userOptions = users
if (users.length === 1) {
this.empForm.userId = users[0].userId
this.userSearchHint = `已自动匹配到用户:${users[0].userName} (${users[0].nickName})`
} else {
this.userSearchHint = `找到 ${users.length} 个匹配的用户,请选择`
}
}
} else {
this.userOptions = []
this.userSearchHint = `未找到昵称为"${nickName}"的用户,请手动输入用户名或昵称进行搜索`
}
}).catch(() => {
this.userOptions = []
this.userSearchHint = '搜索失败,请手动输入用户名或昵称进行搜索'
}).finally(() => {
this.userLoading = false
})
},
searchUsers(query) {
if (!query && !this.empForm.userId) {
this.userOptions = []
this.userSearchHint = ''
return
}
this.userLoading = true
this.userSearchHint = ''
selectUser({ userName: query, nickName: query, pageNum: 1, pageSize: 20 }).then(res => {
this.userOptions = res.rows || []
if (this.userOptions.length === 0) {
this.userSearchHint = '未找到匹配的用户,请尝试其他关键词'
} else if (this.userOptions.length === 1) {
// 如果只有一个结果,自动选中
if (!this.empForm.userId) {
this.empForm.userId = this.userOptions[0].userId
this.userSearchHint = `已自动匹配到用户:${this.userOptions[0].userName} (${this.userOptions[0].nickName || ''})`
}
}
// 如果编辑时已有 userId确保在选项中
if (this.empForm.userId && !this.userOptions.find(u => u.userId === this.empForm.userId)) {
// 尝试单独查询该用户
selectUser({ userId: this.empForm.userId }).then(res2 => {
if (res2.rows && res2.rows.length > 0) {
this.userOptions.unshift(res2.rows[0])
}
})
}
}).catch(() => {
this.userOptions = []
this.userSearchHint = '搜索失败,请重试'
}).finally(() => {
this.userLoading = false
})
},
handleSubmit() {
this.$refs.empFormRef.validate(valid => {
if (!valid) return
// 如果不关联用户,清空 userId
if (!this.empForm.linkUser) {
this.empForm.userId = undefined
}
this.submitting = true
const api = this.empForm.empId ? updateEmployee : addEmployee
api(this.empForm).then(() => {
this.$message.success(this.empForm.empId ? '更新成功' : '新增成功')
this.dialogVisible = false
// 新增后重置到第一页并刷新
if (!this.empForm.empId) {
this.query.pageNum = 1
}
this.loadEmployeeList()
}).catch(() => {
// 错误已在 request 拦截器中处理
}).finally(() => {
this.submitting = false
})
})
}
}
}
</script>
<style lang="scss" scoped>
.employee-page {
padding: 20px;
background-color: #f0f2f5;
}
.main-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 16px;
}
.dept-tree-card,
.employee-list-card {
border: none;
border-radius: 4px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.header-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 8px;
}
}
.tree-wrapper {
height: calc(100vh - 160px);
overflow-y: auto;
}
.tree-node-label {
font-size: 14px;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.form-hint {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.dialog-footer {
text-align: right;
}
</style>