|
|
<template>
|
|
|
<div class="personnel-container">
|
|
|
<div class="page-header">
|
|
|
<span class="page-title">已录入人员</span>
|
|
|
<el-button type="primary" @click="handleAdd">
|
|
|
<el-icon><Plus /></el-icon>
|
|
|
添加人员
|
|
|
</el-button>
|
|
|
</div>
|
|
|
|
|
|
<div class="search-bar">
|
|
|
<el-input
|
|
|
v-model="searchForm.name"
|
|
|
placeholder="姓名"
|
|
|
clearable
|
|
|
style="width: 160px"
|
|
|
@keyup.enter="handleSearch"
|
|
|
/>
|
|
|
<el-input
|
|
|
v-model="searchForm.department"
|
|
|
placeholder="所属部门"
|
|
|
clearable
|
|
|
style="width: 160px"
|
|
|
@keyup.enter="handleSearch"
|
|
|
/>
|
|
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
|
|
<el-button @click="handleReset">重置</el-button>
|
|
|
</div>
|
|
|
|
|
|
<div class="card-list">
|
|
|
<div
|
|
|
v-for="item in personnelList"
|
|
|
:key="item.id"
|
|
|
class="personnel-card"
|
|
|
>
|
|
|
<div class="card-header">
|
|
|
<el-avatar :size="64" :src="fileHttp + item.avatar">
|
|
|
<el-icon :size="32"><UserFilled /></el-icon>
|
|
|
</el-avatar>
|
|
|
<div class="person-info">
|
|
|
<h3 class="person-name">{{ item.name }}</h3>
|
|
|
<span class="person-id">工号:{{ item.employeeId }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card-body">
|
|
|
<div class="info-row">
|
|
|
<span class="label">人脸样本</span>
|
|
|
<div class="face-samples">
|
|
|
<span class="no-data">
|
|
|
{{ `${item.faceCount || '暂无样本'}` }}
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="info-row">
|
|
|
<span class="label">所属部门</span>
|
|
|
<span class="value">{{ item.department || '-' }}</span>
|
|
|
</div>
|
|
|
<div class="info-row">
|
|
|
<span class="label">联系方式</span>
|
|
|
<span class="value">{{ item.contact || '-' }}</span>
|
|
|
</div>
|
|
|
<div class="info-row">
|
|
|
<span class="label">性别</span>
|
|
|
<span class="value">{{ item.gender == 1 ? '男' : item.gender == 2 ? '女' : '-' }}</span>
|
|
|
</div>
|
|
|
<div class="info-row">
|
|
|
<span class="label">年龄</span>
|
|
|
<span class="value">{{ item.age || '-' }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card-footer">
|
|
|
<el-button type="primary" plain @click="handleEdit(item)">
|
|
|
<el-icon><Edit /></el-icon>
|
|
|
编辑
|
|
|
</el-button>
|
|
|
<el-button type="danger" plain @click="handleDelete(item)">
|
|
|
<el-icon><Delete /></el-icon>
|
|
|
删除
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div v-if="!personnelList.length" class="empty-state">
|
|
|
<el-empty description="暂无人员数据" />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="pagination-wrapper" v-if="total > 0">
|
|
|
<el-pagination
|
|
|
v-model:current-page="currentPage"
|
|
|
v-model:page-size="pageSize"
|
|
|
:page-sizes="[12, 24, 36, 48]"
|
|
|
:total="total"
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
background
|
|
|
@current-change="handlePageChange"
|
|
|
@size-change="handleSizeChange"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
<!-- 添加/编辑弹窗 -->
|
|
|
<PersonnelFormDialog
|
|
|
v-model="formDialogVisible"
|
|
|
:form-data="currentPerson"
|
|
|
:is-edit="isEdit"
|
|
|
@success="handleFormSuccess"
|
|
|
/>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import PersonnelFormDialog from './components/PersonnelFormDialog.vue'
|
|
|
import { getPersonnelList, deletePerson } from '@/api/personnel'
|
|
|
import fileHttp from '@/utils/fileHttp'
|
|
|
|
|
|
const currentPage = ref(1)
|
|
|
const pageSize = ref(12)
|
|
|
const total = ref(0)
|
|
|
const personnelList = ref([])
|
|
|
|
|
|
// 搜索条件
|
|
|
const searchForm = reactive({
|
|
|
name: '',
|
|
|
department: ''
|
|
|
})
|
|
|
|
|
|
// 表单弹窗
|
|
|
const formDialogVisible = ref(false)
|
|
|
const isEdit = ref(false)
|
|
|
const currentPerson = ref({})
|
|
|
|
|
|
// 获取人员列表
|
|
|
const fetchPersonnelList = async () => {
|
|
|
try {
|
|
|
const res = await getPersonnelList({
|
|
|
name: searchForm.name || undefined,
|
|
|
department: searchForm.department || undefined,
|
|
|
pageNum: currentPage.value,
|
|
|
pageSize: pageSize.value
|
|
|
})
|
|
|
personnelList.value = res.data?.list || res.data || []
|
|
|
total.value = res.data?.total || 0
|
|
|
} catch {
|
|
|
personnelList.value = []
|
|
|
total.value = 0
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 搜索
|
|
|
const handleSearch = () => {
|
|
|
currentPage.value = 1
|
|
|
fetchPersonnelList()
|
|
|
}
|
|
|
|
|
|
// 重置搜索
|
|
|
const handleReset = () => {
|
|
|
searchForm.name = ''
|
|
|
searchForm.department = ''
|
|
|
currentPage.value = 1
|
|
|
fetchPersonnelList()
|
|
|
}
|
|
|
|
|
|
// 分页处理
|
|
|
const handlePageChange = (page) => {
|
|
|
currentPage.value = page
|
|
|
fetchPersonnelList()
|
|
|
}
|
|
|
|
|
|
const handleSizeChange = (size) => {
|
|
|
pageSize.value = size
|
|
|
currentPage.value = 1
|
|
|
fetchPersonnelList()
|
|
|
}
|
|
|
|
|
|
// 添加
|
|
|
const handleAdd = () => {
|
|
|
isEdit.value = false
|
|
|
currentPerson.value = {}
|
|
|
formDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
// 编辑
|
|
|
const handleEdit = (item) => {
|
|
|
isEdit.value = true
|
|
|
currentPerson.value = { ...item }
|
|
|
formDialogVisible.value = true
|
|
|
}
|
|
|
|
|
|
// 删除
|
|
|
const handleDelete = async (item) => {
|
|
|
try {
|
|
|
await ElMessageBox.confirm(`确认删除人员「${item.name}」吗?删除后不可恢复。`, '提示', {
|
|
|
confirmButtonText: '确定',
|
|
|
cancelButtonText: '取消',
|
|
|
type: 'warning'
|
|
|
})
|
|
|
await deletePerson(item.id)
|
|
|
ElMessage.success('删除成功')
|
|
|
fetchPersonnelList()
|
|
|
} catch {
|
|
|
// 取消或失败不做处理
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 表单提交成功
|
|
|
const handleFormSuccess = () => {
|
|
|
fetchPersonnelList()
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
fetchPersonnelList()
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.personnel-container {
|
|
|
height: calc(100vh - 120px);
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.page-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
.page-title {
|
|
|
font-size: 18px;
|
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.search-bar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 16px;
|
|
|
padding: 12px 16px;
|
|
|
background: #fff;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
.card-list {
|
|
|
flex: 1;
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
gap: 16px;
|
|
|
overflow-y: auto;
|
|
|
padding-bottom: 20px;
|
|
|
align-content: start;
|
|
|
}
|
|
|
|
|
|
.personnel-card {
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
border: 1px solid #ebeef5;
|
|
|
padding: 20px;
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
&:hover {
|
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
|
transform: translateY(-2px);
|
|
|
}
|
|
|
|
|
|
.card-header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 12px;
|
|
|
padding-bottom: 16px;
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
.person-info {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
|
|
|
.person-name {
|
|
|
font-size: 16px;
|
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
|
margin: 0 0 4px;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.person-id {
|
|
|
font-size: 12px;
|
|
|
color: #909399;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.card-body {
|
|
|
.info-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
padding: 8px 0;
|
|
|
font-size: 13px;
|
|
|
|
|
|
.label {
|
|
|
width: 70px;
|
|
|
color: #909399;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.value {
|
|
|
color: #606266;
|
|
|
flex: 1;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.face-samples {
|
|
|
flex: 1;
|
|
|
display: flex;
|
|
|
gap: 4px;
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
.no-data {
|
|
|
color: #c0c4cc;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.card-footer {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
padding-top: 16px;
|
|
|
border-top: 1px solid #f0f0f0;
|
|
|
margin-top: 8px;
|
|
|
|
|
|
.el-button {
|
|
|
flex: 1;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.empty-state {
|
|
|
grid-column: 1 / -1;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
min-height: 300px;
|
|
|
}
|
|
|
|
|
|
.pagination-wrapper {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
padding-top: 16px;
|
|
|
background: #fff;
|
|
|
border-radius: 8px;
|
|
|
padding: 16px;
|
|
|
}
|
|
|
</style>
|