xlcp/requirements/实施文档-计分考核-前端.md
tangweijie 260dc75a32 fix: 代码审查修复与前端类型完善
代码审查修复:
- 前端页面统一使用 dateFormatter 格式化日期
- API 文件添加分页参数类型定义
- 移除未使用的 Dayjs 导入

前端类型完善:
- RiskAssessmentPageParams 危险评估分页参数
- ScorePageParams 计分考核分页参数
- ConsumptionPageParams 消费记录分页参数

新增评估模块前端:
- assessment API 接口定义
- assessment/record 评估记录列表页面
2026-01-15 20:51:49 +08:00

1640 lines
49 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 计分考核模块 - 前端实施文档
> 版本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 考核规则科目APIsubject.ts
```typescript
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 日常考核记录APIrecord.ts
```typescript
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 月度考核汇总APImonthly.ts
```typescript
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 等级规则APIlevel.ts
```typescript
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 公示公告APInotice.ts
```typescript
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
```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
```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
```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
```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
```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
```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>
```
---
## 四、路由配置
```typescript
// 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
```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类型检查
- [ ] 所有页面响应式适配