代码审查修复: - 前端页面统一使用 dateFormatter 格式化日期 - API 文件添加分页参数类型定义 - 移除未使用的 Dayjs 导入 前端类型完善: - RiskAssessmentPageParams 危险评估分页参数 - ScorePageParams 计分考核分页参数 - ConsumptionPageParams 消费记录分页参数 新增评估模块前端: - assessment API 接口定义 - assessment/record 评估记录列表页面
49 KiB
49 KiB
计分考核模块 - 前端实施文档
版本:v1.0 创建日期:2026-01-14 优先级:P0
一、概述
1.1 文档说明
本文档为计分考核模块的前端实施指南,基于需求文档「需求-03-计分考核.md」和后端实施文档编写。
1.2 前端文件结构
frontend/src/
├── views/prison/score/
│ ├── subject/ # 考核规则配置
│ │ ├── index.vue # 列表页
│ │ └── SubjectForm.vue # 新增/编辑表单
│ ├── record/ # 日常考核记录
│ │ ├── index.vue # 列表页
│ │ └── RecordForm.vue # 新增/编辑表单(含批量录入)
│ ├── monthly/ # 月度考核汇总
│ │ ├── index.vue # 列表页
│ │ └── MonthlyDetail.vue # 月度明细
│ ├── level/ # 等级规则配置
│ │ └── index.vue # 配置页
│ ├── notice/ # 考核公示
│ │ ├── index.vue # 列表页
│ │ └── NoticeForm.vue # 新增/编辑/发布表单
│ └── parole/ # 减刑假释数据
│ └── index.vue # 数据提取页
│
└── api/prison/score/
├── subject.ts # 考核规则API
├── record.ts # 日常记录API
├── monthly.ts # 月度汇总API
├── level.ts # 等级规则API
├── notice.ts # 公示公告API
└── parole.ts # 减刑假释API
二、API接口定义
2.1 考核规则科目API(subject.ts)
import { request } from '@/config/axios'
import type { PageData, PageResult } from '@/types'
/** 考核规则科目接口 */
export interface ScoreSubject {
id: number
code: string
name: string
category: number
categoryName: string
score: number
dailyLimit: number
monthlyLimit: number
description: string
status: number
statusName: string
sort: number
createTime: string
}
/** 查询参数 */
export interface ScoreSubjectQuery extends PageData {
code?: string
name?: string
category?: number
status?: number
}
/** 保存参数 */
export interface ScoreSubjectSaveParams {
id?: number
code: string
name: string
category: number
score: number
dailyLimit?: number
monthlyLimit?: number
description?: string
status?: number
sort?: number
}
/** 考核类别选项 */
export const CATEGORY_OPTIONS = [
{ value: 1, label: '劳动改造' },
{ value: 2, label: '教育改造' },
{ value: 3, label: '日常行为' },
{ value: 4, label: '卫生纪律' },
{ value: 5, label: '加分项' },
{ value: 6, label: '扣分项' }
]
export const ScoreSubjectApi = {
/** 分页查询 */
getPage: async (params: ScoreSubjectQuery) => {
return await request.get({ url: '/score/subject/page', params })
},
/** 获取详情 */
get: async (id: number) => {
return await request.get({ url: `/score/subject/get?id=${id}` })
},
/** 获取列表(无分页) */
getList: async (params?: Omit<ScoreSubjectQuery, 'pageNum' | 'pageSize'>) => {
return await request.get({ url: '/score/subject/list', params })
},
/** 创建 */
create: async (data: ScoreSubjectSaveParams) => {
return await request.post({ url: '/score/subject/create', data })
},
/** 更新 */
update: async (data: ScoreSubjectSaveParams) => {
return await request.put({ url: '/score/subject/update', data })
},
/** 删除 */
delete: async (id: number) => {
return await request.delete({ url: `/score/subject/delete?id=${id}` })
},
/** 批量删除 */
deleteList: async (ids: number[]) => {
return await request.delete({ url: `/score/subject/delete-list?ids=${ids.join(',')}` })
},
/** 更新状态 */
updateStatus: async (id: number, status: number) => {
return await request.put({ url: '/score/subject/update-status', params: { id, status } })
},
/** 导出Excel */
export: async (params: ScoreSubjectQuery) => {
return await request.download({ url: '/score/subject/export-excel', params })
}
}
2.2 日常考核记录API(record.ts)
import { request } from '@/config/axios'
import type { PageData, PageResult } from '@/types'
/** 日常考核记录接口 */
export interface ScoreRecord {
id: number
prisonerId: number
prisonerNo: string
prisonerName: string
prisonAreaId: number
prisonAreaName: string
recordDate: string
subjectId: number
subjectName: string
category: number
categoryName: string
score: number
actualScore: number
remark: string
recorderId: number
recorderName: string
createTime: string
}
/** 查询参数 */
export interface ScoreRecordQuery extends PageData {
prisonerNo?: string
prisonerName?: string
recordDate?: string
recordDateRange?: string[]
category?: number
subjectId?: number
prisonAreaId?: number
}
/** 保存参数 */
export interface ScoreRecordSaveParams {
id?: number
prisonerId: number
recordDate: string
subjectId: number
score: number
remark?: string
}
/** 批量创建参数 */
export interface ScoreRecordBatchParams {
recordDate: string
prisonerIds: number[]
subjectId: number
score: number
remark?: string
}
export const ScoreRecordApi = {
/** 分页查询 */
getPage: async (params: ScoreRecordQuery) => {
return await request.get({ url: '/score/record/page', params })
},
/** 获取详情 */
get: async (id: number) => {
return await request.get({ url: `/score/record/get?id=${id}` })
},
/** 获取罪犯考核明细 */
getByPrisoner: async (prisonerId: number, year?: number, month?: number) => {
return await request.get({ url: '/score/record/get-by-prisoner', params: { prisonerId, year, month } })
},
/** 创建 */
create: async (data: ScoreRecordSaveParams) => {
return await request.post({ url: '/score/record/create', data })
},
/** 批量创建 */
batchCreate: async (data: ScoreRecordBatchParams) => {
return await request.post({ url: '/score/record/batch-create', data })
},
/** 更新 */
update: async (data: ScoreRecordSaveParams) => {
return await request.put({ url: '/score/record/update', data })
},
/** 删除 */
delete: async (id: number) => {
return await request.delete({ url: `/score/record/delete?id=${id}` })
},
/** 导出Excel */
export: async (params: ScoreRecordQuery) => {
return await request.download({ url: '/score/record/export-excel', params })
}
}
2.3 月度考核汇总API(monthly.ts)
import { request } from '@/config/axios'
import type { PageData } from '@/types'
/** 月度考核汇总接口 */
export interface ScoreMonthly {
id: number
prisonerId: number
prisonerNo: string
prisonerName: string
prisonAreaId: number
prisonAreaName: string
year: number
month: number
baseScore: number
rewardScore: number
penaltyScore: number
totalScore: number
level: number
levelName: string
assessorId: number
assessorName: string
status: number
statusName: string
remark: string
createTime: string
}
/** 查询参数 */
export interface ScoreMonthlyQuery extends PageData {
prisonerNo?: string
prisonerName?: string
year: number
month?: number
level?: number
status?: number
prisonAreaId?: number
}
/** 月度汇总明细 */
export interface ScoreMonthlyDetail {
monthly: ScoreMonthly
records: ScoreRecord[]
}
/** 减刑假释数据 */
export interface ScoreParoleData {
prisonerId: number
prisonerNo: string
prisonerName: string
monthlyScores: Array<{
year: number
month: number
totalScore: number
level: number
levelName: string
}>
yearlyTotalScore: number
praiseCount: number
warningCount: number
suggestion: {
canApplyReduction: boolean
suggestedReductionMonths: number
canApplyParole: boolean
paroleOpinion: string
}
}
export const ScoreMonthlyApi = {
/** 分页查询 */
getPage: async (params: ScoreMonthlyQuery) => {
return await request.get({ url: '/score/monthly/page', params })
},
/** 获取汇总详情 */
getSummary: async (id: number) => {
return await request.get({ url: `/score/monthly/get-summary?id=${id}` })
},
/** 获取月度汇总明细 */
getDetail: async (id: number) => {
return await request.get({ url: `/score/monthly/get-detail?id=${id}` })
},
/** 手动触发月度汇总计算 */
calculate: async (year: number, month: number) => {
return await request.post({ url: '/score/monthly/calculate', params: { year, month } })
},
/** 提交审核 */
submitAudit: async (id: number) => {
return await request.post({ url: '/score/monthly/submit-audit', params: { id } })
},
/** 审核通过 */
auditPass: async (id: number, remark?: string) => {
return await request.post({ url: '/score/monthly/audit', params: { id, status: 2, remark } })
},
/** 审核驳回 */
auditReject: async (id: number, remark: string) => {
return await request.post({ url: '/score/monthly/audit', params: { id, status: 3, remark } })
},
/** 导出Excel */
export: async (params: ScoreMonthlyQuery) => {
return await request.download({ url: '/score/monthly/export-excel', params })
},
/** 获取减刑假释数据 */
getParoleData: async (prisonerId: number) => {
return await request.get({ url: '/score/parole/get-data', params: { prisonerId } })
},
/** 生成减刑假释建议 */
generateSuggestion: async (prisonerId: number) => {
return await request.post({ url: '/score/parole/generate-suggestion', params: { prisonerId } })
}
}
2.4 等级规则API(level.ts)
import { request } from '@/config/axios'
/** 等级规则接口 */
export interface ScoreLevelRule {
id: number
level: number
levelName: string
minScore: number
maxScore: number
description: string
sort: number
status: number
}
/** 基础分配置 */
export interface BaseScoreConfig {
baseScore: number
}
export const ScoreLevelApi = {
/** 获取等级规则列表 */
getList: async () => {
return await request.get({ url: '/score/level/get' })
},
/** 更新等级规则 */
update: async (data: ScoreLevelRule[]) => {
return await request.put({ url: '/score/level/update', data })
},
/** 获取基础分配置 */
getBaseScore: async () => {
return await request.get({ url: '/score/level/get-base-score' })
},
/** 更新基础分配置 */
updateBaseScore: async (baseScore: number) => {
return await request.put({ url: '/score/level/update-base-score', params: { baseScore } })
}
}
2.5 公示公告API(notice.ts)
import { request } from '@/config/axios'
import type { PageData } from '@/types'
/** 公示公告接口 */
export interface ScoreNotice {
id: number
title: string
noticeType: number
noticeTypeName: string
startDate: string
endDate: string
scopeType: number
scopeTypeName: string
scopePrisonAreaIds: string
scopePrisonAreaNames: string
content: string
status: number
statusName: string
publisherId: number
publisherName: string
publishTime: string
createTime: string
}
/** 查询参数 */
export interface ScoreNoticeQuery extends PageData {
title?: string
noticeType?: number
status?: number
startDate?: string
endDate?: string
}
/** 保存参数 */
export interface ScoreNoticeSaveParams {
id?: number
title: string
noticeType: number
startDate: string
endDate: string
scopeType: number
scopePrisonAreaIds?: string
content?: string
}
export const ScoreNoticeApi = {
/** 分页查询 */
getPage: async (params: ScoreNoticeQuery) => {
return await request.get({ url: '/score/notice/page', params })
},
/** 获取详情 */
get: async (id: number) => {
return await request.get({ url: `/score/notice/get?id=${id}` })
},
/** 创建 */
create: async (data: ScoreNoticeSaveParams) => {
return await request.post({ url: '/score/notice/create', data })
},
/** 更新 */
update: async (data: ScoreNoticeSaveParams) => {
return await request.put({ url: '/score/notice/update', data })
},
/** 发布 */
publish: async (id: number) => {
return await request.put({ url: `/score/notice/publish?id=${id}` })
},
/** 撤回 */
withdraw: async (id: number) => {
return await request.put({ url: `/score/notice/withdraw?id=${id}` })
},
/** 删除 */
delete: async (id: number) => {
return await request.delete({ url: `/score/notice/delete?id=${id}` })
}
}
三、页面组件设计
3.1 考核规则配置页面(subject/index.vue)
<template>
<div class="score-subject">
<el-card>
<template #header>
<div class="card-header">
<span>考核规则配置</span>
<el-button type="primary" @click="handleAdd">新增规则</el-button>
</div>
</template>
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item label="规则编码" prop="code">
<el-input v-model="queryParams.code" placeholder="请输入规则编码" clearable />
</el-form-item>
<el-form-item label="规则名称" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入规则名称" clearable />
</el-form-item>
<el-form-item label="考核类别" prop="category">
<el-select v-model="queryParams.category" placeholder="请选择" clearable>
<el-option v-for="item in CATEGORY_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格区域 -->
<el-table v-loading="loading" :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" />
<el-table-column prop="code" label="规则编码" width="120" />
<el-table-column prop="name" label="规则名称" width="180" />
<el-table-column prop="categoryName" label="考核类别" width="120" />
<el-table-column prop="score" label="分值" width="100">
<template #default="{ row }">
<span :class="{ 'text-success': row.score > 0, 'text-danger': row.score < 0 }">
{{ row.score > 0 ? '+' : '' }}{{ row.score }}
</span>
</template>
</el-table-column>
<el-table-column prop="dailyLimit" label="日限" width="80" />
<el-table-column prop="monthlyLimit" label="月限" width="80" />
<el-table-column prop="statusName" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.statusName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="primary" @click="handleToggleStatus(row)">
{{ row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<SubjectForm ref="formRef" @success="getList" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ScoreSubjectApi, type ScoreSubject, type ScoreSubjectQuery } from '@/api/prison/score/subject'
import SubjectForm from './SubjectForm.vue'
const loading = ref(false)
const tableData = ref<ScoreSubject[]>([])
const total = ref(0)
const queryParams = reactive<ScoreSubjectQuery>({
pageNum: 1,
pageSize: 10,
code: '',
name: '',
category: undefined,
status: undefined
})
const selectedIds = ref<number[]>([])
const formRef = ref()
const getList = async () => {
loading.value = true
try {
const data = await ScoreSubjectApi.getPage(queryParams)
tableData.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const resetQuery = () => {
queryParams.code = ''
queryParams.name = ''
queryParams.category = undefined
queryParams.status = undefined
handleQuery()
}
const handleAdd = () => {
formRef.value?.open()
}
const handleEdit = (row: ScoreSubject) => {
formRef.value?.open(row.id)
}
const handleDelete = async (row: ScoreSubject) => {
await ElMessageBox.confirm(`确定要删除规则"${row.name}"吗?`, '提示')
await ScoreSubjectApi.delete(row.id)
ElMessage.success('删除成功')
getList()
}
const handleToggleStatus = async (row: ScoreSubject) => {
const newStatus = row.status === 1 ? 2 : 1
await ScoreSubjectApi.updateStatus(row.id, newStatus)
ElMessage.success(newStatus === 1 ? '已启用' : '已禁用')
getList()
}
const handleSelectionChange = (selection: ScoreSubject[]) => {
selectedIds.value = selection.map(item => item.id)
}
onMounted(() => {
getList()
})
</script>
3.2 考核规则表单组件(subject/SubjectForm.vue)
<template>
<el-dialog v-model="visible" :title="isEdit ? '编辑规则' : '新增规则'" width="600px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="规则编码" prop="code">
<el-input v-model="formData.code" :disabled="isEdit" placeholder="请输入规则编码" />
</el-form-item>
<el-form-item label="规则名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入规则名称" />
</el-form-item>
<el-form-item label="考核类别" prop="category">
<el-select v-model="formData.category" placeholder="请选择考核类别">
<el-option v-for="item in CATEGORY_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="分值" prop="score">
<el-input-number v-model="formData.score" :precision="2" :step="0.5" placeholder="正数为加分,负数为扣分" />
</el-form-item>
<el-form-item label="日最高分" prop="dailyLimit">
<el-input-number v-model="formData.dailyLimit" :precision="2" :step="0.5" placeholder="请输入日最高分限制" />
</el-form-item>
<el-form-item label="月最高分" prop="monthlyLimit">
<el-input-number v-model="formData.monthlyLimit" :precision="2" :step="0.5" placeholder="请输入月最高分限制" />
</el-form-item>
<el-form-item label="规则说明" prop="description">
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入规则说明" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="999" />
</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>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { ScoreSubjectApi, CATEGORY_OPTIONS, type ScoreSubjectSaveParams } from '@/api/prison/score/subject'
const emit = defineEmits(['success'])
const visible = ref(false)
const submitting = ref(false)
const formRef = ref()
const currentId = ref<number | null>(null)
const isEdit = computed(() => !!currentId.value)
const formData = reactive<ScoreSubjectSaveParams>({
code: '',
name: '',
category: undefined as unknown as number,
score: 0,
dailyLimit: 0,
monthlyLimit: 0,
description: '',
sort: 0,
status: 1
})
const rules = {
code: [{ required: true, message: '请输入规则编码', trigger: 'blur' }],
name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
category: [{ required: true, message: '请选择考核类别', trigger: 'change' }],
score: [{ required: true, message: '请输入分值', trigger: 'blur' }]
}
const open = async (id?: number) => {
currentId.value = id ?? null
visible.value = true
if (id) {
const data = await ScoreSubjectApi.get(id)
Object.assign(formData, data)
} else {
Object.assign(formData, {
code: '',
name: '',
category: undefined,
score: 0,
dailyLimit: 0,
monthlyLimit: 0,
description: '',
sort: 0,
status: 1
})
}
}
const handleSubmit = async () => {
await formRef.value?.validate()
submitting.value = true
try {
if (isEdit.value) {
await ScoreSubjectApi.update(formData)
ElMessage.success('更新成功')
} else {
await ScoreSubjectApi.create(formData)
ElMessage.success('创建成功')
}
visible.value = false
emit('success')
} finally {
submitting.value = false
}
}
defineExpose({ open })
</script>
3.3 日常考核记录页面(record/index.vue)
<template>
<div class="score-record">
<el-card>
<template #header>
<div class="card-header">
<span>日常考核记录</span>
<div>
<el-button type="primary" @click="handleAdd">新增记录</el-button>
<el-button type="primary" @click="handleBatchAdd">批量录入</el-button>
</div>
</div>
</template>
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item label="考核日期" prop="recordDateRange">
<el-date-picker
v-model="queryParams.recordDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input v-model="queryParams.prisonerNo" placeholder="请输入罪犯编号" clearable />
</el-form-item>
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input v-model="queryParams.prisonerName" placeholder="请输入罪犯姓名" clearable />
</el-form-item>
<el-form-item label="考核类别" prop="category">
<el-select v-model="queryParams.category" placeholder="请选择" clearable>
<el-option v-for="item in CATEGORY_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="success" @click="handleExport">导出</el-button>
</el-form-item>
</el-form>
<!-- 表格区域 -->
<el-table v-loading="loading" :data="tableData" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" />
<el-table-column prop="recordDate" label="考核日期" width="120" />
<el-table-column prop="prisonerNo" label="罪犯编号" width="100" />
<el-table-column prop="prisonerName" label="罪犯姓名" width="100" />
<el-table-column prop="prisonAreaName" label="监区/分监区" width="150" />
<el-table-column prop="categoryName" label="考核类别" width="120" />
<el-table-column prop="subjectName" label="规则名称" width="180" />
<el-table-column prop="score" label="得分" width="100">
<template #default="{ row }">
<span :class="{ 'text-success': row.score > 0, 'text-danger': row.score < 0 }">
{{ row.score > 0 ? '+' : '' }}{{ row.score }}
</span>
</template>
</el-table-column>
<el-table-column prop="actualScore" label="实际得分" width="100" />
<el-table-column prop="recorderName" label="记录人" width="100" />
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<!-- 新增/编辑弹窗 -->
<RecordForm ref="formRef" @success="getList" />
<!-- 批量录入弹窗 -->
<BatchRecordForm ref="batchFormRef" @success="getList" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ScoreRecordApi, CATEGORY_OPTIONS, type ScoreRecord, type ScoreRecordQuery } from '@/api/prison/score/record'
import RecordForm from './RecordForm.vue'
import BatchRecordForm from './BatchRecordForm.vue'
const loading = ref(false)
const tableData = ref<ScoreRecord[]>([])
const total = ref(0)
const queryParams = reactive<ScoreRecordQuery>({
pageNum: 1,
pageSize: 10,
recordDateRange: [],
prisonerNo: '',
prisonerName: '',
category: undefined
})
const selectedIds = ref<number[]>([])
const formRef = ref()
const batchFormRef = ref()
const getList = async () => {
loading.value = true
try {
const params = { ...queryParams }
if (params.recordDateRange?.length === 2) {
params.recordDate = params.recordDateRange[0]
}
delete params.recordDateRange
const data = await ScoreRecordApi.getPage(params)
tableData.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const resetQuery = () => {
Object.assign(queryParams, {
pageNum: 1,
recordDateRange: [],
prisonerNo: '',
prisonerName: '',
category: undefined
})
handleQuery()
}
const handleAdd = () => {
formRef.value?.open()
}
const handleBatchAdd = () => {
batchFormRef.value?.open()
}
const handleDelete = async (row: ScoreRecord) => {
await ElMessageBox.confirm(`确定要删除该考核记录吗?`, '提示')
await ScoreRecordApi.delete(row.id)
ElMessage.success('删除成功')
getList()
}
const handleExport = () => {
ScoreRecordApi.export(queryParams)
}
const handleSelectionChange = (selection: ScoreRecord[]) => {
selectedIds.value = selection.map(item => item.id)
}
onMounted(() => {
getList()
})
</script>
3.4 批量录入表单组件(record/BatchRecordForm.vue)
<template>
<el-dialog v-model="visible" title="批量录入考核" width="700px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="考核日期" prop="recordDate">
<el-date-picker
v-model="formData.recordDate"
type="date"
placeholder="请选择考核日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="选择罪犯" prop="prisonerIds">
<el-select
v-model="formData.prisonerIds"
multiple
filterable
remote
placeholder="请选择罪犯"
:remote-method="searchPrisoners"
:loading="prisonerLoading"
>
<el-option
v-for="item in prisonerOptions"
:key="item.id"
:label="`${item.prisonerNo} - ${item.name}`"
:value="item.id"
/>
</el-select>
<div class="form-tip">已选择 {{ formData.prisonerIds.length }} 名罪犯</div>
</el-form-item>
<el-form-item label="考核规则" prop="subjectId">
<el-select v-model="formData.subjectId" placeholder="请选择考核规则" filterable>
<el-option-group v-for="group in subjectOptions" :key="group.label" :label="group.label">
<el-option
v-for="item in group.options"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="得分" prop="score">
<el-input-number v-model="formData.score" :precision="2" :step="0.5" />
<span class="unit">分</span>
</el-form-item>
<el-form-item label="备注说明" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注说明" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting" :disabled="formData.prisonerIds.length === 0">
确定录入
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { ScoreRecordApi, type ScoreRecordBatchParams } from '@/api/prison/score/record'
const emit = defineEmits(['success'])
const visible = ref(false)
const submitting = ref(false)
const prisonerLoading = ref(false)
const formRef = ref()
const prisonerOptions = ref<any[]>([])
const subjectOptions = ref<any[]>([])
const formData = reactive<ScoreRecordBatchParams>({
recordDate: '',
prisonerIds: [],
subjectId: undefined as unknown as number,
score: 0,
remark: ''
})
const rules = {
recordDate: [{ required: true, message: '请选择考核日期', trigger: 'change' }],
prisonerIds: [{ required: true, message: '请选择罪犯', trigger: 'change' }],
subjectId: [{ required: true, message: '请选择考核规则', trigger: 'change' }],
score: [{ required: true, message: '请输入得分', trigger: 'blur' }]
}
const open = async () => {
visible.value = true
// 加载考核规则列表
const subjects = await ScoreSubjectApi.getList({ status: 1 })
// 按类别分组
subjectOptions.value = groupSubjectsByCategory(subjects)
// 重置表单
Object.assign(formData, {
recordDate: new Date().toISOString().split('T')[0],
prisonerIds: [],
subjectId: undefined,
score: 0,
remark: ''
})
}
const searchPrisoners = async (keyword: string) => {
if (!keyword) return
prisonerLoading.value = true
try {
// 调用罪犯搜索接口
prisonerOptions.value = await prisonerApi.search(keyword)
} finally {
prisonerLoading.value = false
}
}
const handleSubmit = async () => {
await formRef.value?.validate()
submitting.value = true
try {
await ScoreRecordApi.batchCreate(formData)
ElMessage.success(`成功录入 ${formData.prisonerIds.length} 条考核记录`)
visible.value = false
emit('success')
} finally {
submitting.value = false
}
}
const groupSubjectsByCategory = (subjects: any[]) => {
const groups: Record<number, any[]> = {}
subjects.forEach(item => {
if (!groups[item.category]) {
groups[item.category] = []
}
groups[item.category].push(item)
})
return Object.entries(groups).map(([key, value]) => ({
label: getCategoryLabel(Number(key)),
options: value
}))
}
const getCategoryLabel = (category: number) => {
const labels: Record<number, string> = {
1: '劳动改造',
2: '教育改造',
3: '日常行为',
4: '卫生纪律',
5: '加分项',
6: '扣分项'
}
return labels[category] || '其他'
}
defineExpose({ open })
</script>
3.5 月度考核汇总页面(monthly/index.vue)
<template>
<div class="score-monthly">
<el-card>
<template #header>
<div class="card-header">
<span>月度考核汇总</span>
<div>
<el-button type="primary" @click="handleCalculate">重新计算</el-button>
<el-button type="success" @click="handleExport">导出报表</el-button>
</div>
</div>
</template>
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item label="考核年月" prop="yearMonth">
<el-date-picker
v-model="queryParams.yearMonth"
type="month"
placeholder="请选择考核年月"
value-format="YYYY-MM"
/>
</el-form-item>
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input v-model="queryParams.prisonerNo" placeholder="请输入罪犯编号" clearable />
</el-form-item>
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input v-model="queryParams.prisonerName" placeholder="请输入罪犯姓名" clearable />
</el-form-item>
<el-form-item label="考核等级" prop="level">
<el-select v-model="queryParams.level" placeholder="请选择" clearable>
<el-option label="优秀" :value="1" />
<el-option label="良好" :value="2" />
<el-option label="合格" :value="3" />
<el-option label="不合格" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="审核状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="待审核" :value="1" />
<el-option label="已通过" :value="2" />
<el-option label="已驳回" :value="3" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stat-cards">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ summaryStats.excellent }}</div>
<div class="stat-label">优秀</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ summaryStats.good }}</div>
<div class="stat-label">良好</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-value">{{ summaryStats.qualified }}</div>
<div class="stat-label">合格</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card danger">
<div class="stat-value">{{ summaryStats.unqualified }}</div>
<div class="stat-label">不合格</div>
</el-card>
</el-col>
</el-row>
<!-- 表格区域 -->
<el-table v-loading="loading" :data="tableData" @row-click="handleRowClick">
<el-table-column prop="prisonerNo" label="罪犯编号" width="100" />
<el-table-column prop="prisonerName" label="罪犯姓名" width="100" />
<el-table-column prop="prisonAreaName" label="监区/分监区" width="150" />
<el-table-column prop="baseScore" label="基础分" width="80" />
<el-table-column prop="rewardScore" label="加分" width="80">
<template #default="{ row }">
<span class="text-success">+{{ row.rewardScore }}</span>
</template>
</el-table-column>
<el-table-column prop="penaltyScore" label="扣分" width="80">
<template #default="{ row }">
<span class="text-danger">{{ row.penaltyScore }}</span>
</template>
</el-table-column>
<el-table-column prop="totalScore" label="总分" width="100">
<template #default="{ row }">
<span class="total-score">{{ row.totalScore }}</span>
</template>
</el-table-column>
<el-table-column prop="levelName" label="等级" width="80">
<template #default="{ row }">
<el-tag :type="getLevelType(row.level)">{{ row.levelName }}</el-tag>
</template>
</el-table-column>
<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="assessorName" label="考核人" width="100" />
<el-table-column label="操作" width="150" fixed="right" @click.stop>
<template #default="{ row }">
<el-button link type="primary" @click="handleViewDetail(row)">明细</el-button>
<el-button v-if="row.status === 1" link type="success" @click="handleSubmitAudit(row)">提交审核</el-button>
<el-button v-if="row.status === 3" link type="primary" @click="handleViewRemark(row)">查看驳回原因</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<!-- 月度明细弹窗 -->
<MonthlyDetail ref="detailRef" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ScoreMonthlyApi, type ScoreMonthly, type ScoreMonthlyQuery } from '@/api/prison/score/monthly'
import MonthlyDetail from './MonthlyDetail.vue'
const loading = ref(false)
const tableData = ref<ScoreMonthly[]>([])
const total = ref(0)
const queryParams = reactive<ScoreMonthlyQuery>({
pageNum: 1,
pageSize: 10,
yearMonth: new Date().toISOString().slice(0, 7),
prisonerNo: '',
prisonerName: '',
level: undefined,
status: undefined
})
const summaryStats = reactive({
excellent: 0,
good: 0,
qualified: 0,
unqualified: 0
})
const detailRef = ref()
const getList = async () => {
loading.value = true
try {
const params = { ...queryParams }
if (params.yearMonth) {
const [year, month] = params.yearMonth.split('-')
params.year = parseInt(year)
params.month = parseInt(month)
}
delete params.yearMonth
const data = await ScoreMonthlyApi.getPage(params)
tableData.value = data.list
total.value = data.total
// 计算统计数据
calculateStats(data.list)
} finally {
loading.value = false
}
}
const calculateStats = (list: ScoreMonthly[]) => {
summaryStats.excellent = list.filter(item => item.level === 1).length
summaryStats.good = list.filter(item => item.level === 2).length
summaryStats.qualified = list.filter(item => item.level === 3).length
summaryStats.unqualified = list.filter(item => item.level === 4).length
}
const getLevelType = (level: number) => {
const types: Record<number, string> = {
1: 'success',
2: '',
3: 'warning',
4: 'danger'
}
return types[level] || ''
}
const getStatusType = (status: number) => {
const types: Record<number, string> = {
1: 'info',
2: 'success',
3: 'danger'
}
return types[status] || ''
}
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
const resetQuery = () => {
Object.assign(queryParams, {
pageNum: 1,
yearMonth: new Date().toISOString().slice(0, 7),
prisonerNo: '',
prisonerName: '',
level: undefined,
status: undefined
})
handleQuery()
}
const handleCalculate = async () => {
const [year, month] = queryParams.yearMonth!.split('-').map(Number)
await ElMessageBox.confirm('确定要重新计算本月度考核汇总吗?这将覆盖现有数据。', '提示')
await ScoreMonthlyApi.calculate(year, month)
ElMessage.success('计算完成')
getList()
}
const handleExport = () => {
const params = { ...queryParams }
if (params.yearMonth) {
const [year, month] = params.yearMonth.split('-')
params.year = parseInt(year)
params.month = parseInt(month)
}
delete params.yearMonth
ScoreMonthlyApi.export(params)
}
const handleRowClick = (row: ScoreMonthly) => {
handleViewDetail(row)
}
const handleViewDetail = (row: ScoreMonthly) => {
detailRef.value?.open(row.id)
}
const handleSubmitAudit = async (row: ScoreMonthly) => {
await ElMessageBox.confirm(`确定要提交"${row.prisonerName}"的考核结果进行审核吗?`, '提示')
await ScoreMonthlyApi.submitAudit(row.id)
ElMessage.success('提交成功')
getList()
}
const handleViewRemark = (row: ScoreMonthly) => {
ElMessage.info(row.remark || '无驳回原因')
}
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.stat-cards {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
.stat-value {
font-size: 32px;
font-weight: bold;
color: #409eff;
}
.stat-label {
color: #909399;
margin-top: 8px;
}
&.danger .stat-value {
color: #f56c6c;
}
}
.text-success {
color: #67c23a;
}
.text-danger {
color: #f56c6c;
}
.total-score {
font-size: 16px;
font-weight: bold;
}
</style>
3.6 月度明细组件(monthly/MonthlyDetail.vue)
<template>
<el-dialog v-model="visible" title="月度考核明细" width="900px">
<div class="monthly-summary">
<!-- 基本信息 -->
<el-descriptions :column="3" border>
<el-descriptions-item label="罪犯编号">{{ data.prisonerNo }}</el-descriptions-item>
<el-descriptions-item label="罪犯姓名">{{ data.prisonerName }}</el-descriptions-item>
<el-descriptions-item label="考核期间">{{ data.year }}年{{ data.month }}月</el-descriptions-item>
<el-descriptions-item label="基础分">{{ data.baseScore }}</el-descriptions-item>
<el-descriptions-item label="加分合计">
<span class="text-success">+{{ data.rewardScore }}</span>
</el-descriptions-item>
<el-descriptions-item label="扣分合计">
<span class="text-danger">{{ data.penaltyScore }}</span>
</el-descriptions-item>
<el-descriptions-item label="月度总分">
<span class="total-score">{{ data.totalScore }}</span>
</el-descriptions-item>
<el-descriptions-item label="考核等级">
<el-tag :type="getLevelType(data.level)">{{ data.levelName }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="审核状态">
<el-tag :type="getStatusType(data.status)">{{ data.statusName }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<!-- 考核明细表格 -->
<div class="detail-section">
<h4>考核明细</h4>
<el-table :data="records" max-height="400">
<el-table-column prop="recordDate" label="考核日期" width="120" />
<el-table-column prop="categoryName" label="考核类别" width="100" />
<el-table-column prop="subjectName" label="规则名称" width="180" />
<el-table-column prop="score" label="原始得分" width="100">
<template #default="{ row }">
<span :class="{ 'text-success': row.score > 0, 'text-danger': row.score < 0 }">
{{ row.score > 0 ? '+' : '' }}{{ row.score }}
</span>
</template>
</el-table-column>
<el-table-column prop="actualScore" label="实际得分" width="100" />
<el-table-column prop="recorderName" label="记录人" width="100" />
<el-table-column prop="remark" label="备注" min-width="150" />
</el-table>
</div>
</div>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ScoreMonthlyApi, type ScoreMonthly, type ScoreRecord } from '@/api/prison/score/monthly'
const visible = ref(false)
const data = ref<ScoreMonthly>({} as ScoreMonthly)
const records = ref<ScoreRecord[]>([])
const open = async (id: number) => {
visible.value = true
const detail = await ScoreMonthlyApi.getDetail(id)
data.value = detail.monthly
records.value = detail.records
}
const getLevelType = (level: number) => {
const types: Record<number, string> = { 1: 'success', 2: '', 3: 'warning', 4: 'danger' }
return types[level] || ''
}
const getStatusType = (status: number) => {
const types: Record<number, string> = { 1: 'info', 2: 'success', 3: 'danger' }
return types[status] || ''
}
defineExpose({ open })
</script>
四、路由配置
// router/routes/modules/prison.ts
{
path: '/prison/score',
component: Layout,
redirect: '/prison/score/subject',
name: 'PrisonScore',
meta: {
title: '计分考核',
icon: 'score',
alwaysShow: true
},
children: [
{
path: 'subject',
component: () => import('@/views/prison/score/subject/index.vue'),
name: 'PrisonScoreSubject',
meta: {
title: '考核规则配置',
icon: 'list',
permissions: ['prison:score:subject:query']
}
},
{
path: 'record',
component: () => import('@/views/prison/score/record/index.vue'),
name: 'PrisonScoreRecord',
meta: {
title: '日常考核记录',
icon: 'edit',
permissions: ['prison:score:record:query']
}
},
{
path: 'monthly',
component: () => import('@/views/prison/score/monthly/index.vue'),
name: 'PrisonScoreMonthly',
meta: {
title: '月度考核汇总',
icon: 'date',
permissions: ['prison:score:monthly:query']
}
},
{
path: 'level',
component: () => import('@/views/prison/score/level/index.vue'),
name: 'PrisonScoreLevel',
meta: {
title: '等级规则配置',
icon: 'setting',
permissions: ['prison:score:level:query']
}
},
{
path: 'notice',
component: () => import('@/views/prison/score/notice/index.vue'),
name: 'PrisonScoreNotice',
meta: {
title: '考核公示',
icon: 'notification',
permissions: ['prison:score:notice:query']
}
},
{
path: 'parole',
component: () => import('@/views/prison/score/parole/index.vue'),
name: 'PrisonScoreParole',
meta: {
title: '减刑假释数据',
icon: 'document',
permissions: ['prison:score:parole:query']
}
}
]
}
五、菜单权限SQL
-- 计分考核模块菜单权限
-- 1. 考核规则配置
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component) VALUES
('考核规则配置', '', 2, 1, (SELECT id FROM system_menu WHERE name = '计分考核'), 'subject', 'list', 'prison/score/subject/index');
-- 2. 日常考核记录
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component) VALUES
('日常考核记录', '', 2, 2, (SELECT id FROM system_menu WHERE name = '计分考核'), 'record', 'edit', 'prison/score/record/index');
-- 3. 月度考核汇总
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component) VALUES
('月度考核汇总', '', 2, 3, (SELECT id FROM system_menu WHERE name = '计分考核'), 'monthly', 'date', 'prison/score/monthly/index');
-- 4. 等级规则配置
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component) VALUES
('等级规则配置', '', 2, 4, (SELECT id FROM system_menu WHERE name = '计分考核'), 'level', 'setting', 'prison/score/level/index');
-- 5. 考核公示
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component) VALUES
('考核公示', '', 2, 5, (SELECT id FROM system_menu WHERE name = '计分考核'), 'notice', 'notification', 'prison/score/notice/index');
-- 6. 减刑假释数据
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component) VALUES
('减刑假释数据', '', 2, 6, (SELECT id FROM system_menu WHERE name = '计分考核'), 'parole', 'document', 'prison/score/parole/index');
六、验收标准
- 考核规则配置页面:支持规则的增删改查、状态切换
- 日常考核记录页面:支持单条录入和批量录入
- 月度考核汇总页面:支持月度统计展示、明细查看
- 等级规则配置页面:支持等级标准和基础分配置
- 考核公示页面:支持公示的发布和撤回
- 减刑假释数据页面:支持考核数据提取和展示
- 所有页面通过TypeScript类型检查
- 所有页面响应式适配