代码审查修复: - 前端页面统一使用 dateFormatter 格式化日期 - API 文件添加分页参数类型定义 - 移除未使用的 Dayjs 导入 前端类型完善: - RiskAssessmentPageParams 危险评估分页参数 - ScorePageParams 计分考核分页参数 - ConsumptionPageParams 消费记录分页参数 新增评估模块前端: - assessment API 接口定义 - assessment/record 评估记录列表页面
1640 lines
49 KiB
Markdown
1640 lines
49 KiB
Markdown
# 计分考核模块 - 前端实施文档
|
||
|
||
> 版本: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)
|
||
|
||
```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 日常考核记录API(record.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 月度考核汇总API(monthly.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 等级规则API(level.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 公示公告API(notice.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类型检查
|
||
- [ ] 所有页面响应式适配
|