You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

365 lines
8.0 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>