Initial commit - xlcp frontend

This commit is contained in:
tangweijie 2026-01-14 19:42:44 +08:00
parent d4cb996085
commit 4be92f62bd
25 changed files with 3504 additions and 331 deletions

1
public/china.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,21 +3,66 @@ import type { Dayjs } from 'dayjs';
/** 监区信息信息 */
export interface Area {
id: number; // 监区ID
name?: string; // 监区名称
code?: string; // 监区编码
type: number; // 监区类型1-普通监区 2-严管监区 3-医院 4-禁闭室
capacity: number; // 容纳人数
currentCount: number; // 当前人数
sort: number; // 排序
status?: number; // 状态1-启用 2-禁用
remark: string; // 备注
}
id: number // 监区ID
name?: string // 监区名称
code?: string // 监区编码
parentId?: number // 父级ID0表示顶级监区
level?: number // 级别1-监区(大队) 2-分监区(中队)
type: number // 监区类型1-普通监区 2-严管监区 3-集训监区 4-出监监区 5-医院监区 6-禁闭室
capacity: number // 容纳人数
currentCount: number // 当前人数
sort: number // 排序
status?: number // 状态1-启用 2-禁用
remark: string // 备注
children?: Area[] // 子监区列表(树形结构)
createTime?: Date // 创建时间
}
// 监区树形节点
export interface AreaNode {
id: number
name: string
code: string
parentId: number
level: number
type: number
capacity: number
currentCount: number
sort: number
status: number
remark: string
children?: AreaNode[]
}
// 监区信息创建/更新请求
export interface AreaSaveReqVO {
id?: number
name: string
code: string
parentId?: number
level?: number
type: number
capacity: number
sort?: number
status?: number
remark?: string
}
// 监区信息分页查询
export interface AreaPageReqVO {
pageNo: number
pageSize: number
name?: string
code?: string
type?: number
level?: number
status?: number
}
// 监区信息 API
export const AreaApi = {
// 查询监区信息分页
getAreaPage: async (params: any) => {
getAreaPage: async (params: AreaPageReqVO) => {
return await request.get({ url: `/prison/area/page`, params })
},
@ -26,13 +71,23 @@ export const AreaApi = {
return await request.get({ url: `/prison/area/get?id=` + id })
},
// 查询监区树形结构
getAreaTree: async () => {
return await request.get({ url: `/prison/area/tree` })
},
// 查询父级监区列表(用于新增/编辑时选择)
getParentAreas: async (level?: number) => {
return await request.get({ url: `/prison/area/parent-list`, params: { level } })
},
// 新增监区信息
createArea: async (data: Area) => {
createArea: async (data: AreaSaveReqVO) => {
return await request.post({ url: `/prison/area/create`, data })
},
// 修改监区信息
updateArea: async (data: Area) => {
updateArea: async (data: AreaSaveReqVO) => {
return await request.put({ url: `/prison/area/update`, data })
},
@ -49,5 +104,10 @@ export const AreaApi = {
// 导出监区信息 Excel
exportArea: async (params) => {
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 })
}
}

View File

@ -12,6 +12,7 @@ export interface Cell {
sort: number; // 排序
status?: number; // 状态1-启用 2-禁用
remark: string; // 备注
createTime?: Date; // 创建时间
}
// 监室信息 API

View File

@ -0,0 +1,36 @@
import request from '@/config/axios'
/** 看板统计响应 */
export interface DashboardStatisticsVO {
totalPrisoners: number // 在册罪犯总数
monthlyReleased: number // 本月释放人数
monthlyTransferred: number // 本月移交人数
hospitalCount: number // 当前就医人数
solitaryCount: number // 当前禁闭人数
ageDistribution: ChartDataVO[] // 年龄分布
sentenceDistribution: ChartDataVO[] // 刑期分布
educationDistribution: ChartDataVO[] // 文化程度分布
provinceDistribution: ProvinceChartVO[] // 省份分布
statisticsTime: string // 统计时间
}
/** 图表数据项 */
export interface ChartDataVO {
name: string // 分组名称
value: number // 数量
percentage?: number // 占比
}
/** 省份数据 */
export interface ProvinceChartVO {
province: string // 省份名称
provinceCode: number // 省份编码
count: number // 人数
}
export const DashboardApi = {
// 获取看板统计数据
getStatistics: async (): Promise<DashboardStatisticsVO> => {
return await request.get({ url: '/prison/dashboard/statistics' })
}
}

View File

@ -5,6 +5,51 @@ export interface PrisonerVO {
prisonerNo: string
name: string
gender: number
genderName?: string // 性别名称
birthday: string
idCard: string
ethnicity: string
nativePlace: string
education: number
educationName?: string // 文化程度名称
occupation: string
address: string
crime: string
sentenceYears: number
sentenceMonths: number
lifeImprisonment: number
deathSentenceReprieve: number
courtName: string
judgmentDate: string
judgmentNo: string
originalSentence: string
imprisonmentDate: string
releaseDate: string
supervisionLevel: number
supervisionLevelName?: string // 监管等级名称
riskLevel: number
riskLevelName?: string // 风险等级名称
prisonAreaId: number
prisonAreaName?: string // 监区名称
subAreaId?: number // 分监区ID
subAreaName?: string // 分监区名称
prisonCellId: number
prisonCellName?: string // 监室名称
status: number
statusName?: string // 状态名称
photo?: string // 照片URL
releaseType?: number // 释放类型
releaseReason?: string // 释放原因
remark: string
createTime: Date
}
// 服刑人员创建/更新请求
export interface PrisonerCreateVO {
id?: number
prisonerNo: string
name: string
gender: number
birthday: string
idCard: string
ethnicity: string
@ -15,15 +60,21 @@ export interface PrisonerVO {
crime: string
sentenceYears: number
sentenceMonths: number
lifeImprisonment: number
deathSentenceReprieve: number
courtName: string
judgmentDate: string
judgmentNo: string
originalSentence: string
imprisonmentDate: string
releaseDate: string
supervisionLevel: number
riskLevel: number
prisonAreaId: number
subAreaId?: number // 分监区ID
prisonCellId: number
status: number
photo?: string // 照片URL
remark: string
createTime: Date
}
// 服刑人员分页查询
@ -37,12 +88,12 @@ export const getPrisoner = (id: number) => {
}
// 新增服刑人员
export const createPrisoner = (data: PrisonerVO) => {
export const createPrisoner = (data: PrisonerCreateVO) => {
return request.post({ url: '/prison/prisoner/create', data })
}
// 修改服刑人员
export const updatePrisoner = (data: PrisonerVO) => {
export const updatePrisoner = (data: PrisonerCreateVO) => {
return request.put({ url: '/prison/prisoner/update', data })
}
@ -60,3 +111,61 @@ export const deletePrisonerList = (ids: number[]) => {
export const exportPrisoner = (params) => {
return request.download({ url: '/prison/prisoner/export-excel', params })
}
// 调监请求
export interface TransferReqVO {
prisonerId: number
targetCellId: number
reason?: string
}
// 执行调监
export const doTransfer = (data: TransferReqVO) => {
return request.post({ url: '/prison/prisoner/transfer', params: data })
}
// 罪犯位置历史记录
export interface PrisonerAreaLogVO {
id: number
prisonerId: number
prisonerNo: string
prisonerName: string
fromAreaId: number
fromAreaName: string
fromCellId: number
fromCellName: string
toAreaId: number
toAreaName: string
toCellId: number
toCellName: string
transferType: number
transferTypeName: string
reason: string
operatorId: number
operatorName: string
createTime: string
}
// 获取罪犯位置历史
export const getPrisonerAreaHistory = (prisonerId: number) => {
return request.get({ url: '/prison/prisoner-area-log/list-by-prisoner-id', params: { prisonerId } })
}
// 导入服刑人员
export const importPrisoner = (data: FormData) => {
return request.upload({ url: '/prison/prisoner/import-excel', data })
}
// PrisonerApi 对象 - 用于组件导入
export const PrisonerApi = {
getPage: getPrisonerPage,
get: getPrisoner,
create: createPrisoner,
update: updatePrisoner,
delete: deletePrisoner,
deleteList: deletePrisonerList,
export: exportPrisoner,
doTransfer: doTransfer,
getAreaHistory: getPrisonerAreaHistory,
import: importPrisoner
}

View File

@ -0,0 +1,81 @@
import request from '@/config/axios'
export interface Release {
id: number
prisonerId: number
prisonerNo: string
prisonerName: string
releaseType: number
releaseTypeName: string
releaseReason: string
courtName: string
judgmentNo: string
actualReleaseDate: string
handoverPerson: string
handoverUnit: string
certificateType: number
certificateNo: string
status: number
statusName: string
remark: string
operatorId: number
operatorName: string
createTime: string
}
export interface ReleasePageReqVO {
prisonerNo?: string
prisonerName?: string
releaseType?: number
status?: number
actualReleaseDateStart?: string
actualReleaseDateEnd?: string
pageNo: number
pageSize: number
}
export interface ReleaseSaveReqVO {
id?: number
prisonerId: number
releaseType: number
releaseReason?: string
courtName?: string
judgmentNo?: string
actualReleaseDate: string
handoverPerson?: string
handoverUnit?: string
certificateType?: number
certificateNo?: string
remark?: string
}
// 释放登记 API
export const ReleaseApi = {
getReleasePage: (params: ReleasePageReqVO) => {
return request.get({ url: '/prison/release/page', params })
},
getRelease: (id: number) => {
return request.get({ url: `/prison/release/get?id=${id}` })
},
createRelease: (data: ReleaseSaveReqVO) => {
return request.post({ url: '/prison/release/create', data })
},
updateRelease: (data: ReleaseSaveReqVO) => {
return request.put({ url: '/prison/release/update', data })
},
deleteRelease: (id: number) => {
return request.delete({ url: `/prison/release/delete?id=${id}` })
},
deleteReleaseList: (ids: number[]) => {
return request.delete({ url: `/prison/release/delete-list?ids=${ids.join(',')}` })
},
doRelease: (id: number) => {
return request.post({ url: `/prison/release/do-release?id=${id}` })
},
cancelRelease: (id: number) => {
return request.post({ url: `/prison/release/cancel-release?id=${id}` })
},
exportRelease: (params: ReleasePageReqVO) => {
return request.download({ url: '/prison/release/export-excel', params })
}
}

View File

@ -0,0 +1,66 @@
import request from '@/config/axios'
export interface ScoreDetail {
id: number
prisonerId: number
prisonerNo: string
recordDate: string
ruleId: number
ruleName: string
score: number
scoreType: number
scoreTypeName: string
remark: string
recorderId: number
recorderName: string
status: number
statusName: string
createTime: string
}
export interface ScoreDetailPageReqVO {
prisonerNo?: string
prisonerId?: number
recordDateStart?: string
recordDateEnd?: string
scoreType?: number
ruleId?: number
status?: number
pageNo: number
pageSize: number
}
export interface ScoreDetailSaveReqVO {
id?: number
prisonerId: number
recordDate: string
ruleId: number
score: number
scoreType: number
remark?: string
}
// 考核记录 API
export const ScoreDetailApi = {
getPage: (params: ScoreDetailPageReqVO) => {
return request.get({ url: '/prison/score-detail/page', params })
},
get: (id: number) => {
return request.get({ url: `/prison/score-detail/get?id=${id}` })
},
create: (data: ScoreDetailSaveReqVO) => {
return request.post({ url: '/prison/score-detail/create', data })
},
update: (data: ScoreDetailSaveReqVO) => {
return request.put({ url: '/prison/score-detail/update', data })
},
delete: (id: number) => {
return request.delete({ url: `/prison/score-detail/delete?id=${id}` })
},
deleteList: (ids: number[]) => {
return request.delete({ url: `/prison/score-detail/delete-list?ids=${ids.join(',')}` })
},
export: (params: ScoreDetailPageReqVO) => {
return request.download({ url: '/prison/score-detail/export-excel', params })
}
}

View File

@ -0,0 +1,67 @@
import request from '@/config/axios'
export interface ScoreRule {
id: number
category: number
categoryName: string
itemName: string
itemCode: string
score: number
maxDailyScore: number
maxMonthlyScore: number
description: string
status: number
statusName: string
sort: number
createTime: string
}
export interface ScoreRulePageReqVO {
category?: number
itemName?: string
itemCode?: string
status?: number
pageNo: number
pageSize: number
}
export interface ScoreRuleSaveReqVO {
id?: number
category: number
itemName: string
itemCode: string
score: number
maxDailyScore?: number
maxMonthlyScore?: number
description?: string
status?: number
sort?: number
}
// 考核规则 API
export const ScoreRuleApi = {
getRulePage: (params: ScoreRulePageReqVO) => {
return request.get({ url: '/prison/score-rule/page', params })
},
getRule: (id: number) => {
return request.get({ url: `/prison/score-rule/get?id=${id}` })
},
createRule: (data: ScoreRuleSaveReqVO) => {
return request.post({ url: '/prison/score-rule/create', data })
},
updateRule: (data: ScoreRuleSaveReqVO) => {
return request.put({ url: '/prison/score-rule/update', data })
},
deleteRule: (id: number) => {
return request.delete({ url: `/prison/score-rule/delete?id=${id}` })
},
deleteRuleList: (ids: number[]) => {
return request.delete({ url: `/prison/score-rule/delete-list?ids=${ids.join(',')}` })
},
getRuleByCategory: (category: number) => {
return request.get({ url: `/prison/score-rule/list-by-category?category=${category}` })
},
exportRule: (params: ScoreRulePageReqVO) => {
return request.download({ url: '/prison/score-rule/export-excel', params })
}
}

View File

@ -1,4 +1,5 @@
import * as echarts from 'echarts/core'
import type { EChartsType } from 'echarts/core'
import {
BarChart,
@ -48,4 +49,23 @@ echarts.use([
FunnelChart
])
// 导出 echarts 实例和类型
export default echarts
export type { EChartsType }
// 动态注册中国地图
let chinaMapRegistered = false
export const registerChinaMap = async () => {
if (chinaMapRegistered) return
try {
const response = await fetch('/china.json')
if (!response.ok) {
throw new Error('Failed to fetch China map data')
}
const chinaJson = await response.json()
echarts.registerMap('china', chinaJson)
chinaMapRegistered = true
} catch (error) {
console.error('加载中国地图数据失败:', error)
}
}

View File

@ -746,6 +746,30 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/iot/ota/firmware/detail/index.vue')
}
]
},
// 监管看板路由(开发测试用,上线后由后端菜单动态生成)
{
path: '/prison',
component: Layout,
name: 'Prison',
meta: {
hidden: true
},
children: [
{
path: 'dashboard',
component: () => import('@/views/prison/dashboard/index.vue'),
name: 'PrisonDashboard',
meta: {
title: '监管看板',
icon: 'ep:data-board',
permission: 'prison:dashboard:query',
noCache: false,
hidden: true,
canTo: true
}
}
]
}
]

View File

@ -0,0 +1,289 @@
import { describe, it, expect } from 'vitest'
import {
is,
isDef,
isUnDef,
isObject,
isEmpty,
isDate,
isNull,
isNullAndUnDef,
isNullOrUnDef,
isNumber,
isPromise,
isString,
isFunction,
isBoolean,
isRegExp,
isArray,
isElement,
isMap,
isUrl,
isImgPath,
isEmptyVal
} from '../is'
describe('is', () => {
describe('is()', () => {
it('should return true for matching type', () => {
expect(is({}, 'Object')).toBe(true)
expect(is([], 'Object')).toBe(true)
expect(is('test', 'String')).toBe(true)
expect(is(123, 'Number')).toBe(true)
})
it('should return false for non-matching type', () => {
expect(is('test', 'Object')).toBe(false)
expect(is(123, 'String')).toBe(false)
})
})
describe('isDef()', () => {
it('should return true for defined values', () => {
expect(isDef(0)).toBe(true)
expect(isDef('')).toBe(true)
expect(isDef(false)).toBe(true)
expect(isDef({})).toBe(true)
})
it('should return false for undefined', () => {
expect(isDef(undefined)).toBe(false)
})
})
describe('isUnDef()', () => {
it('should return true for undefined', () => {
expect(isUnDef(undefined)).toBe(true)
})
it('should return false for defined values', () => {
expect(isUnDef(null)).toBe(false)
expect(isUnDef(0)).toBe(false)
expect(isUnDef('')).toBe(false)
})
})
describe('isObject()', () => {
it('should return true for objects', () => {
expect(isObject({})).toBe(true)
expect(isObject({ a: 1 })).toBe(true)
})
it('should return false for null and non-objects', () => {
expect(isObject(null)).toBe(false)
expect(isObject([])).toBe(false)
expect(isObject('string')).toBe(false)
expect(isObject(123)).toBe(false)
})
})
describe('isEmpty()', () => {
it('should return true for empty values', () => {
expect(isEmpty(null)).toBe(true)
expect(isEmpty(undefined)).toBe(true)
expect(isEmpty('')).toBe(true)
expect(isEmpty([])).toBe(true)
expect(isEmpty({})).toBe(true)
})
it('should return false for non-empty values', () => {
expect(isEmpty('test')).toBe(false)
expect(isEmpty([1, 2, 3])).toBe(false)
expect(isEmpty({ a: 1 })).toBe(false)
expect(isEmpty(0)).toBe(false)
expect(isEmpty(false)).toBe(false)
})
})
describe('isDate()', () => {
it('should return true for Date objects', () => {
expect(isDate(new Date())).toBe(true)
})
it('should return false for non-date values', () => {
expect(isDate('2024-01-01')).toBe(false)
expect(isDate(123456)).toBe(false)
expect(isDate(null)).toBe(false)
})
})
describe('isNull()', () => {
it('should return true for null', () => {
expect(isNull(null)).toBe(true)
})
it('should return false for non-null values', () => {
expect(isNull(undefined)).toBe(false)
expect(isNull(0)).toBe(false)
expect(isNull('')).toBe(false)
expect(isNull({})).toBe(false)
})
})
describe('isNullAndUnDef()', () => {
it('should return true for null and undefined', () => {
expect(isNullAndUnDef(null)).toBe(true)
expect(isNullAndUnDef(undefined)).toBe(true)
})
it('should return false for other values', () => {
expect(isNullAndUnDef(0)).toBe(false)
expect(isNullAndUnDef('')).toBe(false)
expect(isNullAndUnDef({})).toBe(false)
})
})
describe('isNullOrUnDef()', () => {
it('should return true for null or undefined', () => {
expect(isNullOrUnDef(null)).toBe(true)
expect(isNullOrUnDef(undefined)).toBe(true)
})
it('should return false for defined values', () => {
expect(isNullOrUnDef(0)).toBe(false)
expect(isNullOrUnDef('')).toBe(false)
expect(isNullOrUnDef(false)).toBe(false)
})
})
describe('isNumber()', () => {
it('should return true for number type', () => {
expect(isNumber(123)).toBe(true)
expect(isNumber(0)).toBe(true)
expect(isNumber(-123)).toBe(true)
expect(isNumber(3.14)).toBe(true)
})
it('should return false for non-number values', () => {
expect(isNumber('123')).toBe(false)
expect(isNumber(true)).toBe(false)
expect(isNumber(null)).toBe(false)
})
})
describe('isString()', () => {
it('should return true for string type', () => {
expect(isString('test')).toBe(true)
expect(isString('')).toBe(true)
})
it('should return false for non-string values', () => {
expect(isString(123)).toBe(false)
expect(isString(true)).toBe(false)
})
})
describe('isFunction()', () => {
it('should return true for functions', () => {
expect(isFunction(function () {})).toBe(true)
expect(isFunction(() => {})).toBe(true)
})
it('should return false for non-functions', () => {
expect(isFunction('function')).toBe(false)
expect(isFunction({})).toBe(false)
})
})
describe('isBoolean()', () => {
it('should return true for boolean type', () => {
expect(isBoolean(true)).toBe(true)
expect(isBoolean(false)).toBe(true)
})
it('should return false for non-boolean values', () => {
expect(isBoolean(1)).toBe(false)
expect(isBoolean('true')).toBe(false)
})
})
describe('isRegExp()', () => {
it('should return true for RegExp', () => {
expect(isRegExp(/test/)).toBe(true)
expect(isRegExp(new RegExp('test'))).toBe(true)
})
it('should return false for non-RegExp', () => {
expect(isRegExp('test')).toBe(false)
expect(isRegExp({})).toBe(false)
})
})
describe('isArray()', () => {
it('should return true for arrays', () => {
expect(isArray([])).toBe(true)
expect(isArray([1, 2, 3])).toBe(true)
})
it('should return false for non-arrays', () => {
expect(isArray({})).toBe(false)
expect(isArray('array')).toBe(false)
})
})
describe('isElement()', () => {
it('should return true for DOM elements', () => {
const div = document.createElement('div')
expect(isElement(div)).toBe(true)
})
it('should return false for non-elements', () => {
expect(isElement({})).toBe(false)
expect(isElement('div')).toBe(false)
})
})
describe('isMap()', () => {
it('should return true for Map', () => {
expect(isMap(new Map())).toBe(true)
})
it('should return false for non-Map', () => {
expect(isMap({})).toBe(false)
expect(isMap([])).toBe(false)
})
})
describe('isUrl()', () => {
it('should return true for valid URLs', () => {
expect(isUrl('https://example.com')).toBe(true)
expect(isUrl('http://example.com/path')).toBe(true)
expect(isUrl('https://example.com/path?query=value')).toBe(true)
expect(isUrl('https://example.com/path#hash')).toBe(true)
})
it('should return false for invalid URLs', () => {
expect(isUrl('not-a-url')).toBe(false)
expect(isUrl('')).toBe(false)
})
})
describe('isImgPath()', () => {
it('should return true for image paths', () => {
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)
})
it('should return false for non-image paths', () => {
expect(isImgPath('https://example.com/page.html')).toBe(false)
expect(isImgPath('/path/to/file.pdf')).toBe(false)
})
})
describe('isEmptyVal()', () => {
it('should return true for empty values', () => {
expect(isEmptyVal('')).toBe(true)
expect(isEmptyVal(null)).toBe(true)
expect(isEmptyVal(undefined)).toBe(true)
})
it('should return false for non-empty values', () => {
expect(isEmptyVal('test')).toBe(false)
expect(isEmptyVal(0)).toBe(false)
expect(isEmptyVal(false)).toBe(false)
expect(isEmptyVal({})).toBe(false)
})
})
})

View File

@ -266,5 +266,12 @@ export enum DICT_TYPE {
PRISON_CELL_STATUS = 'prison_cell_status', // 监室状态
PRISON_RECORD_PASS_STATUS = 'prison_record_pass_status', // 问卷答题是否及格
PRISON_RECORD_STATUS = 'prison_record_status', // 问卷答题记录状态
PRISON_QUESTION_AUTO_FILL_SOURCE = 'prison_question_auto_fill_source' // 问卷问题自动填充来源
PRISON_QUESTION_AUTO_FILL_SOURCE = 'prison_question_auto_fill_source', // 问卷问题自动填充来源
PRISON_AREA_LEVEL = 'prison_area_level', // 监区级别1-监区(大队) 2-分监区(中队)
PRISON_RELEASE_TYPE = 'prison_release_type', // 释放类型1-刑满释放 2-假释 3-暂予监外执行 4-减刑 5-法院裁定释放 6-死亡 7-其他
PRISON_AREA_CHANGE_TYPE = 'prison_area_change_type', // 变动类型1-入监分配 2-调监 3-出监 4-移交转入 5-移交转出
PRISON_SCORE_CATEGORY = 'prison_score_category', // 考核类别1-劳动改造 2-教育改造 3-日常行为 4-卫生纪律 5-加分项 6-扣分项
PRISON_SCORE_TYPE = 'prison_score_type', // 考核类型1-加分 2-扣分
PRISON_RELEASE_STATUS = 'prison_release_status', // 释放状态1-待释放 2-已释放 3-已取消
PRISON_CERTIFICATE_TYPE = 'prison_certificate_type' // 证件类型1-身份证 2-户口簿 3-其他
}

View File

@ -1,5 +1,5 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<Dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
@ -13,6 +13,32 @@
<el-form-item label="监区编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入监区编码" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择级别" @change="handleLevelChange">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_AREA_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="父级监区" prop="parentId">
<el-select v-model="formData.parentId" placeholder="请选择父级监区" clearable :disabled="formData.level === 1">
<el-option
v-for="area in parentAreaOptions"
:key="area.id"
:label="area.name"
:value="area.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="监区类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择监区类型">
<el-option
@ -23,15 +49,18 @@
/>
</el-select>
</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
@ -42,7 +71,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>
@ -51,9 +80,10 @@
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { AreaApi, Area } from '@/api/prison/area'
import { AreaApi, AreaSaveReqVO } from '@/api/prison/area'
/** 监区信息 表单 */
defineOptions({ name: 'AreaForm' })
@ -65,35 +95,84 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
name: undefined,
code: undefined,
type: undefined,
capacity: undefined,
currentCount: undefined,
sort: undefined,
status: undefined,
parentId: undefined as number | undefined,
level: 1,
type: 1,
capacity: 0,
sort: 0,
status: 1,
remark: undefined
})
const formRules = reactive({
name: [{ required: true, message: '监区名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '监区编码不能为空', trigger: 'blur' }],
level: [{ required: true, message: '级别不能为空', trigger: 'blur' }],
type: [{ required: true, message: '监区类型不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
const parentAreaOptions = ref<{ id: number; name: string }[]>([]) //
/** 加载父级监区列表 */
const loadParentAreas = async () => {
// 1
try {
const data = await AreaApi.getParentAreas(1)
parentAreaOptions.value = data
} catch {
parentAreaOptions.value = []
}
}
/** 级别变化时处理 */
const handleLevelChange = (level: number) => {
//
if (formData.value.id && level === 2) {
parentAreaOptions.value = parentAreaOptions.value.filter(
(area) => area.id !== formData.value.id
)
}
//
if (level === 1) {
formData.value.parentId = undefined
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
//
await loadParentAreas()
resetForm()
//
if (id) {
formLoading.value = true
try {
formData.value = await AreaApi.getArea(id)
const data = await AreaApi.getArea(id)
formData.value = {
id: data.id,
name: data.name,
code: data.code,
parentId: data.parentId || undefined,
level: data.level || 1,
type: data.type,
capacity: data.capacity,
sort: data.sort,
status: data.status,
remark: data.remark
}
} finally {
formLoading.value = false
}
@ -109,7 +188,12 @@ const submitForm = async () => {
//
formLoading.value = true
try {
const data = formData.value as unknown as Area
const data = formData.value as unknown as AreaSaveReqVO
//
if (data.level === 2 && data.parentId === data.id) {
message.error('父级监区不能选择自己')
return
}
if (formType.value === 'create') {
await AreaApi.createArea(data)
message.success(t('common.createSuccess'))
@ -131,13 +215,14 @@ const resetForm = () => {
id: undefined,
name: undefined,
code: undefined,
type: undefined,
capacity: undefined,
currentCount: undefined,
sort: undefined,
status: undefined,
parentId: undefined,
level: 1,
type: 1,
capacity: 0,
sort: 0,
status: 1,
remark: undefined
}
formRef.value?.resetFields()
}
</script>
</script>

View File

@ -32,6 +32,21 @@
/>
</el-select>
</el-form-item>
<el-form-item label="级别" prop="level">
<el-select
v-model="queryParams.level"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in levelOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
@ -56,7 +71,7 @@
@click="openForm('create')"
v-hasPermi="['prison:area:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
<Icon icon="ep:plus" class="mr-5px" /> 新增监区
</el-button>
<el-button
type="success"
@ -67,86 +82,144 @@
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:area:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<!-- 树形监区列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleRowCheckboxChange"
>
<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="name" width="120" />
<el-table-column label="监区编码" align="center" prop="code" width="120" />
<el-table-column label="监区类型" align="center" prop="type" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_AREA_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="容纳人数" align="center" prop="capacity" width="90" />
<el-table-column label="当前人数" align="center" prop="currentCount" width="90" />
<el-table-column label="排序" align="center" prop="sort" width="70" />
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_CELL_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:area:update']"
<el-row :gutter="20">
<!-- 左侧树形结构 -->
<el-col :span="showDetail ? 14 : 24">
<el-card shadow="never" class="area-tree-card">
<template #header>
<div class="card-header">
<span><Icon icon="ep:grid" class="mr-5px" /> 监区树形结构</span>
<el-button
type="primary"
link
@click="handleRefreshTree"
>
<Icon icon="ep:refresh" class="mr-5px" /> 刷新
</el-button>
</div>
</template>
<el-tree
v-loading="treeLoading"
:data="treeData"
:props="treeProps"
node-key="id"
default-expand-all
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:area:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<template #default="{ data }">
<span class="area-tree-node">
<el-icon :class="getLevelIconClass(data.level)">
<component :is="getLevelIcon(data.level)" />
</el-icon>
<span class="node-name">{{ data.name }}</span>
<el-tag size="small" :type="data.currentCount >= data.capacity ? 'danger' : 'success'">
{{ data.currentCount }}/{{ data.capacity }}
</el-tag>
<el-tag
v-if="data.level === 1"
size="small"
type="info"
>
{{ getTypeLabel(data.type) }}
</el-tag>
</span>
</template>
</el-tree>
<el-empty v-if="!treeLoading && treeData.length === 0" description="暂无监区数据" />
</el-card>
</el-col>
<!-- 右侧详情面板 -->
<el-col v-if="showDetail" :span="10">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span><Icon icon="ep:info-filled" class="mr-5px" /> 监区详情</span>
<el-button type="primary" link @click="showDetail = false">
<Icon icon="ep:close" /> 关闭
</el-button>
</div>
</template>
<el-descriptions :column="1" border v-if="currentNode">
<el-descriptions-item label="监区名称">
{{ currentNode.name }}
</el-descriptions-item>
<el-descriptions-item label="监区编码">
{{ currentNode.code }}
</el-descriptions-item>
<el-descriptions-item label="级别">
<dict-tag :type="DICT_TYPE.PRISON_AREA_LEVEL" :value="currentNode.level" />
</el-descriptions-item>
<el-descriptions-item label="监区类型">
<dict-tag :type="DICT_TYPE.PRISON_AREA_TYPE" :value="currentNode.type" />
</el-descriptions-item>
<el-descriptions-item label="容纳人数">
{{ currentNode.capacity }}
</el-descriptions-item>
<el-descriptions-item label="当前人数">
<el-tag :type="currentNode.currentCount >= currentNode.capacity ? 'danger' : 'success'">
{{ currentNode.currentCount }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :type="DICT_TYPE.PRISON_CELL_STATUS" :value="currentNode.status" />
</el-descriptions-item>
<el-descriptions-item v-if="currentNode.parentId && currentNode.parentId > 0" label="父级监区">
{{ getParentName(currentNode.parentId) }}
</el-descriptions-item>
<el-descriptions-item v-if="currentNode.remark" label="备注">
{{ currentNode.remark }}
</el-descriptions-item>
</el-descriptions>
<div class="detail-actions" v-if="currentNode">
<el-button
type="primary"
@click="openForm('update', currentNode.id)"
v-hasPermi="['prison:area:update']"
>
<Icon icon="ep:edit" class="mr-5px" /> 修改
</el-button>
<el-button
type="warning"
@click="handleSyncCount(currentNode.id)"
v-hasPermi="['prison:area:update']"
>
<Icon icon="ep:refresh" class="mr-5px" /> 同步人数
</el-button>
<el-button
type="danger"
@click="handleDelete(currentNode.id)"
v-hasPermi="['prison:area:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</div>
</el-card>
</el-col>
</el-row>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AreaForm ref="formRef" @success="getList" />
<AreaForm ref="formRef" @success="handleRefreshTree" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { AreaApi, Area } from '@/api/prison/area'
import { AreaApi, AreaNode } from '@/api/prison/area'
import AreaForm from './AreaForm.vue'
import { OfficeBuilding, Files } from '@element-plus/icons-vue'
defineOptions({ name: 'Area' })
@ -161,21 +234,76 @@ const queryParams = reactive({
pageSize: 10,
name: undefined,
type: undefined,
level: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
//
const treeLoading = ref(false)
const treeData = ref<AreaNode[]>([])
const currentNode = ref<AreaNode | null>(null)
const showDetail = ref(false)
// 使
const typeOptions = getIntDictOptions(DICT_TYPE.PRISON_AREA_TYPE)
const levelOptions = getIntDictOptions(DICT_TYPE.PRISON_AREA_LEVEL)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_CELL_STATUS)
//
const treeProps = {
label: 'name',
children: 'children'
}
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 获取级别图标 */
const getLevelIcon = (level: number) => {
return level === 1 ? OfficeBuilding : Files
}
/** 获取级别图标样式 */
const getLevelIconClass = (level: number) => {
return level === 1 ? 'area-level-icon area-level-main' : 'area-level-icon area-level-sub'
}
/** 获取类型标签 */
const getTypeLabel = (type: number) => {
const dict = typeOptions.find((d: any) => d.value === type)
return dict ? dict.label : '-'
}
/** 获取父级监区名称 */
const getParentName = (parentId: number) => {
const findParent = (nodes: AreaNode[]): string => {
for (const node of nodes) {
if (node.id === parentId) return node.name
if (node.children && node.children.length > 0) {
const found = findParent(node.children)
if (found) return found
}
}
return ''
}
return findParent(treeData.value) || '-'
}
/** 获取树形数据 */
const getTreeList = async () => {
treeLoading.value = true
try {
treeData.value = await AreaApi.getAreaTree()
} finally {
treeLoading.value = false
}
}
/** 查询列表 */
const getList = async () => {
loading.value = true
@ -206,29 +334,38 @@ const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 树节点点击 */
const handleNodeClick = (data: AreaNode) => {
currentNode.value = data
showDetail.value = true
}
/** 刷新树形数据 */
const handleRefreshTree = () => {
getTreeList()
showDetail.value = false
currentNode.value = null
}
/** 同步人数 */
const handleSyncCount = async (id: number) => {
try {
await message.confirm('确认要同步该监区的人数吗?')
await AreaApi.syncCurrentCount(id)
message.success('人数同步成功')
await getTreeList()
} catch {}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await AreaApi.deleteArea(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: Area[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await AreaApi.deleteAreaList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
await getList()
await getTreeList()
showDetail.value = false
currentNode.value = null
} catch {}
}
@ -247,6 +384,55 @@ const handleExport = async () => {
/** 初始化 */
onMounted(() => {
getTreeList()
getList()
})
</script>
<style lang="scss" scoped>
.area-tree-card {
:deep(.el-card__body) {
max-height: 600px;
overflow-y: auto;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
}
.area-tree-node {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
padding-right: 8px;
.area-level-icon {
font-size: 16px;
&.area-level-main {
color: var(--el-color-primary);
}
&.area-level-sub {
color: var(--el-color-success);
}
}
.node-name {
flex: 1;
font-size: 14px;
}
}
.detail-actions {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: center;
}
</style>

View File

@ -0,0 +1,239 @@
<template>
<div class="china-map-container">
<div ref="chartRef" class="chart-container"></div>
<el-empty v-if="!loading && data.length === 0" description="暂无省份分布数据" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, onUnmounted } from 'vue'
import type { EChartsOption } from 'echarts'
import echarts from '@/plugins/echarts'
import { registerChinaMap } from '@/plugins/echarts'
import { ProvinceChartVO } from '@/api/prison/dashboard'
import { debounce } from 'lodash-es'
defineOptions({ name: 'ChinaMap' })
interface Props {
data: ProvinceChartVO[]
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
loading: false
})
const chartRef = ref<HTMLDivElement>()
let chartInstance: echarts.ECharts | null = null
// 2 6 adcode
const provinceCodeMap: Record<string, string> = {
// 2
'11': '北京市',
'12': '天津市',
'13': '河北省',
'14': '山西省',
'15': '内蒙古自治区',
'21': '辽宁省',
'22': '吉林省',
'23': '黑龙江省',
'31': '上海市',
'32': '江苏省',
'33': '浙江省',
'34': '安徽省',
'35': '福建省',
'36': '江西省',
'37': '山东省',
'41': '河南省',
'42': '湖北省',
'43': '湖南省',
'44': '广东省',
'45': '广西壮族自治区',
'46': '海南省',
'50': '重庆市',
'51': '四川省',
'52': '贵州省',
'53': '云南省',
'54': '西藏自治区',
'61': '陕西省',
'62': '甘肃省',
'63': '青海省',
'64': '宁夏回族自治区',
'65': '新疆维吾尔自治区',
'71': '台湾省',
'81': '香港特别行政区',
'82': '澳门特别行政区',
// 6 adcode
'110000': '北京市',
'120000': '天津市',
'130000': '河北省',
'140000': '山西省',
'150000': '内蒙古自治区',
'210000': '辽宁省',
'220000': '吉林省',
'230000': '黑龙江省',
'310000': '上海市',
'320000': '江苏省',
'330000': '浙江省',
'340000': '安徽省',
'350000': '福建省',
'360000': '江西省',
'370000': '山东省',
'410000': '河南省',
'420000': '湖北省',
'430000': '湖南省',
'440000': '广东省',
'450000': '广西壮族自治区',
'460000': '海南省',
'500000': '重庆市',
'510000': '四川省',
'520000': '贵州省',
'530000': '云南省',
'540000': '西藏自治区',
'610000': '陕西省',
'620000': '甘肃省',
'630000': '青海省',
'640000': '宁夏回族自治区',
'650000': '新疆维吾尔自治区',
'710000': '台湾省',
'810000': '香港特别行政区',
'820000': '澳门特别行政区'
}
//
const getProvinceName = (code: string | number): string => {
const codeStr = String(code)
// 6 adcode
if (provinceCodeMap[codeStr]) {
return provinceCodeMap[codeStr]
}
// 2
const shortCode = codeStr.padStart(2, '0')
return provinceCodeMap[shortCode] || codeStr
}
// ECharts
const formatMapData = (data: ProvinceChartVO[]) => {
return data.map((item) => ({
name: getProvinceName(item.provinceCode),
value: item.count
}))
}
//
const getMaxValue = (data: ProvinceChartVO[]): number => {
if (data.length === 0) return 100
return Math.max(...data.map((item) => item.count), 100)
}
//
const updateChart = () => {
if (!chartInstance || !chartRef.value) return
const mapData = formatMapData(props.data)
const maxValue = getMaxValue(props.data)
const option: EChartsOption = {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
if (params.value === undefined) {
return `${params.name}: 暂无数据`
}
return `${params.name}: ${params.value}`
}
},
visualMap: {
min: 0,
max: maxValue,
left: 'left',
top: 'bottom',
text: ['高', '低'],
calculable: true,
inRange: {
color: ['#e0f3f8', '#91c7ae', '#c23531']
}
},
series: [
{
name: '籍贯分布',
type: 'map',
map: 'china',
roam: true,
zoom: 1.2,
scaleLimit: {
min: 0.5,
max: 3
},
label: {
show: true,
fontSize: 10,
color: '#333'
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
},
itemStyle: {
areaColor: '#ffd700'
}
},
data: mapData
}
]
}
chartInstance.setOption(option)
}
//
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
updateChart()
}
//
watch(
() => props.data,
() => {
updateChart()
},
{ deep: true }
)
//
const resizeHandler = debounce(() => {
chartInstance?.resize()
}, 100)
onMounted(async () => {
//
await registerChinaMap()
//
initChart()
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
chartInstance?.dispose()
})
</script>
<style lang="scss" scoped>
.china-map-container {
width: 100%;
height: 500px;
position: relative;
.chart-container {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-info">
<div class="stat-value" v-loading="loading">
<el-skeleton v-if="loading" animated :rows="0" />
<template v-else>{{ formatNumber(value) }}</template>
</div>
<div class="stat-title">{{ title }}</div>
</div>
<div class="stat-icon" :class="iconClass">
<Icon :icon="icon" :size="36" />
</div>
</div>
</el-card>
</template>
<script lang="ts" setup>
interface Props {
title: string
value: number
icon: string
iconClass?: string
loading?: boolean
}
defineOptions({ name: 'StatCard' })
const props = defineProps<Props>()
const formatNumber = (num: number) => {
return new Intl.NumberFormat('zh-CN').format(num)
}
</script>
<style lang="scss" scoped>
.stat-card {
height: 100%;
:deep(.el-card__body) {
height: 100%;
}
.stat-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: #303133;
}
.stat-title {
font-size: 14px;
color: #909399;
margin-top: 8px;
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
&.primary {
background: #ecf5ff;
color: #409eff;
}
&.success {
background: #f0f9eb;
color: #67c23a;
}
&.warning {
background: #fdf6ec;
color: #e6a23c;
}
&.danger {
background: #fef0f0;
color: #f56c6c;
}
}
}
</style>

View File

@ -0,0 +1,295 @@
<template>
<div class="dashboard-container">
<!-- 核心指标卡片 -->
<el-row :gutter="16" class="mb-16px">
<el-col :xs="24" :sm="12" :md="8" :lg="24 / 5" v-for="item in statCards" :key="item.title">
<StatCard v-bind="item" :loading="loading" :value="item.value" />
</el-col>
</el-row>
<!-- 饼图区域 -->
<el-row :gutter="16" class="mb-16px">
<el-col :xs="24" :sm="24" :md="8">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span><Icon icon="ep:pie-chart" class="mr-5px" />年龄分布</span>
</div>
</template>
<EChart v-if="!loading" :options="ageChartOptions" :height="'350px'" />
<div v-else class="chart-loading">
<el-skeleton animated :rows="6" />
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="8">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span><Icon icon="ep:pie-chart" class="mr-5px" />刑期分布</span>
</div>
</template>
<EChart v-if="!loading" :options="sentenceChartOptions" :height="'350px'" />
<div v-else class="chart-loading">
<el-skeleton animated :rows="6" />
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="8">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span><Icon icon="ep:pie-chart" class="mr-5px" />文化程度</span>
</div>
</template>
<EChart v-if="!loading" :options="educationChartOptions" :height="'350px'" />
<div v-else class="chart-loading">
<el-skeleton animated :rows="6" />
</div>
</el-card>
</el-col>
</el-row>
<!-- 地图区域 -->
<el-row :gutter="16">
<el-col :span="24">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span><Icon icon="ep:location" class="mr-5px" />籍贯分布</span>
<span v-if="statisticsTime" class="statistics-time">
统计时间{{ formatDate(statisticsTime) }}
</span>
</div>
</template>
<ChinaMap v-if="!loading" :data="provinceData" :loading="loading" />
<div v-else class="chart-loading">
<el-skeleton animated :rows="10" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import type { EChartsOption } from 'echarts'
import { StatCard } from './components'
import { DashboardApi, type DashboardStatisticsVO } from '@/api/prison/dashboard'
import EChart from '@/components/Echart/src/Echart.vue'
import ChinaMap from './components/ChinaMap.vue'
defineOptions({ name: 'PrisonDashboard' })
const loading = ref(true)
const data = ref<DashboardStatisticsVO | null>(null)
const statCards = computed(() => [
{
title: '在册罪犯',
value: data.value?.totalPrisoners || 0,
icon: 'ep:user',
iconClass: 'primary'
},
{
title: '本月释放',
value: data.value?.monthlyReleased || 0,
icon: 'ep:select',
iconClass: 'success'
},
{
title: '本月移交',
value: data.value?.monthlyTransferred || 0,
icon: 'ep:rank-list',
iconClass: 'warning'
},
{
title: '当前就医',
value: data.value?.hospitalCount || 0,
icon: 'ep:first-aid-kit',
iconClass: 'danger'
},
{
title: '当前禁闭',
value: data.value?.solitaryCount || 0,
icon: 'ep:lock',
iconClass: 'danger'
}
])
const ageChartOptions = computed<EChartsOption>(() => ({
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle'
},
series: [
{
type: 'pie',
radius: ['45%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 6,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data.value?.ageDistribution || []
}
]
}))
const sentenceChartOptions = computed<EChartsOption>(() => ({
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle'
},
series: [
{
type: 'pie',
radius: ['45%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 6,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data.value?.sentenceDistribution || []
}
]
}))
const educationChartOptions = computed<EChartsOption>(() => ({
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle'
},
series: [
{
type: 'pie',
radius: ['45%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 6,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data.value?.educationDistribution || []
}
]
}))
const provinceData = computed(() => data.value?.provinceDistribution || [])
const statisticsTime = computed(() => data.value?.statisticsTime)
const formatDate = (dateStr: string) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
const fetchData = async () => {
loading.value = true
try {
data.value = await DashboardApi.getStatistics()
} catch (error) {
console.error('获取看板数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchData()
})
</script>
<style lang="scss" scoped>
.dashboard-container {
padding: 16px;
}
.mb-16px {
margin-bottom: 16px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
.statistics-time {
font-size: 12px;
font-weight: normal;
color: #909399;
}
}
.chart-loading {
height: 350px;
padding: 20px;
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<el-drawer v-model="dialogVisible" title="罪犯详情" size="600px">
<div v-loading="loading">
<!-- 基本信息 -->
<el-descriptions title="基本信息" :column="2" border>
<el-descriptions-item label="罪犯编号">{{ data.prisonerNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ data.name }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="data.gender" />
</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ data.idCard }}</el-descriptions-item>
<el-descriptions-item label="出生日期">{{ data.birthday }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ data.ethnicity }}</el-descriptions-item>
<el-descriptions-item label="籍贯" :span="2">{{ data.nativePlace }}</el-descriptions-item>
<el-descriptions-item label="文化程度">
<dict-tag :type="DICT_TYPE.PRISON_EDUCATION" :value="data.education" />
</el-descriptions-item>
<el-descriptions-item label="职业">{{ data.occupation }}</el-descriptions-item>
<el-descriptions-item label="家庭住址" :span="2">{{ data.address }}</el-descriptions-item>
</el-descriptions>
<!-- 刑罚信息 -->
<el-descriptions title="刑罚信息" :column="2" border style="margin-top: 20px">
<el-descriptions-item label="罪名">{{ data.crime }}</el-descriptions-item>
<el-descriptions-item label="刑期">
{{ formatSentence(data) }}
</el-descriptions-item>
<el-descriptions-item label="判决法院">{{ data.courtName }}</el-descriptions-item>
<el-descriptions-item label="判决日期">{{ data.judgmentDate }}</el-descriptions-item>
<el-descriptions-item label="判决书编号" :span="2">{{ data.judgmentNo }}</el-descriptions-item>
<el-descriptions-item label="入狱日期">{{ data.imprisonmentDate }}</el-descriptions-item>
<el-descriptions-item label="释放日期">{{ data.releaseDate }}</el-descriptions-item>
</el-descriptions>
<!-- 监管信息 -->
<el-descriptions title="监管信息" :column="2" border style="margin-top: 20px">
<el-descriptions-item label="监管等级">
<dict-tag :type="DICT_TYPE.PRISON_SUPERVISION_LEVEL" :value="data.supervisionLevel" />
</el-descriptions-item>
<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="状态">
<dict-tag :type="DICT_TYPE.PRISONER_STATUS" :value="data.status" />
</el-descriptions-item>
<el-descriptions-item label="备注">{{ data.remark }}</el-descriptions-item>
</el-descriptions>
<!-- 位置历史 -->
<div style="margin-top: 20px">
<h4>位置变更历史</h4>
<el-timeline v-if="areaHistory.length > 0">
<el-timeline-item
v-for="(log, index) in areaHistory"
:key="index"
:timestamp="log.createTime"
placement="top"
>
<el-card>
<p><strong></strong>{{ log.fromAreaName || '-' }} / {{ log.fromCellName || '-' }}</p>
<p><strong></strong>{{ log.toAreaName || '-' }} / {{ log.toCellName || '-' }}</p>
<p><strong>原因</strong>{{ log.reason || '-' }}</p>
<p><strong>操作人</strong>{{ log.operatorName }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无位置变更记录" />
</div>
</div>
</el-drawer>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import * as PrisonerApi from '@/api/prison/prisoner'
defineOptions({ name: 'PrisonPrisonerDetail' })
const dialogVisible = ref(false)
const loading = ref(false)
const data = ref<any>({})
const areaHistory = ref([])
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
loading.value = true
try {
//
data.value = await PrisonerApi.getPrisoner(id)
//
areaHistory.value = await PrisonerApi.getPrisonerAreaHistory(id)
} finally {
loading.value = false
}
}
/** 刑期显示格式化 */
const formatSentence = (row: any) => {
if (row.lifeImprisonment === 1) {
return '无期'
}
if (row.deathSentenceReprieve === 1) {
return '死缓'
}
const years = row.sentenceYears || 0
const months = row.sentenceMonths || 0
let result = ''
if (years > 0) result += `${years}`
if (months > 0) result += `${months}`
return result || '-'
}
defineExpose({ open })
</script>

View File

@ -1,8 +1,8 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'create' ? '新增服刑人员' : '修改服刑人员'"
width="900px"
:title="dialogType === 'create' ? '入监登记' : '修改罪犯信息'"
width="1000px"
:close-on-click-modal="false"
>
<el-form
@ -12,181 +12,265 @@
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="服刑人员编号" prop="prisonerNo">
<el-input v-model="formData.prisonerNo" placeholder="请输入服刑人员编号" :disabled="dialogType === 'update'" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="性别" prop="gender">
<el-select v-model="formData.gender" placeholder="请选择性别" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="formData.idCard" placeholder="请输入身份证号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="formData.birthday"
type="date"
placeholder="请选择出生日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="民族" prop="ethnicity">
<el-input v-model="formData.ethnicity" placeholder="请输入民族" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="籍贯" prop="nativePlace">
<el-input v-model="formData.nativePlace" placeholder="请输入籍贯" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="文化程度" prop="education">
<el-select v-model="formData.education" placeholder="请选择文化程度" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_EDUCATION)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="职业" prop="occupation">
<el-input v-model="formData.occupation" placeholder="请输入职业" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="家庭住址" prop="address">
<el-input v-model="formData.address" placeholder="请输入家庭住址" />
</el-form-item>
</el-col>
</el-row>
<el-divider border-style="dashed">刑罚信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="罪名" prop="crime">
<el-input v-model="formData.crime" placeholder="请输入罪名" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="刑期(年)" prop="sentenceYears">
<el-input-number v-model="formData.sentenceYears" :min="0" :max="100" placeholder="请输入" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="刑期(月)" prop="sentenceMonths">
<el-input-number v-model="formData.sentenceMonths" :min="0" :max="11" placeholder="请输入" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="入狱日期" prop="imprisonmentDate">
<el-date-picker
v-model="formData.imprisonmentDate"
type="date"
placeholder="请选择入狱日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="释放日期" prop="releaseDate">
<el-date-picker
v-model="formData.releaseDate"
type="date"
placeholder="请选择释放日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="监管等级" prop="supervisionLevel">
<el-select v-model="formData.supervisionLevel" placeholder="请选择监管等级" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SUPERVISION_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="风险等级" prop="riskLevel">
<el-select v-model="formData.riskLevel" placeholder="请选择风险等级" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="监区ID" prop="prisonAreaId">
<el-input-number v-model="formData.prisonAreaId" :min="0" placeholder="请输入监区ID" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="监室ID" prop="prisonCellId">
<el-input-number v-model="formData.prisonCellId" :min="0" placeholder="请输入监室ID" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISONER_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
<el-tabs v-model="activeTab">
<el-tab-pane label="基础信息" name="basic">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input v-model="formData.prisonerNo" placeholder="请输入罪犯编号" :disabled="dialogType === 'update'" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="性别" prop="gender">
<el-select v-model="formData.gender" placeholder="请选择性别" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="formData.idCard" placeholder="请输入身份证号" @blur="parseIdCard" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="formData.birthday"
type="date"
placeholder="请选择出生日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="民族" prop="ethnicity">
<el-input v-model="formData.ethnicity" placeholder="请输入民族" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="籍贯" prop="nativePlace">
<el-input v-model="formData.nativePlace" placeholder="请输入籍贯" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="文化程度" prop="education">
<el-select v-model="formData.education" placeholder="请选择文化程度" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_EDUCATION)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="职业" prop="occupation">
<el-input v-model="formData.occupation" placeholder="请输入职业" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="家庭住址" prop="address">
<el-input v-model="formData.address" placeholder="请输入家庭住址" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="刑罚信息" name="sentence">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="罪名" prop="crime">
<el-input v-model="formData.crime" placeholder="请输入罪名" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="刑期(年)" prop="sentenceYears">
<el-input-number v-model="formData.sentenceYears" :min="0" :max="100" placeholder="请输入" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="刑期(月)" prop="sentenceMonths">
<el-input-number v-model="formData.sentenceMonths" :min="0" :max="11" placeholder="请输入" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="是否无期" prop="lifeImprisonment">
<el-select v-model="formData.lifeImprisonment" placeholder="请选择" clearable style="width: 100%">
<el-option :key="0" :label="'否'" :value="0" />
<el-option :key="1" :label="'是'" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否死缓" prop="deathSentenceReprieve">
<el-select v-model="formData.deathSentenceReprieve" placeholder="请选择" clearable style="width: 100%">
<el-option :key="0" :label="'否'" :value="0" />
<el-option :key="1" :label="'是'" :value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="原判刑期" prop="originalSentence">
<el-input v-model="formData.originalSentence" placeholder="如有期徒刑10年" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="判决法院" prop="courtName">
<el-input v-model="formData.courtName" placeholder="请输入判决法院" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="判决日期" prop="judgmentDate">
<el-date-picker
v-model="formData.judgmentDate"
type="date"
placeholder="请选择判决日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="判决书编号" prop="judgmentNo">
<el-input v-model="formData.judgmentNo" placeholder="如:(2023)沪01刑初123号" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="入狱日期" prop="imprisonmentDate">
<el-date-picker
v-model="formData.imprisonmentDate"
type="date"
placeholder="请选择入狱日期"
value-format="YYYY-MM-DD"
style="width: 100%"
@change="calculateReleaseDate"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="释放日期" prop="releaseDate">
<el-date-picker
v-model="formData.releaseDate"
type="date"
placeholder="请选择释放日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="监管信息" name="supervision">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="监管等级" prop="supervisionLevel">
<el-select v-model="formData.supervisionLevel" placeholder="请选择监管等级" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SUPERVISION_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="风险等级" prop="riskLevel">
<el-select v-model="formData.riskLevel" placeholder="请选择风险等级" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISONER_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<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-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-form-item label="监室" prop="prisonCellId">
<el-select v-model="formData.prisonCellId" placeholder="请选择监室" clearable style="width: 100%">
<el-option
v-for="cell in cellList"
:key="cell.id"
:label="cell.name"
:value="cell.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@ -197,18 +281,28 @@
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { PrisonerCreateVO } from '@/api/prison/prisoner'
import * as PrisonerApi from '@/api/prison/prisoner'
import { AreaApi } from '@/api/prison/area'
import { CellApi } from '@/api/prison/cell'
defineOptions({ name: 'PrisonPrisonerForm' })
const emit = defineEmits(['success'])
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogType = ref('create')
const dialogType = ref<'create' | 'update'>('create')
const formLoading = ref(false)
const formRef = ref()
const formData = ref({
const activeTab = ref('basic')
const areaList = ref([])
const subAreaList = ref([])
const cellList = ref([])
const formData = ref<PrisonerCreateVO>({
id: undefined,
prisonerNo: '',
name: '',
@ -223,18 +317,25 @@ const formData = ref({
crime: '',
sentenceYears: 0,
sentenceMonths: 0,
lifeImprisonment: undefined,
deathSentenceReprieve: undefined,
courtName: '',
judgmentDate: '',
judgmentNo: '',
originalSentence: '',
imprisonmentDate: '',
releaseDate: '',
supervisionLevel: undefined,
riskLevel: undefined,
prisonAreaId: undefined,
subAreaId: undefined,
prisonCellId: undefined,
status: undefined,
remark: ''
})
const rules = {
prisonerNo: [{ required: true, message: '请输入服刑人员编号', trigger: 'blur' }],
prisonerNo: [{ required: true, message: '请输入罪犯编号', trigger: 'blur' }],
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
gender: [{ required: true, message: '请选择性别', trigger: 'change' }],
idCard: [{ required: true, message: '请输入身份证号', trigger: 'blur' }],
@ -245,16 +346,107 @@ const rules = {
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
/** 加载监区列表 */
const loadAreas = async () => {
const treeData = await AreaApi.getAreaTree()
//
areaList.value = treeData.filter((item: any) => item.level === 1 || item.parentId === 0)
}
/** 监区变化时加载分监区 */
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
}
}
}
/** 分监区变化时加载监室 */
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
if (idCard && idCard.length === 18) {
const birthday = idCard.substring(6, 14)
const formattedBirthday = `${birthday.substring(0, 4)}-${birthday.substring(4, 6)}-${birthday.substring(6, 8)}`
formData.value.birthday = formattedBirthday
}
}
/** 计算释放日期 */
const calculateReleaseDate = () => {
if (!formData.value.imprisonmentDate) return
const imprisonmentDate = new Date(formData.value.imprisonmentDate)
let releaseDate = new Date(imprisonmentDate)
//
if (formData.value.lifeImprisonment === 1 || formData.value.deathSentenceReprieve === 1) {
return
}
//
const years = formData.value.sentenceYears || 0
const months = formData.value.sentenceMonths || 0
releaseDate.setFullYear(releaseDate.getFullYear() + years)
releaseDate.setMonth(releaseDate.getMonth() + months)
formData.value.releaseDate = releaseDate.toISOString().split('T')[0]
}
/** 打开弹窗 */
const open = (type: string, id?: number) => {
dialogType.value = type
dialogVisible.value = true
activeTab.value = 'basic'
resetForm()
loadAreas()
if (id) {
getPrisonerDetail(id)
} else {
//
generatePrisonerNo()
}
}
/** 自动生成罪犯编号 */
const generatePrisonerNo = async () => {
try {
const date = new Date()
const prefix = `Z${date.getFullYear().toString().slice(2)}`
//
formData.value.prisonerNo = prefix + Math.random().toString().slice(2, 8)
} catch {}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
@ -272,15 +464,24 @@ const resetForm = () => {
crime: '',
sentenceYears: 0,
sentenceMonths: 0,
lifeImprisonment: undefined,
deathSentenceReprieve: undefined,
courtName: '',
judgmentDate: '',
judgmentNo: '',
originalSentence: '',
imprisonmentDate: '',
releaseDate: '',
supervisionLevel: undefined,
riskLevel: undefined,
prisonAreaId: undefined,
subAreaId: undefined,
prisonCellId: undefined,
status: undefined,
remark: ''
}
subAreaList.value = []
cellList.value = []
formRef.value?.resetFields()
}
@ -289,7 +490,21 @@ const getPrisonerDetail = async (id: number) => {
formLoading.value = true
try {
const data = await PrisonerApi.getPrisoner(id)
formData.value = data
formData.value = {
...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 || [])
}
}
} finally {
formLoading.value = false
}
@ -303,7 +518,7 @@ const submitForm = async () => {
try {
if (dialogType.value === 'create') {
await PrisonerApi.createPrisoner(formData.value)
message.success('新增成功')
message.success('入监登记成功')
} else {
await PrisonerApi.updatePrisoner(formData.value)
message.success('修改成功')

View File

@ -0,0 +1,201 @@
<template>
<el-dialog
v-model="dialogVisible"
title="调监操作"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px" v-loading="loading">
<!-- 罪犯信息展示 -->
<el-form-item label="罪犯信息">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="罪犯编号">{{ prisoner.prisonerNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ prisoner.name }}</el-descriptions-item>
<el-descriptions-item label="当前监区">{{ prisoner.areaName }}</el-descriptions-item>
<el-descriptions-item label="当前监室">{{ prisoner.cellName }}</el-descriptions-item>
</el-descriptions>
</el-form-item>
<el-form-item label="目标监区" prop="targetAreaId">
<el-select
v-model="formData.targetAreaId"
placeholder="请选择目标监区"
clearable
style="width: 100%"
@change="handleTargetAreaChange"
>
<el-option
v-for="area in areaList"
:key="area.id"
:label="area.name"
:value="area.id"
/>
</el-select>
</el-form-item>
<el-form-item label="目标分监区" prop="targetSubAreaId">
<el-select
v-model="formData.targetSubAreaId"
placeholder="请选择目标分监区"
clearable
style="width: 100%"
@change="handleTargetSubAreaChange"
>
<el-option
v-for="subArea in subAreaList"
:key="subArea.id"
:label="subArea.name"
:value="subArea.id"
/>
</el-select>
</el-form-item>
<el-form-item label="目标监室" prop="targetCellId">
<el-select
v-model="formData.targetCellId"
placeholder="请选择目标监室"
clearable
style="width: 100%"
>
<el-option
v-for="cell in cellList"
:key="cell.id"
:label="`${cell.name} (${cell.currentCount}/${cell.capacity})`"
:value="cell.id"
:disabled="cell.currentCount >= cell.capacity"
/>
</el-select>
</el-form-item>
<el-form-item label="调监原因" prop="reason">
<el-input v-model="formData.reason" type="textarea" :rows="3" placeholder="请输入调监原因" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import * as PrisonerApi from '@/api/prison/prisoner'
import { AreaApi } from '@/api/prison/area'
import { CellApi } from '@/api/prison/cell'
defineOptions({ name: 'PrisonTransferForm' })
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const loading = ref(false)
const formRef = ref()
const prisoner = ref<any>({})
const areaList = ref([])
const subAreaList = ref([])
const cellList = ref([])
const formData = reactive({
prisonerId: undefined as number | undefined,
targetAreaId: undefined as number | undefined,
targetSubAreaId: undefined as number | undefined,
targetCellId: undefined as number | undefined,
reason: ''
})
const rules = {
targetAreaId: [{ required: true, message: '请选择目标监区', trigger: 'change' }],
targetSubAreaId: [{ required: true, message: '请选择目标分监区', trigger: 'change' }],
targetCellId: [{ required: true, message: '请选择目标监室', trigger: 'change' }]
}
/** 打开弹窗 */
const open = async (prisonerId: number) => {
dialogVisible.value = true
loading.value = true
resetForm()
try {
//
prisoner.value = await PrisonerApi.getPrisoner(prisonerId)
formData.prisonerId = prisonerId
//
const treeData = await AreaApi.getAreaTree()
areaList.value = treeData.filter((item: any) => item.level === 1 || item.parentId === 0)
} finally {
loading.value = false
}
}
/** 目标监区变化 */
const handleTargetAreaChange = async (areaId: number) => {
formData.targetSubAreaId = undefined
formData.targetCellId = 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
}
}
}
/** 目标分监区变化 */
const handleTargetSubAreaChange = async (subAreaId: number) => {
formData.targetCellId = 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 resetForm = () => {
formData.prisonerId = undefined
formData.targetAreaId = undefined
formData.targetSubAreaId = undefined
formData.targetCellId = undefined
formData.reason = ''
prisoner.value = {}
subAreaList.value = []
cellList.value = []
formRef.value?.resetFields()
}
/** 提交 */
const handleSubmit = async () => {
const valid = await formRef.value?.validate()
if (!valid) return
loading.value = true
try {
await PrisonerApi.doTransfer({
prisonerId: formData.prisonerId!,
targetCellId: formData.targetCellId!,
reason: formData.reason
})
ElMessage.success('调监成功')
dialogVisible.value = false
emit('success')
} catch {
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>

View File

@ -8,10 +8,10 @@
:inline="true"
label-width="100px"
>
<el-form-item label="服刑人员编号" prop="prisonerNo">
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input
v-model="queryParams.prisonerNo"
placeholder="请输入服刑人员编号"
placeholder="请输入罪犯编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
@ -71,6 +71,37 @@
/>
</el-select>
</el-form-item>
<el-form-item label="监区" prop="prisonAreaId">
<el-select
v-model="queryParams.prisonAreaId"
placeholder="请选择监区"
clearable
class="!w-240px"
@change="handleAreaChange"
>
<el-option
v-for="area in areaList"
:key="area.id"
:label="area.name"
:value="area.id"
/>
</el-select>
</el-form-item>
<el-form-item label="监室" prop="prisonCellId">
<el-select
v-model="queryParams.prisonCellId"
placeholder="请选择监室"
clearable
class="!w-240px"
>
<el-option
v-for="cell in cellList"
:key="cell.id"
:label="cell.name"
:value="cell.id"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
@ -95,7 +126,15 @@
@click="openForm('create')"
v-hasPermi="['prison:prisoner:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
<Icon icon="ep:plus" class="mr-5px" /> 入监登记
</el-button>
<el-button
type="warning"
plain
@click="openTransferForm"
v-hasPermi="['prison:prisoner:transfer']"
>
<Icon icon="ep:rank" class="mr-5px" /> 调监
</el-button>
<el-button
type="danger"
@ -123,33 +162,36 @@
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="服刑人员编号" align="center" prop="prisonerNo" width="120" />
<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="gender" width="80">
<el-table-column label="性别" align="center" prop="genderName" width="80" />
<el-table-column label="身份证号" align="center" prop="idCard" width="180">
<template #default="scope">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.gender" />
{{ formatIdCard(scope.row.idCard) }}
</template>
</el-table-column>
<el-table-column label="身份证号" align="center" prop="idCard" width="180" />
<el-table-column label="罪名" align="center" prop="crime" width="150" />
<el-table-column label="监管等级" align="center" prop="supervisionLevel" width="100">
<el-table-column label="刑期" align="center" width="120">
<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" />
{{ formatSentence(scope.row) }}
</template>
</el-table-column>
<el-table-column label="入狱日期" align="center" prop="imprisonmentDate" width="120" />
<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="200">
<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="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" width="240">
<template #default="scope">
<el-button
type="primary"
link
@click="handleDetail(scope.row)"
v-hasPermi="['prison:prisoner:query']"
>
详情
</el-button>
<el-button
type="primary"
link
@ -158,6 +200,14 @@
>
修改
</el-button>
<el-button
type="warning"
link
@click="handleTransfer(scope.row)"
v-hasPermi="['prison:prisoner:transfer']"
>
调监
</el-button>
<el-button
type="danger"
link
@ -180,14 +230,25 @@
<!-- 表单弹窗添加/修改 -->
<PrisonerForm ref="formRef" @success="getList" />
<!-- 详情弹窗 -->
<PrisonerDetail ref="detailRef" />
<!-- 调监弹窗 -->
<TransferForm ref="transferRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { checkPermi } from '@/utils/permission'
import PrisonerForm from './PrisonerForm.vue'
import PrisonerDetail from './PrisonerDetail.vue'
import TransferForm from './TransferForm.vue'
import download from '@/utils/download'
import * as PrisonerApi from '@/api/prison/prisoner'
import { AreaApi } from '@/api/prison/area'
import { CellApi } from '@/api/prison/cell'
import { ref, reactive, onMounted } from 'vue'
defineOptions({ name: 'PrisonPrisoner' })
@ -197,6 +258,8 @@ const message = useMessage() // 消息弹窗
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const areaList = ref([]) //
const cellList = ref([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@ -214,6 +277,9 @@ const queryParams = reactive({
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
const formRef = ref() //
const detailRef = ref() //
const transferRef = ref() //
/** 查询列表 */
const getList = async () => {
@ -236,6 +302,7 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
queryParams.prisonCellId = undefined
handleQuery()
}
@ -247,7 +314,7 @@ const handleExport = async () => {
//
exportLoading.value = true
const data = await PrisonerApi.exportPrisoner(queryParams)
download.excel(data, '服刑人员信息.xls')
download.excel(data, '罪犯信息.xls')
} catch {
} finally {
exportLoading.value = false
@ -255,11 +322,65 @@ const handleExport = async () => {
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 查看详情 */
const handleDetail = (row: any) => {
detailRef.value.open(row.id)
}
/** 打开调监弹窗 */
const openTransferForm = () => {
if (checkedIds.value.length !== 1) {
message.warning('请先选择一个罪犯进行调监')
return
}
transferRef.value.open(checkedIds.value[0])
}
/** 调监操作 */
const handleTransfer = (row: any) => {
transferRef.value.open(row.id)
}
/** 监区变化时加载监室列表 */
const handleAreaChange = async (areaId: number) => {
queryParams.prisonCellId = undefined
cellList.value = []
if (areaId) {
cellList.value = await CellApi.getCellPage({ areaId, pageNo: 1, pageSize: 200 }).then((res: any) => res.list || [])
}
}
/** 加载监区列表 */
const loadAreas = async () => {
areaList.value = await AreaApi.getAreaTree()
}
/** 身份证脱敏显示 */
const formatIdCard = (idCard: string) => {
if (!idCard || idCard.length < 8) return idCard
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2')
}
/** 刑期显示格式化 */
const formatSentence = (row: any) => {
if (row.lifeImprisonment === 1) {
return '无期'
}
if (row.deathSentenceReprieve === 1) {
return '死缓'
}
const years = row.sentenceYears || 0
const months = row.sentenceMonths || 0
let result = ''
if (years > 0) result += `${years}`
if (months > 0) result += `${months}`
return result || '-'
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
@ -275,7 +396,7 @@ const handleDelete = async (id: number) => {
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: PrisonerApi.PrisonerVO[]) => {
const handleRowCheckboxChange = (rows: any[]) => {
checkedIds.value = rows.map((row) => row.id)
}
@ -295,5 +416,6 @@ const handleDeleteBatch = async () => {
/** 初始化 **/
onMounted(() => {
getList()
loadAreas()
})
</script>

View File

@ -0,0 +1,224 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="isDetail ? '释放登记详情' : (formData.id ? '编辑释放登记' : '新增释放登记')"
width="600px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
<el-form-item label="罪犯" prop="prisonerId">
<el-select
v-model="formData.prisonerId"
placeholder="请选择罪犯"
filterable
remote
:remote-method="searchPrisoner"
:loading="prisonerLoading"
@change="handlePrisonerChange"
style="width: 100%"
>
<el-option
v-for="item in prisonerList"
:key="item.id"
:label="`${item.prisonerNo} - ${item.name}`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="罪犯编号">
<el-input v-model="formData.prisonerNo" disabled />
</el-form-item>
<el-form-item label="罪犯姓名">
<el-input v-model="formData.prisonerName" disabled />
</el-form-item>
<el-form-item label="释放类型" prop="releaseType">
<el-select v-model="formData.releaseType" placeholder="请选择释放类型" style="width: 100%">
<el-option label="刑满释放" :value="1" />
<el-option label="假释" :value="2" />
<el-option label="暂予监外执行" :value="3" />
<el-option label="减刑" :value="4" />
<el-option label="法院裁定释放" :value="5" />
<el-option label="死亡" :value="6" />
<el-option label="其他" :value="7" />
</el-select>
</el-form-item>
<el-form-item label="释放原因" prop="releaseReason">
<el-input v-model="formData.releaseReason" type="textarea" :rows="2" placeholder="请输入释放原因" />
</el-form-item>
<el-form-item label="实际释放日期" prop="actualReleaseDate">
<el-date-picker
v-model="formData.actualReleaseDate"
type="date"
placeholder="请选择释放日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<template v-if="formData.releaseType === 2 || formData.releaseType === 4">
<el-form-item label="裁定法院">
<el-input v-model="formData.courtName" placeholder="请输入裁定法院" />
</el-form-item>
<el-form-item label="裁定书编号">
<el-input v-model="formData.judgmentNo" placeholder="请输入裁定书编号" />
</el-form-item>
</template>
<template v-if="formData.releaseType === 5">
<el-form-item label="裁定法院">
<el-input v-model="formData.courtName" placeholder="请输入裁定法院" />
</el-form-item>
</template>
<el-form-item label="交接人">
<el-input v-model="formData.handoverPerson" placeholder="请输入交接人" />
</el-form-item>
<el-form-item label="交接单位">
<el-input v-model="formData.handoverUnit" placeholder="请输入交接单位" />
</el-form-item>
<el-form-item label="证件类型">
<el-select v-model="formData.certificateType" placeholder="请选择证件类型" style="width: 100%">
<el-option label="身份证" :value="1" />
<el-option label="户口簿" :value="2" />
<el-option label="其他" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="证件号码">
<el-input v-model="formData.certificateNo" placeholder="请输入证件号码" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button v-if="!isDetail" type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { ReleaseApi } from '@/api/prison/release'
import { PrisonerApi } from '@/api/prison/prisoner'
defineOptions({ name: 'PrisonReleaseForm' })
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const isDetail = ref(false)
const submitLoading = ref(false)
const prisonerLoading = ref(false)
const prisonerList = ref<any[]>([])
const formData = reactive({
id: undefined as number | undefined,
prisonerId: undefined as number | undefined,
prisonerNo: '',
prisonerName: '',
releaseType: undefined as number | undefined,
releaseReason: '',
courtName: '',
judgmentNo: '',
actualReleaseDate: '',
handoverPerson: '',
handoverUnit: '',
certificateType: undefined as number | undefined,
certificateNo: '',
remark: ''
})
const rules = {
prisonerId: [{ required: true, message: '请选择罪犯', trigger: 'change' }],
releaseType: [{ required: true, message: '请选择释放类型', trigger: 'change' }],
actualReleaseDate: [{ required: true, message: '请选择释放日期', trigger: 'change' }]
}
/** 打开弹窗 */
const open = (id?: number, detail = false) => {
dialogVisible.value = true
isDetail.value = detail
resetForm()
if (id) {
loadData(id)
}
}
/** 重置表单 */
const resetForm = () => {
formData.id = undefined
formData.prisonerId = undefined
formData.prisonerNo = ''
formData.prisonerName = ''
formData.releaseType = undefined
formData.releaseReason = ''
formData.courtName = ''
formData.judgmentNo = ''
formData.actualReleaseDate = ''
formData.handoverPerson = ''
formData.handoverUnit = ''
formData.certificateType = undefined
formData.certificateNo = ''
formData.remark = ''
prisonerList.value = []
}
/** 加载数据 */
const loadData = async (id: number) => {
const data = await ReleaseApi.getRelease(id)
Object.assign(formData, data)
//
prisonerList.value = [{
id: data.prisonerId,
prisonerNo: data.prisonerNo,
name: data.prisonerName
}]
}
/** 搜索罪犯 */
const searchPrisoner = async (keyword: string) => {
if (!keyword) return
prisonerLoading.value = true
try {
const list = await PrisonerApi.getPrisonerPage({
prisonerNo: keyword,
name: keyword,
pageNo: 1,
pageSize: 10
})
prisonerList.value = list.list || []
} finally {
prisonerLoading.value = false
}
}
/** 罪犯选择变化 */
const handlePrisonerChange = (val: number) => {
const prisoner = prisonerList.value.find((item: any) => item.id === val)
if (prisoner) {
formData.prisonerNo = prisoner.prisonerNo
formData.prisonerName = prisoner.name
}
}
/** 提交 */
const handleSubmit = async () => {
submitLoading.value = true
try {
if (formData.id) {
await ReleaseApi.updateRelease(formData)
ElMessage.success('更新成功')
} else {
await ReleaseApi.createRelease(formData)
ElMessage.success('创建成功')
}
dialogVisible.value = false
emit('success')
} finally {
submitLoading.value = false
}
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,302 @@
<template>
<div class="release-page">
<!-- 搜索表单 -->
<el-card shadow="never" class="search-card">
<el-form ref="searchFormRef" :model="searchForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input v-model="searchForm.prisonerNo" placeholder="请输入罪犯编号" clearable />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input v-model="searchForm.prisonerName" placeholder="请输入罪犯姓名" clearable />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="释放类型" prop="releaseType">
<el-select v-model="searchForm.releaseType" placeholder="请选择" clearable>
<el-option label="刑满释放" :value="1" />
<el-option label="假释" :value="2" />
<el-option label="暂予监外执行" :value="3" />
<el-option label="减刑" :value="4" />
<el-option label="法院裁定释放" :value="5" />
<el-option label="死亡" :value="6" />
<el-option label="其他" :value="7" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
<el-option label="待释放" :value="1" />
<el-option label="已释放" :value="2" />
<el-option label="已取消" :value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="释放日期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
@change="handleDateChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<el-button type="primary" @click="handleSearch">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="handleReset">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>释放登记列表</span>
<el-button type="primary" @click="handleCreate" v-hasPermi="'prison:release:create'">
<Icon icon="ep:plus" class="mr-5px" /> 新增释放登记
</el-button>
</div>
</template>
<el-table v-loading="loading" :data="tableData" border stripe>
<el-table-column prop="prisonerNo" label="罪犯编号" width="120" />
<el-table-column prop="prisonerName" label="罪犯姓名" width="100" />
<el-table-column prop="releaseTypeName" label="释放类型" width="120" />
<el-table-column prop="releaseReason" label="释放原因" show-overflow-tooltip />
<el-table-column prop="actualReleaseDate" label="实际释放日期" width="120" />
<el-table-column prop="handoverPerson" label="交接人" width="100" />
<el-table-column prop="handoverUnit" label="交接单位" show-overflow-tooltip />
<el-table-column prop="statusName" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ row.statusName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" width="100" />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="'prison:release:query'">
详情
</el-button>
<el-button
link
type="primary"
@click="handleEdit(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:update'"
>
编辑
</el-button>
<el-button
link
type="success"
@click="handleDoRelease(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:update'"
>
执行释放
</el-button>
<el-button
link
type="warning"
@click="handleCancel(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:update'"
>
取消
</el-button>
<el-button
link
type="danger"
@click="handleDelete(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:delete'"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="searchForm.pageNo"
v-model:limit="searchForm.pageSize"
@pagination="getPage"
/>
</el-card>
<!-- 释放登记表单弹窗 -->
<ReleaseForm ref="formRef" @success="getPage" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import ReleaseForm from './ReleaseForm.vue'
import { ReleaseApi, type ReleasePageReqVO } from '@/api/prison/release'
defineOptions({ name: 'PrisonRelease' })
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const dateRange = ref([])
const searchForm = reactive<ReleasePageReqVO>({
prisonerNo: '',
prisonerName: '',
releaseType: undefined,
status: undefined,
actualReleaseDateStart: '',
actualReleaseDateEnd: '',
pageNo: 1,
pageSize: 20
})
const formRef = ref()
/** 获取分页数据 */
const getPage = async () => {
loading.value = true
try {
const data = await ReleaseApi.getReleasePage(searchForm)
tableData.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索 */
const handleSearch = () => {
searchForm.pageNo = 1
getPage()
}
/** 重置 */
const handleReset = () => {
searchForm.prisonerNo = ''
searchForm.prisonerName = ''
searchForm.releaseType = undefined
searchForm.status = undefined
searchForm.actualReleaseDateStart = ''
searchForm.actualReleaseDateEnd = ''
dateRange.value = []
getPage()
}
/** 日期范围变化 */
const handleDateChange = (val: any) => {
if (val) {
searchForm.actualReleaseDateStart = val[0]
searchForm.actualReleaseDateEnd = val[1]
} else {
searchForm.actualReleaseDateStart = ''
searchForm.actualReleaseDateEnd = ''
}
}
/** 新增 */
const handleCreate = () => {
formRef.value.open()
}
/** 编辑 */
const handleEdit = (row: any) => {
formRef.value.open(row.id)
}
/** 详情 */
const handleDetail = (row: any) => {
formRef.value.open(row.id, true)
}
/** 执行释放 */
const handleDoRelease = (row: any) => {
ElMessageBox.confirm(`确认要执行释放罪犯【${row.prisonerName}】吗?`, '操作确认', {
type: 'warning'
}).then(async () => {
await ReleaseApi.doRelease(row.id)
ElMessage.success('执行释放成功')
getPage()
})
}
/** 取消释放 */
const handleCancel = (row: any) => {
ElMessageBox.confirm(`确认要取消释放登记吗?`, '操作确认', {
type: 'warning'
}).then(async () => {
await ReleaseApi.cancelRelease(row.id)
ElMessage.success('取消释放成功')
getPage()
})
}
/** 删除 */
const handleDelete = (row: any) => {
ElMessageBox.confirm(`确认要删除该释放登记吗?`, '操作确认', {
type: 'warning'
}).then(async () => {
await ReleaseApi.deleteRelease(row.id)
ElMessage.success('删除成功')
getPage()
})
}
/** 状态样式 */
const getStatusType = (status: number) => {
switch (status) {
case 1:
return 'info'
case 2:
return 'success'
case 3:
return 'warning'
default:
return 'info'
}
}
onMounted(() => {
getPage()
})
</script>
<style scoped>
.release-page {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="formData.id ? '编辑考核规则' : '新增考核规则'"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="考核类别" prop="category">
<el-select v-model="formData.category" placeholder="请选择考核类别" style="width: 100%">
<el-option label="劳动改造" :value="1" />
<el-option label="教育改造" :value="2" />
<el-option label="日常行为" :value="3" />
<el-option label="卫生纪律" :value="4" />
<el-option label="加分项" :value="5" />
<el-option label="扣分项" :value="6" />
</el-select>
</el-form-item>
<el-form-item label="项目名称" prop="itemName">
<el-input v-model="formData.itemName" placeholder="请输入项目名称" />
</el-form-item>
<el-form-item label="项目编码" prop="itemCode">
<el-input v-model="formData.itemCode" placeholder="请输入项目编码(唯一)" />
</el-form-item>
<el-form-item label="分值" prop="score">
<el-input-number v-model="formData.score" :precision="2" :step="0.5" :min="-100" :max="100" style="width: 100%" />
</el-form-item>
<el-form-item label="日最高分">
<el-input-number v-model="formData.maxDailyScore" :precision="2" :step="0.5" :min="0" :max="100" style="width: 100%" />
</el-form-item>
<el-form-item label="月最高分">
<el-input-number v-model="formData.maxMonthlyScore" :precision="2" :step="0.5" :min="0" :max="500" style="width: 100%" />
</el-form-item>
<el-form-item label="规则说明">
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入规则说明" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="2">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="formData.sort" :min="0" :max="999" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { ScoreRuleApi, type ScoreRuleSaveReqVO } from '@/api/prison/score-rule'
defineOptions({ name: 'PrisonScoreRuleForm' })
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formData = reactive<ScoreRuleSaveReqVO>({
id: undefined,
category: undefined,
itemName: '',
itemCode: '',
score: 0,
maxDailyScore: undefined,
maxMonthlyScore: undefined,
description: '',
status: 1,
sort: 0
})
const rules = {
category: [{ required: true, message: '请选择考核类别', trigger: 'change' }],
itemName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
itemCode: [{ required: true, message: '请输入项目编码', trigger: 'blur' }],
score: [{ required: true, message: '请输入分值', trigger: 'blur' }]
}
/** 打开弹窗 */
const open = (id?: number) => {
dialogVisible.value = true
resetForm()
if (id) {
loadData(id)
}
}
/** 重置表单 */
const resetForm = () => {
formData.id = undefined
formData.category = undefined
formData.itemName = ''
formData.itemCode = ''
formData.score = 0
formData.maxDailyScore = undefined
formData.maxMonthlyScore = undefined
formData.description = ''
formData.status = 1
formData.sort = 0
}
/** 加载数据 */
const loadData = async (id: number) => {
const data = await ScoreRuleApi.getRule(id)
Object.assign(formData, data)
}
/** 提交 */
const handleSubmit = async () => {
submitLoading.value = true
try {
if (formData.id) {
await ScoreRuleApi.updateRule(formData)
ElMessage.success('更新成功')
} else {
await ScoreRuleApi.createRule(formData)
ElMessage.success('创建成功')
}
dialogVisible.value = false
emit('success')
} finally {
submitLoading.value = false
}
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,200 @@
<template>
<div class="score-rule-page">
<!-- 搜索表单 -->
<el-card shadow="never" class="search-card">
<el-form ref="searchFormRef" :model="searchForm" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="考核类别" prop="category">
<el-select v-model="searchForm.category" placeholder="请选择" clearable>
<el-option label="劳动改造" :value="1" />
<el-option label="教育改造" :value="2" />
<el-option label="日常行为" :value="3" />
<el-option label="卫生纪律" :value="4" />
<el-option label="加分项" :value="5" />
<el-option label="扣分项" :value="6" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目名称" prop="itemName">
<el-input v-model="searchForm.itemName" placeholder="请输入项目名称" clearable />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item>
<el-button type="primary" @click="handleSearch">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="handleReset">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 数据表格 -->
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>考核规则配置</span>
<el-button type="primary" @click="handleCreate" v-hasPermi="'prison:score-rule:create'">
<Icon icon="ep:plus" class="mr-5px" /> 新增规则
</el-button>
</div>
</template>
<el-table v-loading="loading" :data="tableData" border stripe>
<el-table-column prop="categoryName" label="考核类别" width="120" />
<el-table-column prop="itemName" label="项目名称" width="180" />
<el-table-column prop="itemCode" label="项目编码" width="120" />
<el-table-column prop="score" label="分值" width="100">
<template #default="{ row }">
<span :class="{ 'text-danger': row.score < 0, 'text-success': row.score > 0 }">
{{ row.score > 0 ? '+' : '' }}{{ row.score }}
</span>
</template>
</el-table-column>
<el-table-column prop="maxDailyScore" label="日最高分" width="100" />
<el-table-column prop="maxMonthlyScore" label="月最高分" width="100" />
<el-table-column prop="description" label="规则说明" show-overflow-tooltip />
<el-table-column prop="statusName" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.statusName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)" v-hasPermi="'prison:score-rule:update'">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(row)" v-hasPermi="'prison:score-rule:delete'">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="searchForm.pageNo"
v-model:limit="searchForm.pageSize"
@pagination="getPage"
/>
</el-card>
<!-- 规则表单弹窗 -->
<ScoreRuleForm ref="formRef" @success="getPage" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import ScoreRuleForm from './ScoreRuleForm.vue'
import { ScoreRuleApi, type ScoreRulePageReqVO } from '@/api/prison/score-rule'
defineOptions({ name: 'PrisonScoreRule' })
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const searchForm = reactive<ScoreRulePageReqVO>({
category: undefined,
itemName: '',
status: undefined,
pageNo: 1,
pageSize: 20
})
const formRef = ref()
/** 获取分页数据 */
const getPage = async () => {
loading.value = true
try {
const data = await ScoreRuleApi.getRulePage(searchForm)
tableData.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索 */
const handleSearch = () => {
searchForm.pageNo = 1
getPage()
}
/** 重置 */
const handleReset = () => {
searchForm.category = undefined
searchForm.itemName = ''
searchForm.status = undefined
getPage()
}
/** 新增 */
const handleCreate = () => {
formRef.value.open()
}
/** 编辑 */
const handleEdit = (row: any) => {
formRef.value.open(row.id)
}
/** 删除 */
const handleDelete = (row: any) => {
ElMessageBox.confirm(`确认要删除规则【${row.itemName}】吗?`, '操作确认', {
type: 'warning'
}).then(async () => {
await ScoreRuleApi.deleteRule(row.id)
ElMessage.success('删除成功')
getPage()
})
}
onMounted(() => {
getPage()
})
</script>
<style scoped>
.score-rule-page {
padding: 20px;
}
.search-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.text-danger {
color: #f56c6c;
}
.text-success {
color: #67c23a;
}
</style>