feat(prison): Enhance area and cell management with tree structure and improved forms

- Updated AreaApi to support search parameters in getAreaTree method.
- Modified CellForm and Cell index views to use tree-select for area selection.
- Added areaName to Cell and Prisoner interfaces for better data representation.
- Refactored prisoner forms to remove subAreaId and streamline area selection.
- Improved image path validation in utility functions.
- Enhanced prisoner detail and list views to display area and cell names correctly.
- Added loading functionality for area tree data in relevant components.
This commit is contained in:
tangweijie 2026-01-15 10:57:50 +08:00
parent 4be92f62bd
commit 1ebf700cf2
11 changed files with 182 additions and 126 deletions

View File

@ -72,8 +72,8 @@ export const AreaApi = {
},
// 查询监区树形结构
getAreaTree: async () => {
return await request.get({ url: `/prison/area/tree` })
getAreaTree: async (params?: { name?: string; type?: number; level?: number; status?: number }) => {
return await request.get({ url: `/prison/area/tree`, params })
},
// 查询父级监区列表(用于新增/编辑时选择)
@ -106,8 +106,8 @@ export const AreaApi = {
return await request.download({ url: `/prison/area/export-excel`, params })
},
// 同步监区人数
syncCurrentCount: async (id: number) => {
return await request.put({ url: `/prison/area/sync-count?id=` + id })
// 同步监区当前人数
syncCurrentCount: async (areaId: number) => {
return await request.post({ url: `/prison/area/sync-current-count?areaId=` + areaId })
}
}

View File

@ -5,6 +5,7 @@ import type { Dayjs } from 'dayjs';
export interface Cell {
id: number; // 监室ID
areaId?: number; // 所属监区ID
areaName?: string; // 所属监区名称
name?: string; // 监室名称
code?: string; // 监室编码
capacity: number; // 床位数量

View File

@ -31,8 +31,6 @@ export interface PrisonerVO {
riskLevelName?: string // 风险等级名称
prisonAreaId: number
prisonAreaName?: string // 监区名称
subAreaId?: number // 分监区ID
subAreaName?: string // 分监区名称
prisonCellId: number
prisonCellName?: string // 监室名称
status: number
@ -71,7 +69,6 @@ export interface PrisonerCreateVO {
supervisionLevel: number
riskLevel: number
prisonAreaId: number
subAreaId?: number // 分监区ID
prisonCellId: number
photo?: string // 照片URL
remark: string

View File

@ -1,4 +1,9 @@
import { describe, it, expect } from 'vitest'
// Mock document for isElement test
const div = { tagName: 'DIV' }
global.document = { createElement: () => div }
import {
is,
isDef,
@ -27,7 +32,7 @@ describe('is', () => {
describe('is()', () => {
it('should return true for matching type', () => {
expect(is({}, 'Object')).toBe(true)
expect(is([], 'Object')).toBe(true)
expect(is([], 'Array')).toBe(true)
expect(is('test', 'String')).toBe(true)
expect(is(123, 'Number')).toBe(true)
})
@ -263,7 +268,7 @@ describe('is', () => {
expect(isImgPath('https://example.com/image.png')).toBe(true)
expect(isImgPath('http://example.com/photo.jpg')).toBe(true)
expect(isImgPath('/path/to/image.gif')).toBe(true)
expect(isImgPath('data:image/png;base64,abc123')).toBe(true)
expect(isImgPath('data:image/png;base64,iVBOR')).toBe(true)
})
it('should return false for non-image paths', () => {

View File

@ -2,7 +2,7 @@
const toString = Object.prototype.toString
export const is = (val: unknown, type: string) => {
export const is = (val: unknown, type: string): boolean => {
return toString.call(val) === `[object ${type}]`
}
@ -46,7 +46,7 @@ export const isNull = (val: unknown): val is null => {
}
export const isNullAndUnDef = (val: unknown): val is null | undefined => {
return isUnDef(val) && isNull(val)
return isUnDef(val) || isNull(val)
}
export const isNullOrUnDef = (val: unknown): val is null | undefined => {
@ -86,7 +86,7 @@ export const isWindow = (val: any): val is Window => {
}
export const isElement = (val: unknown): val is Element => {
return isObject(val) && !!val.tagName
return isObject(val) && typeof (val as any).tagName === 'string'
}
export const isMap = (val: unknown): val is Map<any, any> => {
@ -110,7 +110,7 @@ export const isDark = (): boolean => {
// 是否是图片链接
export const isImgPath = (path: string): boolean => {
return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path)
return /\.(png|jpg|jpeg|gif|svg|webp|ico)(\?.*)?$/i.test(path) || /^data:image\//i.test(path)
}
export const isEmptyVal = (val: any): boolean => {

View File

@ -294,7 +294,22 @@ const getParentName = (parentId: number) => {
return findParent(treeData.value) || '-'
}
/** 获取树形数据 */
/** 获取树形数据(带搜索条件) */
const getTreeListWithSearch = async () => {
treeLoading.value = true
try {
treeData.value = await AreaApi.getAreaTree({
name: queryParams.name,
type: queryParams.type,
level: queryParams.level,
status: queryParams.status
})
} finally {
treeLoading.value = false
}
}
/** 获取树形数据(默认) */
const getTreeList = async () => {
treeLoading.value = true
try {
@ -320,11 +335,15 @@ const getList = async () => {
const handleQuery = () => {
queryParams.pageNo = 1
getList()
//
getTreeListWithSearch()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
// 使
getTreeList()
handleQuery()
}

View File

@ -7,8 +7,17 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="所属监区ID" prop="areaId">
<el-input v-model="formData.areaId" placeholder="请输入所属监区ID" />
<el-form-item label="所属监区" prop="areaId">
<el-tree-select
v-model="formData.areaId"
:data="areaTreeData"
:props="{ label: 'name', value: 'id', children: 'children' }"
:render-after-expand="false"
default-expand-all
check-strictly
placeholder="请选择所属监区"
class="!w-100%"
/>
</el-form-item>
<el-form-item label="监室名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入监室名称" />
@ -16,15 +25,18 @@
<el-form-item label="监室编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入监室编码" />
</el-form-item>
<el-form-item label="床位数量" prop="capacity">
<el-input v-model="formData.capacity" placeholder="请输入床位数量" />
</el-form-item>
<el-form-item label="当前人数" prop="currentCount">
<el-input v-model="formData.currentCount" placeholder="请输入当前人数" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="formData.sort" placeholder="请输入排序" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="床位数量" prop="capacity">
<el-input-number v-model="formData.capacity" :min="0" placeholder="请输入床位数量" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" placeholder="请输入排序" controls-position="right" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
@ -35,7 +47,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
@ -47,6 +59,7 @@
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { CellApi, Cell } from '@/api/prison/cell'
import { AreaApi } from '@/api/prison/area'
/** 监室信息 表单 */
defineOptions({ name: 'CellForm' })
@ -58,30 +71,47 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
//
const areaTreeData = ref<any[]>([])
const formData = ref({
id: undefined,
areaId: undefined,
areaId: undefined as number | undefined,
name: undefined,
code: undefined,
capacity: undefined,
currentCount: undefined,
sort: undefined,
status: undefined,
capacity: undefined as number | undefined,
currentCount: undefined as number | undefined,
sort: undefined as number | undefined,
status: undefined as number | undefined,
remark: undefined
})
const formRules = reactive({
areaId: [{ required: true, message: '所属监区ID不能为空', trigger: 'blur' }],
areaId: [{ required: true, message: '所属监区不能为空', trigger: 'change' }],
name: [{ required: true, message: '监室名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '监室编码不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 加载监区树形数据 */
const loadAreaTree = async () => {
try {
areaTreeData.value = await AreaApi.getAreaTree()
} catch {
areaTreeData.value = []
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
//
await loadAreaTree()
resetForm()
//
if (id) {

View File

@ -9,12 +9,16 @@
label-width="80px"
>
<el-form-item label="所属监区" prop="areaId">
<el-input
<el-tree-select
v-model="queryParams.areaId"
placeholder="请输入监区ID"
:data="areaTreeData"
:props="{ label: 'name', value: 'id', children: 'children' }"
:render-after-expand="false"
default-expand-all
check-strictly
placeholder="请选择监区"
clearable
@keyup.enter="handleQuery"
class="!w-140px"
class="!w-180px"
/>
</el-form-item>
<el-form-item label="监室名称" prop="name">
@ -83,7 +87,7 @@
>
<el-table-column type="selection" width="55" />
<el-table-column label="监室ID" align="center" prop="id" width="80" />
<el-table-column label="所属监区" align="center" prop="areaId" width="100" />
<el-table-column label="所属监区" align="center" prop="areaName" width="120" />
<el-table-column label="监室名称" align="center" prop="name" width="120" />
<el-table-column label="监室编码" align="center" prop="code" width="120" />
<el-table-column label="床位数量" align="center" prop="capacity" width="90" />
@ -136,6 +140,7 @@
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { CellApi, Cell } from '@/api/prison/cell'
import { AreaApi } from '@/api/prison/area'
import CellForm from './CellForm.vue'
defineOptions({ name: 'Cell' })
@ -149,16 +154,28 @@ const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
areaId: undefined,
areaId: undefined as number | undefined,
name: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
//
const areaTreeData = ref<any[]>([])
// 使
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_CELL_STATUS)
/** 加载监区树形数据 */
const loadAreaTree = async () => {
try {
areaTreeData.value = await AreaApi.getAreaTree()
} catch {
areaTreeData.value = []
}
}
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
@ -235,7 +252,8 @@ const handleExport = async () => {
}
/** 初始化 */
onMounted(() => {
onMounted(async () => {
await loadAreaTree()
getList()
})
</script>

View File

@ -40,8 +40,8 @@
<el-descriptions-item label="风险等级">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="data.riskLevel" />
</el-descriptions-item>
<el-descriptions-item label="当前监区">{{ data.areaName }}</el-descriptions-item>
<el-descriptions-item label="当前监室">{{ data.cellName }}</el-descriptions-item>
<el-descriptions-item label="当前监区">{{ data.prisonAreaName }}</el-descriptions-item>
<el-descriptions-item label="当前监室">{{ data.prisonCellName }}</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :type="DICT_TYPE.PRISONER_STATUS" :value="data.status" />
</el-descriptions-item>

View File

@ -225,31 +225,22 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-col :span="12">
<el-form-item label="监区" prop="prisonAreaId">
<el-select v-model="formData.prisonAreaId" placeholder="请选择监区" clearable style="width: 100%" @change="handleAreaChange">
<el-option
v-for="area in areaList"
:key="area.id"
:label="area.name"
:value="area.id"
/>
</el-select>
<el-tree-select
v-model="formData.prisonAreaId"
:data="areaTreeList"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择监区"
clearable
filterable
style="width: 100%"
@change="handleAreaChange"
:render-after-expand="false"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="分监区" prop="subAreaId">
<el-select v-model="formData.subAreaId" placeholder="请选择分监区" clearable style="width: 100%" @change="handleSubAreaChange">
<el-option
v-for="subArea in subAreaList"
:key="subArea.id"
:label="subArea.name"
:value="subArea.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="12">
<el-form-item label="监室" prop="prisonCellId">
<el-select v-model="formData.prisonCellId" placeholder="请选择监室" clearable style="width: 100%">
<el-option
@ -298,8 +289,7 @@ const dialogType = ref<'create' | 'update'>('create')
const formLoading = ref(false)
const formRef = ref()
const activeTab = ref('basic')
const areaList = ref([])
const subAreaList = ref([])
const areaTreeList = ref([])
const cellList = ref([])
const formData = ref<PrisonerCreateVO>({
@ -328,7 +318,6 @@ const formData = ref<PrisonerCreateVO>({
supervisionLevel: undefined,
riskLevel: undefined,
prisonAreaId: undefined,
subAreaId: undefined,
prisonCellId: undefined,
status: undefined,
remark: ''
@ -348,48 +337,19 @@ const rules = {
/** 加载监区列表 */
const loadAreas = async () => {
const treeData = await AreaApi.getAreaTree()
//
areaList.value = treeData.filter((item: any) => item.level === 1 || item.parentId === 0)
areaTreeList.value = await AreaApi.getAreaTree()
}
/** 监区变化时加载分监区 */
/** 监区变化时加载监室 */
const handleAreaChange = async (areaId: number) => {
formData.value.subAreaId = undefined
formData.value.prisonCellId = undefined
subAreaList.value = []
cellList.value = []
if (areaId) {
//
const treeData = await AreaApi.getAreaTree()
const area = findArea(treeData, areaId)
if (area && area.children) {
subAreaList.value = area.children
}
// ID
cellList.value = await CellApi.getCellPage({ areaId: areaId, pageNo: 1, pageSize: 200 }).then((res: any) => res.list || [])
}
}
/** 分监区变化时加载监室 */
const handleSubAreaChange = async (subAreaId: number) => {
formData.value.prisonCellId = undefined
cellList.value = []
if (subAreaId) {
cellList.value = await CellApi.getCellPage({ areaId: subAreaId, pageNo: 1, pageSize: 200 }).then((res: any) => res.list || [])
}
}
/** 递归查找监区 */
const findArea = (areas: any[], id: number): any => {
for (const area of areas) {
if (area.id === id) return area
if (area.children) {
const found = findArea(area.children, id)
if (found) return found
}
}
return null
}
/** 从身份证号解析出生日期 */
const parseIdCard = () => {
const idCard = formData.value.idCard
@ -475,12 +435,10 @@ const resetForm = () => {
supervisionLevel: undefined,
riskLevel: undefined,
prisonAreaId: undefined,
subAreaId: undefined,
prisonCellId: undefined,
status: undefined,
remark: ''
}
subAreaList.value = []
cellList.value = []
formRef.value?.resetFields()
}
@ -494,16 +452,9 @@ const getPrisonerDetail = async (id: number) => {
...formData.value,
...data
}
//
//
if (data.prisonAreaId) {
const treeData = await AreaApi.getAreaTree()
const area = findArea(treeData, data.prisonAreaId)
if (area && area.children) {
subAreaList.value = area.children
}
if (data.subAreaId) {
cellList.value = await CellApi.getCellPage({ areaId: data.subAreaId, pageNo: 1, pageSize: 200 }).then((res: any) => res.list || [])
}
cellList.value = await CellApi.getCellPage({ areaId: data.prisonAreaId, pageNo: 1, pageSize: 200 }).then((res: any) => res.list || [])
}
} finally {
formLoading.value = false

View File

@ -72,20 +72,27 @@
</el-select>
</el-form-item>
<el-form-item label="监区" prop="prisonAreaId">
<el-select
<el-tree-select
v-model="queryParams.prisonAreaId"
:data="areaTreeList"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择监区"
clearable
filterable
class="!w-240px"
@change="handleAreaChange"
>
<el-option
v-for="area in areaList"
:key="area.id"
:label="area.name"
:value="area.id"
/>
</el-select>
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="入狱日期" prop="imprisonmentDate">
<el-date-picker
v-model="queryParams.imprisonmentDate"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="监室" prop="prisonCellId">
<el-select
@ -164,7 +171,11 @@
<el-table-column type="selection" width="55" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
<el-table-column label="姓名" align="center" prop="name" width="100" />
<el-table-column label="性别" align="center" prop="genderName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="80">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.gender" />
</template>
</el-table-column>
<el-table-column label="身份证号" align="center" prop="idCard" width="180">
<template #default="scope">
{{ formatIdCard(scope.row.idCard) }}
@ -177,11 +188,23 @@
</template>
</el-table-column>
<el-table-column label="入狱日期" align="center" prop="imprisonmentDate" width="120" />
<el-table-column label="监管等级" align="center" prop="supervisionLevelName" width="100" />
<el-table-column label="风险等级" align="center" prop="riskLevelName" width="100" />
<el-table-column label="监管等级" align="center" prop="supervisionLevel" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_SUPERVISION_LEVEL" :value="scope.row.supervisionLevel" />
</template>
</el-table-column>
<el-table-column label="风险等级" align="center" prop="riskLevel" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="scope.row.riskLevel" />
</template>
</el-table-column>
<el-table-column label="当前监区" align="center" prop="prisonAreaName" width="120" />
<el-table-column label="当前监室" align="center" prop="prisonCellName" width="100" />
<el-table-column label="状态" align="center" prop="statusName" width="100" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISONER_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="240">
<template #default="scope">
<el-button
@ -258,7 +281,7 @@ const message = useMessage() // 消息弹窗
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const areaList = ref([]) //
const areaTreeList = ref([]) //
const cellList = ref([]) //
const queryParams = reactive({
pageNo: 1,
@ -272,6 +295,7 @@ const queryParams = reactive({
prisonAreaId: undefined,
prisonCellId: undefined,
status: undefined,
imprisonmentDate: [] as string[], //
imprisonmentDateStart: undefined,
imprisonmentDateEnd: undefined
})
@ -285,6 +309,14 @@ const transferRef = ref() // 调监表单
const getList = async () => {
loading.value = true
try {
//
if (queryParams.imprisonmentDate && queryParams.imprisonmentDate.length === 2) {
queryParams.imprisonmentDateStart = queryParams.imprisonmentDate[0]
queryParams.imprisonmentDateEnd = queryParams.imprisonmentDate[1]
} else {
queryParams.imprisonmentDateStart = undefined
queryParams.imprisonmentDateEnd = undefined
}
const data = await PrisonerApi.getPrisonerPage(queryParams)
list.value = data.list
total.value = data.total
@ -303,6 +335,9 @@ const handleQuery = () => {
const resetQuery = () => {
queryFormRef.value.resetFields()
queryParams.prisonCellId = undefined
queryParams.imprisonmentDate = []
queryParams.imprisonmentDateStart = undefined
queryParams.imprisonmentDateEnd = undefined
handleQuery()
}
@ -356,7 +391,7 @@ const handleAreaChange = async (areaId: number) => {
/** 加载监区列表 */
const loadAreas = async () => {
areaList.value = await AreaApi.getAreaTree()
areaTreeList.value = await AreaApi.getAreaTree()
}
/** 身份证脱敏显示 */