分组管理模块调试完成

This commit is contained in:
BianLzhaoMin 2026-01-27 11:30:04 +08:00
parent 49cc1d8afa
commit d8cccea4c5
9 changed files with 848 additions and 149 deletions

View File

@ -0,0 +1,38 @@
import request from '@/utils/request'
// 分组管理 - 查询列表
export function listGroupAPI(query) {
return request({
url: '/group/getGroupList',
method: 'GET',
params: query,
})
}
// 分组管理 - 新增
export function addGroupAPI(data) {
return request({
url: '/group/addGroup',
method: 'POST',
data,
})
}
// 分组管理 - 修改
export function updateGroupAPI(data) {
return request({
url: '/group/updateGroup',
method: 'POST',
data,
})
}
// 分组管理 - 删除
export function delGroupAPI(data) {
return request({
url: '/group/delGroup',
method: 'POST',
data,
})
}

View File

@ -35,3 +35,11 @@ export function delPersonAPI(data) {
data,
})
}
// 获取人员全量
export function getAllPersonAPI() {
return request({
url: '/worker/getWorkerSelect',
method: 'GET',
})
}

View File

@ -196,7 +196,7 @@ aside {
.fixed-bottom {
position: fixed;
bottom: 15px;
bottom: 10px;
right: 15px;
width: 100%;
background-color: #fff;

View File

@ -0,0 +1,502 @@
<template>
<el-form
size="large"
label-width="auto"
:model="formData"
ref="formRef"
:rules="rules"
class="add-and-edit-form"
>
<el-form-item label="分组名称" prop="groupName">
<el-input
clearable
maxlength="50"
show-word-limit
placeholder="请输入分组名称"
v-model.trim="formData.groupName"
/>
</el-form-item>
<el-form-item label="选择组员" prop="workerList" class="member-select-item">
<div class="member-select-container">
<!-- 左侧可选人员列表 -->
<div class="member-list-left">
<div class="search-bar">
<el-row :gutter="10">
<el-col :span="12">
<el-input
v-model="searchDept"
placeholder="输入部门名称"
clearable
prefix-icon="Search"
/>
</el-col>
<el-col :span="12">
<el-input
v-model="searchName"
placeholder="输入人员姓名"
clearable
prefix-icon="Search"
/>
</el-col>
</el-row>
</div>
<div class="table-container">
<el-table
ref="personTableRef"
:data="filteredPersonList"
height="300"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="orgName" label="人员所属" width="150" />
<el-table-column prop="workerName" label="姓名" width="120" />
<el-table-column prop="sex" label="性别" width="80">
<template #default="{ row }">
{{ row.sex == 1 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="电话" />
</el-table>
</div>
</div>
<!-- 右侧已选人员列表 -->
<div class="member-list-right">
<div class="selected-header">
<span>已选人员</span>
<el-button
type="text"
size="small"
@click="handleClearAll"
:disabled="selectedMembers.length === 0"
>
清空
</el-button>
</div>
<div class="selected-list">
<div
v-for="member in selectedMembers"
:key="member.id"
class="selected-item"
>
<span>{{ member.workerName }}</span>
<el-icon class="remove-icon" @click="handleRemoveMember(member.id)">
<Close />
</el-icon>
</div>
<div v-if="selectedMembers.length === 0" class="empty-tip">
暂无已选人员
</div>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
placeholder="请输入备注"
v-model.trim="formData.remark"
/>
</el-form-item>
</el-form>
</template>
<script setup name="AddAndEditForm">
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { getAllPersonAPI } from '@/api/personManage/person.js'
import { Close } from '@element-plus/icons-vue'
const props = defineProps({
formData: {
type: Object,
default: () => ({}),
},
})
const formRef = ref(null)
const personTableRef = ref(null)
const searchDept = ref('') //
const searchName = ref('') //
const personList = ref([])
const selectedPersonList = ref([])
//
const isSyncing = ref(false)
//
const rules = {
groupName: [{ required: true, message: '请输入分组名称', trigger: 'blur' }],
workerList: [
{
required: true,
message: '请至少选择一个组员',
trigger: 'change',
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少选择一个组员'))
} else {
callback()
}
},
},
],
}
//
const filteredPersonList = computed(() => {
let filtered = personList.value
//
if (searchDept.value) {
const deptKeyword = searchDept.value.toLowerCase()
filtered = filtered.filter((item) => {
const org = (item.orgName || '').toLowerCase()
return org.includes(deptKeyword)
})
}
//
if (searchName.value) {
const nameKeyword = searchName.value.toLowerCase()
filtered = filtered.filter((item) => {
const name = (item.workerName || '').toLowerCase()
return name.includes(nameKeyword)
})
}
return filtered
})
//
const selectedMembers = computed(() => {
if (!props.formData.workerList || props.formData.workerList.length === 0) {
return []
}
return personList.value.filter((item) => props.formData.workerList.includes(item.id))
})
//
const getPersonList = async () => {
try {
const result = await getAllPersonAPI()
if (result.code === 200) {
// result.rows result.data
const data = result.rows || result.data || result || []
personList.value = data.map((item) => ({
id: item.id,
workerName: item.workerName,
orgName: item.orgName,
sex: item.sex,
phone: item.phone,
}))
// watch
// syncTableSelection
}
} catch (error) {
console.error('获取人员列表失败:', error)
//
isSyncing.value = false
}
}
// formData.workerList
watch(
() => props.formData.workerList,
(newVal, oldVal) => {
//
if (isSyncing.value) {
return
}
// workerList
if (personList.value.length > 0 && newVal && newVal.length > 0) {
//
const newValStr = JSON.stringify(newVal.sort())
const oldValStr = oldVal ? JSON.stringify(oldVal.sort()) : ''
if (newValStr !== oldValStr) {
nextTick(() => {
syncTableSelection()
})
}
}
},
{ deep: true },
)
//
watch(
() => personList.value.length,
(newLength, oldLength) => {
//
if (isSyncing.value) {
return
}
// 0
if (
oldLength === 0 &&
newLength > 0 &&
props.formData.workerList &&
props.formData.workerList.length > 0
) {
nextTick(() => {
syncTableSelection()
})
}
},
)
//
const syncTableSelection = () => {
if (
!personTableRef.value ||
!props.formData.workerList ||
props.formData.workerList.length === 0
) {
//
if (personTableRef.value) {
isSyncing.value = true
personTableRef.value.clearSelection()
// clearSelection
nextTick(() => {
isSyncing.value = false
})
}
return
}
// watch handleSelectionChange
isSyncing.value = true
// workerList
const selectedIds = props.formData.workerList.map((id) => Number(id))
nextTick(() => {
// selection-change isSyncing=true
personTableRef.value.clearSelection()
//
// 使
filteredPersonList.value.forEach((row) => {
const rowId = Number(row.id)
if (selectedIds.includes(rowId)) {
personTableRef.value.toggleRowSelection(row, true)
}
})
//
nextTick(() => {
nextTick(() => {
isSyncing.value = false
})
})
})
}
//
const handleSelectionChange = (selection) => {
//
if (isSyncing.value) {
return
}
selectedPersonList.value = selection
// workerList
// ID
const allSelectedIds = new Set(props.formData.workerList || [])
// ID
const currentFilteredIds = new Set(filteredPersonList.value.map((item) => item.id))
//
// currentFilteredIds
currentFilteredIds.forEach((id) => {
if (!selection.find((s) => s.id === id)) {
allSelectedIds.delete(id)
}
})
//
selection.forEach((item) => {
allSelectedIds.add(item.id)
})
// watch
isSyncing.value = true
props.formData.workerList = Array.from(allSelectedIds)
nextTick(() => {
isSyncing.value = false
})
}
//
const handleRemoveMember = (memberId) => {
if (!props.formData.workerList) {
props.formData.workerList = []
return
}
// watch
isSyncing.value = true
props.formData.workerList = props.formData.workerList.filter((id) => id !== memberId)
nextTick(() => {
//
syncTableSelection()
})
}
//
const handleClearAll = () => {
// watch
isSyncing.value = true
props.formData.workerList = []
if (personTableRef.value) {
personTableRef.value.clearSelection()
}
nextTick(() => {
isSyncing.value = false
})
}
//
watch([() => searchDept.value, () => searchName.value], () => {
//
//
if (
personList.value.length > 0 &&
props.formData.workerList &&
props.formData.workerList.length > 0
) {
// handleSelectionChange
isSyncing.value = true
//
nextTick(() => {
nextTick(() => {
syncTableSelection()
})
})
}
})
onMounted(() => {
getPersonList()
})
//
defineExpose({
validate: () => formRef.value.validate(),
resetFields: () => formRef.value.resetFields(),
clearValidate: () => formRef.value.clearValidate(),
})
</script>
<style lang="scss" scoped>
.add-and-edit-form {
padding: 10px 20px;
.member-select-item {
:deep(.el-form-item__content) {
width: 100%;
}
}
.member-select-container {
display: flex;
gap: 20px;
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
background: #fafafa;
}
.member-list-left {
flex: 1;
display: flex;
flex-direction: column;
.search-bar {
margin-bottom: 10px;
:deep(.el-row) {
width: 100%;
}
:deep(.el-col) {
padding: 0;
}
}
.table-container {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
background: #fff;
}
}
.member-list-right {
width: 200px;
display: flex;
flex-direction: column;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
.selected-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid #dcdfe6;
font-weight: 500;
}
.selected-list {
flex: 1;
padding: 10px;
overflow-y: auto;
max-height: 300px;
.selected-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 10px;
margin-bottom: 5px;
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #e1f3ff;
border-color: #409eff;
}
.remove-icon {
color: #909399;
cursor: pointer;
font-size: 16px;
&:hover {
color: #f56c6c;
}
}
}
.empty-tip {
text-align: center;
color: #909399;
padding: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,45 @@
import { reactive } from 'vue'
// 搜索表单配置
export const buildFormColumns = () => [
{
type: 'input',
prop: 'groupName',
placeholder: '请输入分组名称',
},
]
// 表格列配置
export const tableColumns = [
{
prop: 'groupName',
label: '分组名称',
},
{
prop: 'personCount',
label: '人员数量',
formatter: (row) => {
return row.workerList?.length || 0
},
},
{
prop: 'remark',
label: '备注',
},
]
// 弹窗配置
export const dialogConfig = reactive({
outerVisible: false,
outerTitle: '新增分组',
outerWidth: '70%',
minHeight: '80vh',
maxHeight: '95vh',
})
export default {
tableColumns,
dialogConfig,
buildFormColumns,
}

View File

@ -0,0 +1,177 @@
<template>
<div class="app-container">
<!-- 分组管理 -->
<ComTable
ref="comTableRef"
:form-columns="formColumns"
:table-columns="tableColumns"
:load-data="listGroupAPI"
:show-toolbar="true"
:show-action="true"
:action-columns="actionColumns"
>
<template #toolbar>
<ComButton
type="primary"
icon="Plus"
@click="onHandleAdd"
v-hasPermi="['group:group:add']"
>
新建分组
</ComButton>
</template>
</ComTable>
<ComDialog :dialog-config="dialogConfig" @closeDialogOuter="onCloseDialogOuter">
<template #outerContent>
<!-- 将表单抽离到独立组件中保持 index.vue 简洁 -->
<AddAndEditForm ref="formRef" :form-data="addAndEditForm" />
<el-row class="common-btn-row fixed-bottom">
<ComButton plain type="info" @click="onHandleCancel">取消</ComButton>
<ComButton @click="onHandleSave">保存</ComButton>
</el-row>
</template>
</ComDialog>
</div>
</template>
<script setup name="GroupManage">
import { ref, nextTick, getCurrentInstance, computed } from 'vue'
import {
listGroupAPI,
addGroupAPI,
updateGroupAPI,
delGroupAPI,
} from '@/api/basicManage/groupManage.js'
import { bus, BUS_EVENTS } from '@/utils/bus'
import config from './config'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
import ComDialog from '@/components/ComDialog/index.vue'
import AddAndEditForm from './addAndEditForm.vue'
const { tableColumns, dialogConfig, buildFormColumns } = config
const { proxy } = getCurrentInstance()
const formRef = ref(null)
const comTableRef = ref(null)
const editId = ref(null)
//
const formColumns = computed(() => buildFormColumns())
//
const getInitFormData = () => ({
groupName: '', //
workerList: [], // ID
remark: '', //
})
const addAndEditForm = ref(getInitFormData())
//
const actionColumns = [
{
label: '编辑',
type: 'primary',
link: true,
permission: ['group:group:edit'],
handler: (row) => {
editId.value = row.id
dialogConfig.outerTitle = '编辑分组'
dialogConfig.outerVisible = true
// 使 nextTick
nextTick(() => {
const { groupName, workerList, remark } = row
// workerListID
let workerIdList = []
if (workerList && Array.isArray(workerList) && workerList.length > 0) {
workerIdList = workerList.map((worker) => worker.id).filter((id) => id != null)
}
Object.assign(addAndEditForm.value, {
groupName: groupName || '',
workerList: workerIdList,
remark: remark || '',
})
})
},
},
{
label: '删除',
type: 'danger',
link: true,
permission: ['group:group:remove'],
handler: (row) => {
proxy.$modal.confirm('是否确认删除该分组?').then(async () => {
const result = await delGroupAPI({ id: row.id })
if (result.code === 200) {
proxy.$modal.msgSuccess('删除成功')
comTableRef.value?.refresh()
}
})
},
},
]
//
const onHandleAdd = () => {
editId.value = null
dialogConfig.outerTitle = '新增分组'
dialogConfig.outerVisible = true
addAndEditForm.value = getInitFormData()
nextTick(() => {
formRef.value?.clearValidate()
})
}
//
const onHandleCancel = () => {
dialogConfig.outerVisible = false
}
//
const onHandleSave = async () => {
try {
console.log(addAndEditForm.value)
// validate
await formRef.value.validate()
const API = editId.value ? updateGroupAPI : addGroupAPI
const params = JSON.parse(JSON.stringify(addAndEditForm.value))
params.workerList = params.workerList.map((item) => {
return { id: item }
})
if (editId.value) {
params.id = editId.value
}
const result = await API(params)
if (result.code === 200) {
proxy.$modal.msgSuccess(editId.value ? '编辑成功' : '新增成功')
dialogConfig.outerVisible = false
comTableRef.value?.refresh()
}
} catch (error) {
return Promise.reject(error)
}
}
//
const onCloseDialogOuter = (visible) => {
dialogConfig.outerVisible = visible
if (!visible) {
formRef.value?.resetFields()
}
}
</script>
<style lang="scss" scoped>
.common-btn-row {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding: 0 20px 20px;
}
</style>

View File

@ -7,40 +7,26 @@
:rules="rules"
class="add-and-edit-form"
>
<el-form-item label="人员所属部门" prop="inspectionStationId">
<!-- <el-select
filter
clearable
style="width: 100%"
v-model="formData.inspectionStationId"
placeholder="请选择人员所属部门"
>
<el-option
v-for="item in inspectionStationOptions"
:key="item.id"
:value="item.id"
:label="item.value"
/>
</el-select> -->
<el-form-item label="人员所属部门" prop="orgId">
<el-tree-select
clearable
check-strictly
value-key="id"
:data="enabledDeptOptions"
v-model="formData.inspectionStationId"
v-model="formData.orgId"
placeholder="请选择归属部门"
:props="{ value: 'id', label: 'label', children: 'children' }"
@change="handleDeptChange"
/>
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-form-item label="姓名" prop="workerName">
<el-input
clearable
maxlength="20"
show-word-limit
placeholder="请输入姓名"
v-model.trim="formData.name"
v-model.trim="formData.workerName"
/>
</el-form-item>
@ -66,35 +52,21 @@
<script setup name="AddAndEditForm">
import { ref, onMounted } from 'vue'
import { deptTreeSelect } from '@/api/system/user'
const props = defineProps({
formData: {
type: Object,
default: () => ({}),
},
inspectionStationOptions: {
type: Array,
default: () => [],
},
positionOptions: {
type: Array,
default: () => [],
},
natureOptions: {
type: Array,
default: () => [],
},
categoryOptions: {
type: Array,
default: () => [],
},
})
const deptOptions = ref(undefined)
const enabledDeptOptions = ref(undefined)
const formRef = ref(null)
const rules = {
inspectionStationId: [{ required: true, message: '请选择人员所属', trigger: 'change' }],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
orgId: [{ required: true, message: '请选择人员所属部门', trigger: 'change' }],
workerName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
phone: [
{
@ -103,9 +75,6 @@ const rules = {
trigger: 'blur',
},
],
positionId: [{ required: true, message: '请选择岗位', trigger: 'change' }],
personnelNatureId: [{ required: true, message: '请选择人员性质', trigger: 'change' }],
personnelClassificationId: [{ required: true, message: '请选择人员分类', trigger: 'change' }],
}
/** 查询部门下拉树结构 */
@ -129,6 +98,28 @@ const filterDisabledDept = (deptList) => {
})
}
/** 部门选择变化时,更新部门名称 */
const handleDeptChange = (value) => {
if (!value) {
props.formData.orgName = ''
return
}
//
const findDeptName = (list, id) => {
for (const item of list) {
if (item.id === id) {
return item.label
}
if (item.children && item.children.length) {
const found = findDeptName(item.children, id)
if (found) return found
}
}
return ''
}
props.formData.orgName = findDeptName(enabledDeptOptions.value, value)
}
onMounted(() => {
getDeptTree()
})

View File

@ -8,44 +8,44 @@ export const buildFormColumns = (
natureOptions = [],
categoryOptions = [],
) => [
{
type: 'input',
prop: 'name',
placeholder: '请输入姓名',
},
{
type: 'select',
prop: 'sex',
placeholder: '请选择性别',
options: [
{
label: '男',
value: 1,
},
{
label: '女',
value: 0,
},
],
},
{
type: 'select',
prop: 'positionId',
placeholder: '请选择部门',
options: positionOptions.map((item) => ({
label: item.value,
value: item.id,
})),
},
]
{
type: 'input',
prop: 'name',
placeholder: '请输入姓名',
},
{
type: 'select',
prop: 'sex',
placeholder: '请选择性别',
options: [
{
label: '男',
value: 1,
},
{
label: '女',
value: 0,
},
],
},
{
type: 'select',
prop: 'positionId',
placeholder: '请选择部门',
options: positionOptions.map((item) => ({
label: item.value,
value: item.id,
})),
},
]
export const tableColumns = [
{
prop: 'inspectionStationName',
prop: 'orgName',
label: '人员所属部门',
},
{
prop: 'name',
prop: 'workerName',
label: '姓名',
},
{
@ -56,9 +56,8 @@ export const tableColumns = [
{
prop: 'phone',
label: '电话',
formatter: (row) => (CryptoUtil.decrypt(row.phone)),
// formatter: (row) => (CryptoUtil.decrypt(row.phone)),
},
]
export const dialogConfig = reactive({

View File

@ -3,7 +3,7 @@
<!-- 人员管理 -->
<ComTable
ref="comTableRef"
:form-columns="formColumns"
:form-columns="[]"
:table-columns="tableColumns"
:load-data="listPersonAPI"
:show-toolbar="true"
@ -43,14 +43,7 @@
<ComDialog :dialog-config="dialogConfig" @closeDialogOuter="onCloseDialogOuter">
<template #outerContent>
<!-- 将表单抽离到独立组件中保持 index.vue 简洁 -->
<AddAndEditForm
ref="formRef"
:form-data="addAndEditForm"
:inspection-station-options="allPositionAndInspectionStationOptions"
:position-options="positionOptions"
:nature-options="natureOptions"
:category-options="categoryOptions"
/>
<AddAndEditForm ref="formRef" :form-data="addAndEditForm" />
<el-row class="common-btn-row">
<ComButton plain type="info" @click="onHandleCancel">取消</ComButton>
@ -110,11 +103,6 @@ import {
updatePersonAPI,
delPersonAPI,
} from '@/api/personManage/person.js'
import {
getInspectionStationSelectAPI,
getPersonNatureAndCategoryAndPositionSelectAPI,
} from '@/api/common.js'
import { useOptions } from '@/hooks/useOptions'
import { bus, BUS_EVENTS } from '@/utils/bus'
import { getToken } from '@/utils/auth'
import config from './config'
@ -124,7 +112,7 @@ import ComDialog from '@/components/ComDialog/index.vue'
import AddAndEditForm from './addAndEditForm.vue'
import CryptoUtil from '../../../api/crypto.js'
const { tableColumns, dialogConfig, buildFormColumns } = config
const { tableColumns, dialogConfig } = config
const { proxy } = getCurrentInstance()
const formRef = ref(null)
@ -132,13 +120,6 @@ const comTableRef = ref(null)
const editId = ref(null)
const uploadRef = ref(null)
// 使 Hook
// const { options: inspectionStationOptions } = useOptions(
// 'inspectionStationOptions',
// getInspectionStationSelectAPI,
// {},
// )
//
const uploadDialogConfig = reactive({
outerVisible: false,
@ -161,43 +142,13 @@ const upload = reactive({
selectedFile: null,
})
const { options: allPositionAndInspectionStationOptions } = useOptions(
'allPositionAndInspectionStationOptions',
getInspectionStationSelectAPI,
{},
)
const { options: positionOptions } = useOptions(
'positionOptions',
getPersonNatureAndCategoryAndPositionSelectAPI,
{ category: 2 },
)
const { options: natureOptions } = useOptions(
'personNatureOptions',
getPersonNatureAndCategoryAndPositionSelectAPI,
{ category: 1 },
)
const { options: categoryOptions } = useOptions(
'personCategoryOptions',
getPersonNatureAndCategoryAndPositionSelectAPI,
{ category: 0 },
)
//
const formColumns = computed(() =>
buildFormColumns(positionOptions.value, natureOptions.value, categoryOptions.value),
)
// 1.
//
const getInitFormData = () => ({
inspectionStationId: null, //
name: '', //
orgId: null, // id
orgName: '', //
workerName: '', //
sex: 1, // 1 0
phone: '', //
positionId: null, //
personnelNatureId: null, //
personnelClassificationId: null, //
longTermSecondment: 0, //
})
const addAndEditForm = ref(getInitFormData())
@ -214,25 +165,13 @@ const actionColumns = [
dialogConfig.outerVisible = true
// 2. 使 nextTick
nextTick(() => {
const {
inspectionStationId,
name,
phone,
sex,
positionId,
personnelNatureId,
personnelClassificationId,
longTermSecondment,
} = row
const { orgId, orgName, workerName, phone, sex } = row
Object.assign(addAndEditForm.value, {
inspectionStationId: inspectionStationId + '',
name,
phone: CryptoUtil.decrypt(phone),
sex: sex * 1,
positionId: positionId + '',
personnelNatureId: personnelNatureId + '',
personnelClassificationId: personnelClassificationId + '',
longTermSecondment,
orgId: orgId || null,
orgName: orgName || '',
workerName: workerName || '',
phone: phone ? CryptoUtil.decrypt(phone) : '',
sex: sex !== undefined ? sex * 1 : 1,
})
})
},