feat(prison): 新增评估报告等前端页面,优化问卷与罪犯管理

核心变更:
1. 新增页面模块
   - 快捷评语管理 (quick-comment)
   - 报告模板管理 (report-template)
   - 评估报告编辑 (report)
   - 风险分析页面 (risk)
   - 预警管理 (warning)
   - 服刑情况跟踪 (situation)

2. 功能优化
   - 罪犯管理: 新增Workbench工作台页面
   - 问卷模块: 完善QuestionForm组件
   - 计分考核: 优化ScoreForm支持多种评分方式
   - 危险评估: 完善RiskAssessmentForm

3. UI改进
   - 登录页面: 新增监狱特色Loading动画
   - 罪犯详情: 优化展示效果
   - 消费记录: 增强查询功能

4. 基础设施
   - 新增JusticeIcon图标组件
   - 优化字典格式化工具
   - 更新路由配置

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tangweijie 2026-01-16 20:15:17 +08:00
parent 2115e4aa52
commit bbf4c64391
65 changed files with 9437 additions and 1442 deletions

View File

@ -3,8 +3,8 @@ NODE_ENV=development
VITE_DEV=true
# 请求路径 - 本地后端服务地址
VITE_BASE_URL='http://localhost:48080'
# 请求路径 - 后端服务地址
VITE_BASE_URL='http://192.168.9.121:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server

View File

@ -70,6 +70,10 @@ module.exports = defineConfig({
'vue/no-v-html': 'off',
'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
'@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示因为暂时不需要这么严格警告也有点繁琐
'@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
'@unocss/order-attributify': 'off', // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
// 禁止命名空间导入,统一使用默认导入或命名导入
'import/default': 'error',
'import/no-namespace': 'error'
}
})

View File

@ -25,7 +25,7 @@
justify-content: center;
align-items: center;
flex-direction: column;
background: #f0f2f5;
background: linear-gradient(135deg, #1a365d 0%, #2d3748 50%, #1a365d 100%);
}
.app-loading .app-loading-wrap {
@ -41,15 +41,19 @@
}
.app-loading .app-loading-title {
margin-bottom: 30px;
margin-bottom: 20px;
font-size: 20px;
font-weight: bold;
text-align: center;
color: #f6e05e;
letter-spacing: 2px;
}
.app-loading .app-loading-logo {
width: 100px;
margin: 0 auto 15px auto;
width: 64px;
height: 64px;
margin: 0 auto 10px auto;
border-radius: 8px;
}
.app-loading .app-loading-item {
@ -58,91 +62,121 @@
width: 60px;
height: 60px;
vertical-align: middle;
border-radius: 50%;
}
.app-loading .app-loading-outter {
/* 书本+天平图标 */
.app-loading .app-loading-shield {
width: 80px;
height: 80px;
animation: bookFloat 2s ease-in-out infinite;
}
@keyframes bookFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
/* 旋转光环 */
.app-loading .app-loading-ring {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
border: 3px solid transparent;
border-top-color: #ffd700;
border-radius: 50%;
transform: translate(-50%, -50%);
animation: rotateRing 2s linear infinite;
opacity: 0.6;
}
@keyframes rotateRing {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* 进度条 */
.app-loading .app-loading-progress {
width: 200px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
margin-top: 30px;
overflow: hidden;
}
.app-loading .app-loading-progress-bar {
height: 100%;
border: 4px solid #2d8cf0;
border-bottom: 0;
border-left-color: transparent;
border-radius: 50%;
animation: loader-outter 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
width: 30%;
background: linear-gradient(90deg, #daa520, #ffd700, #daa520);
border-radius: 2px;
animation: progressMove 1.5s ease-in-out infinite;
}
.app-loading .app-loading-inner {
position: absolute;
top: calc(50% - 20px);
left: calc(50% - 20px);
width: 40px;
height: 40px;
border: 4px solid #87bdff;
border-right: 0;
border-top-color: transparent;
border-radius: 50%;
animation: loader-inner 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
}
@-webkit-keyframes loader-outter {
@keyframes progressMove {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
width: 10%;
margin-left: 0;
}
50% {
width: 50%;
margin-left: 25%;
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
width: 10%;
margin-left: 90%;
}
}
@keyframes loader-outter {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes loader-inner {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
}
@keyframes loader-inner {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(-360deg);
transform: rotate(-360deg);
}
/* 加载文字 */
.app-loading .app-loading-text {
margin-top: 15px;
font-size: 14px;
color: #a0aec0;
letter-spacing: 2px;
}
</style>
<div class="app-loading">
<div class="app-loading-wrap">
<div class="app-loading-title">
<img src="/logo.gif" class="app-loading-logo" alt="Logo" />
<div class="app-loading-title">%VITE_APP_TITLE%</div>
</div>
<div class="app-loading-title">%VITE_APP_TITLE%</div>
<!-- 书本+天平图标 -->
<div class="app-loading-item">
<div class="app-loading-outter"></div>
<div class="app-loading-inner"></div>
<svg class="app-loading-shield" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- 天平底座 -->
<rect x="35" y="75" width="30" height="6" rx="2" fill="#c41e3a" />
<rect x="48" y="45" width="4" height="30" fill="#ffd700" />
<!-- 天平横杆 -->
<rect x="10" y="42" width="80" height="5" rx="2" fill="#ffd700" />
<!-- 天平左边盘子(往中间移) -->
<path d="M28 47 L38 47 L35 58 L31 58 Z" fill="#ffd700" />
<path d="M25 58 Q33 65 41 58" fill="none" stroke="#ffd700" stroke-width="2" />
<!-- 天平右边盘子 -->
<path d="M62 47 L72 47 L69 58 L65 58 Z" fill="#ffd700" />
<path d="M59 58 Q67 65 75 58" fill="none" stroke="#ffd700" stroke-width="2" />
<!-- 天平顶装饰 -->
<circle cx="50" cy="38" r="5" fill="#c41e3a" />
<polygon points="50,30 52,36 58,36 53,40 55,46 50,42 45,46 47,40 42,36 48,36" fill="#ffd700" />
<!-- 书本(放在天平左侧) -->
<rect x="8" y="48" width="10" height="22" rx="1" fill="#c41e3a" stroke="#ffd700" stroke-width="1.5" />
<line x1="13" y1="52" x2="13" y2="68" stroke="#ffd700" stroke-width="0.5" />
<line x1="9" y1="56" x2="17" y2="56" stroke="#ffd700" stroke-width="0.5" />
<line x1="9" y1="62" x2="17" y2="62" stroke="#ffd700" stroke-width="0.5" />
</svg>
<div class="app-loading-ring"></div>
</div>
<div class="app-loading-progress">
<div class="app-loading-progress-bar"></div>
</div>
<div class="app-loading-text">正在加载系统中...</div>
</div>
</div>
</div>

View File

@ -109,6 +109,7 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.22.0",
"lint-staged": "^15.2.2",

View File

@ -5,6 +5,9 @@ export interface ConsumptionPageParams {
pageNo: number
pageSize: number
prisonerNo?: string
prisonerName?: string // 罪犯姓名
prisonAreaId?: number // 监区ID
prisonCellId?: number // 监室ID
type?: number
status?: number
totalAmount?: number
@ -26,6 +29,11 @@ export interface Consumption {
id: number // 订单ID
prisonerId?: number // 罪犯ID
prisonerNo?: string // 罪犯编号
prisonerName?: string // 罪犯姓名
prisonAreaId?: number // 监区ID
prisonAreaName?: string // 监区名称
prisonCellId?: number // 监室ID
prisonCellName?: string // 监室名称
orderNo?: string // 订单号
type?: number // 类型1-购物 2-餐饮 3-医疗 4-通讯 5-其他
totalAmount?: number // 订单总金额
@ -34,6 +42,7 @@ export interface Consumption {
status?: number // 状态1-成功 2-失败
remark: string // 备注
details?: ConsumptionDetail[] // 消费明细列表
createTime?: Date // 创建时间
}
// 消费订单 API

View File

@ -24,7 +24,7 @@ export interface ChartDataVO {
/** 省份数据 */
export interface ProvinceChartVO {
province: string // 省份名称
provinceCode: number // 省份编码
provinceCode: string | number // 省份编码(支持字符串名称或数字编码)
count: number // 人数
}

View File

@ -1,10 +1,19 @@
import request from '@/config/axios'
// 性别枚举0-未知 1-男 2-女
export const GENDER_ENUM = {
UNKNOWN: 0,
MALE: 1,
FEMALE: 2
} as const
export type GenderType = typeof GENDER_ENUM[keyof typeof GENDER_ENUM]
export interface PrisonerVO {
id: number
prisonerNo: string
name: string
gender: number
gender: number // 性别0-未知 1-男 2-女
genderName?: string // 性别名称
birthday: string
idCard: string
@ -47,7 +56,7 @@ export interface PrisonerCreateVO {
id?: number
prisonerNo: string
name: string
gender: number
gender: number // 性别0-未知 1-男 2-女
birthday: string
idCard: string
ethnicity: string
@ -74,39 +83,48 @@ export interface PrisonerCreateVO {
remark: string
}
// 服刑人员分页查询
export const getPrisonerPage = (params: PageParam) => {
return request.get({ url: '/prison/prisoner/page', params })
}
// 服刑人员详情
export const getPrisoner = (id: number) => {
return request.get({ url: '/prison/prisoner/get?id=' + id })
}
// 新增服刑人员
export const createPrisoner = (data: PrisonerCreateVO) => {
return request.post({ url: '/prison/prisoner/create', data })
}
// 修改服刑人员
export const updatePrisoner = (data: PrisonerCreateVO) => {
return request.put({ url: '/prison/prisoner/update', data })
}
// 删除服刑人员
export const deletePrisoner = (id: number) => {
return request.delete({ url: '/prison/prisoner/delete?id=' + id })
}
// 批量删除服刑人员
export const deletePrisonerList = (ids: number[]) => {
return request.delete({ url: '/prison/prisoner/delete-list', params: { ids: ids.join(',') } })
}
// 导出服刑人员
export const exportPrisoner = (params) => {
return request.download({ url: '/prison/prisoner/export-excel', params })
// PrisonerApi 对象 - 统一使用对象导出模式
export const PrisonerApi = {
// 分页查询
getPage: (params: PageParam) => {
return request.get({ url: '/prison/prisoner/page', params })
},
// 获取详情
get: (id: number) => {
return request.get({ url: '/prison/prisoner/get', params: { id } })
},
// 创建
create: (data: PrisonerCreateVO) => {
return request.post({ url: '/prison/prisoner/create', data })
},
// 更新
update: (data: PrisonerCreateVO) => {
return request.put({ url: '/prison/prisoner/update', data })
},
// 删除
delete: (id: number) => {
return request.delete({ url: '/prison/prisoner/delete', params: { id } })
},
// 批量删除
deleteList: (ids: number[]) => {
return request.delete({ url: '/prison/prisoner/delete-list', params: { ids: ids.join(',') } })
},
// 导出
export: (params: PageParam) => {
return request.download({ url: '/prison/prisoner/export-excel', params })
},
// 调监
doTransfer: (data: TransferReqVO) => {
return request.post({ url: '/prison/prisoner/transfer', params: data })
},
// 获取位置历史
getAreaHistory: (prisonerId: number) => {
return request.get({ url: '/prison/prisoner-area-log/list-by-prisoner-id', params: { prisonerId } })
},
// 导入
import: (data: FormData) => {
return request.upload({ url: '/prison/prisoner/import-excel', data })
}
}
// 调监请求
@ -116,11 +134,6 @@ export interface TransferReqVO {
reason?: string
}
// 执行调监
export const doTransfer = (data: TransferReqVO) => {
return request.post({ url: '/prison/prisoner/transfer', params: data })
}
// 罪犯位置历史记录
export interface PrisonerAreaLogVO {
id: number
@ -142,27 +155,3 @@ export interface PrisonerAreaLogVO {
operatorName: string
createTime: string
}
// 获取罪犯位置历史
export const getPrisonerAreaHistory = (prisonerId: number) => {
return request.get({ url: '/prison/prisoner-area-log/list-by-prisoner-id', params: { prisonerId } })
}
// 导入服刑人员
export const importPrisoner = (data: FormData) => {
return request.upload({ url: '/prison/prisoner/import-excel', data })
}
// PrisonerApi 对象 - 用于组件导入
export const PrisonerApi = {
getPage: getPrisonerPage,
get: getPrisoner,
create: createPrisoner,
update: updatePrisoner,
delete: deletePrisoner,
deleteList: deletePrisonerList,
export: exportPrisoner,
doTransfer: doTransfer,
getAreaHistory: getPrisonerAreaHistory,
import: importPrisoner
}

View File

@ -3,20 +3,19 @@ import request from '@/config/axios'
/** 问卷问题信息 */
export interface Question {
id?: number // 问题ID创建时不需要
questionnaireId?: number // 所属问卷ID
title?: string // 问题标题
type?: number // 问题类型1-单选 2-多选 3-填空 4-评分 5-日期 6-数字
options?: string // 选项JSON
questionnaireId: number // 所属问卷ID
title: string // 问题标题
type: number // 问题类型1-单选 2-多选 3-填空 4-评分 5-日期 6-数字
options?: string // 选项JSON(单选/多选时使用)
score?: number // 分值
sort?: number // 排序
isRequired?: boolean // 是否必答
// 新增字段
partName?: string // 分区名称
partSort?: number // 分区排序
helpText?: string // 帮助说明
helpText?: string // 帮助说明文字
placeholder?: string // 占位提示
defaultValue?: string // 默认值
autoFillType?: string // 自动填充类型NONE/AUTO/MANUAL
autoFillType?: string // 自动填充类型NONE-无 AUTO-自动填充 MANUAL-手动输入
autoFillSource?: string // 自动填充来源
displayCondition?: string // 显示条件JSON
minValue?: number // 最小值
@ -28,23 +27,20 @@ export interface Question {
export interface QuestionPageParams {
pageNo: number
pageSize: number
questionnaireId?: number
title?: string
type?: number
partName?: string
}
/** 批量更新参数 */
export interface BatchUpdateQuestion {
id: number
partName?: string
partSort?: number
sort?: number
questionnaireId?: number // 所属问卷ID
title?: string // 问题标题
type?: number // 问题类型
partName?: string // 分区名称
}
/** 批量更新请求 */
export interface BatchUpdateReq {
questions: BatchUpdateQuestion[]
export interface QuestionBatchUpdateReq {
questions: Array<{
id: number
partName?: string
partSort?: number
sort?: number
}>
}
// 问卷问题 API
@ -79,13 +75,13 @@ export const QuestionApi = {
return await request.delete<boolean>({ url: `/prison/question/delete-list`, params: { ids: ids.join(',') } })
},
// 批量更新问卷问题
batchUpdate: async (data: QuestionBatchUpdateReq) => {
return await request.put<boolean>({ url: `/prison/question/batch-update`, data })
},
// 导出问卷问题 Excel
exportQuestion: async (params: QuestionPageParams) => {
return await request.download({ url: `/prison/question/export-excel`, params })
},
// 批量更新问卷问题(仅排序和分区字段)
batchUpdate: async (data: BatchUpdateReq) => {
return await request.post<boolean>({ url: `/prison/question/batch-update`, data })
}
}

View File

@ -27,18 +27,20 @@ export interface QuestionnairePageParams {
title?: string
type?: number
status?: number
description?: string
coverImage?: string
instruction?: string
estimatedTime?: number
partCount?: number
allowAnonymous?: boolean
createTime?: string
}
// 问卷模板 API
export const QuestionnaireApi = {
// 查询问卷模板分页
getQuestionnairePage: async (params: QuestionnairePageParams) => {
return await request.get({ url: `/prison/questionnaire/page`, params })
return await request.get<{ list: Questionnaire[]; total: number }>({ url: `/prison/questionnaire/page`, params })
},
// 查询问卷模板详情

View File

@ -19,7 +19,7 @@ export interface QuestionnaireRecord {
totalScore?: number // 总分
passScore?: number // 及格分数
passStatus?: number // 及格状态1-及格 2-不及格 3-待评阅
riskLevel?: number // 风险等级1-高风险 2-中风险 3-低风险
riskLevel?: number // 风险等级1-低风险 2-中风险 3-高风险 4-极高风险
evaluatorId?: number // 评阅人ID
evaluatorName?: string // 评阅人姓名
evaluateTime?: string // 评阅时间
@ -73,7 +73,7 @@ export interface AssessmentManualScoreReq {
recordId: number // 测评记录ID
subjectiveScore: number // 主观题得分
comment?: string // 评语
riskLevel?: number // 风险等级1-高风险 2-中风险 3-低风险
riskLevel?: number // 风险等级1-低风险 2-中风险 3-高风险 4-极高风险
}
/** 分数分布数据 */
@ -87,6 +87,7 @@ export interface ScoreDistribution {
/** 风险分布数据 */
export interface RiskDistribution {
extreme?: number // 极高风险
high?: number
medium?: number
low?: number

View File

@ -0,0 +1,107 @@
import request from '@/config/axios'
// ============ 快捷评语相关类型 ============
/** 快捷评语分类 */
export interface CommentCategory {
id: number
name: string
type: number // 评估类型1-入监 2-定期 3-出监 4-减刑 5-专项
sort: number
status: number
}
/** 快捷评语分页参数 */
export interface QuickCommentPageParams {
pageNo: number
pageSize: number
categoryId?: number
content?: string
status?: number
}
/** 快捷评语 */
export interface QuickComment {
id: number
categoryId: number
categoryName?: string // 分类名称
content: string // 评语内容
usageCount: number // 使用次数
sort: number
status: number // 0-停用 1-启用
creator?: string
createTime?: string
}
// ============ 快捷评语分类 API ============
export const CommentCategoryApi = {
// 查询分类列表
getList: async (params?: { type?: number; status?: number }) => {
return await request.get({ url: '/prison/quick-comment/category/list', params })
},
// 查询分类详情
getCategory: async (id: number) => {
return await request.get({ url: '/prison/quick-comment/category/get?id=' + id })
},
// 新增分类
createCategory: async (data: Partial<CommentCategory>) => {
return await request.post({ url: '/prison/quick-comment/category/create', data })
},
// 修改分类
updateCategory: async (data: Partial<CommentCategory>) => {
return await request.put({ url: '/prison/quick-comment/category/update', data })
},
// 删除分类
deleteCategory: async (id: number) => {
return await request.delete({ url: '/prison/quick-comment/category/delete?id=' + id })
}
}
// ============ 快捷评语 API ============
export const QuickCommentApi = {
// 查询评语分页
getPage: async (params: QuickCommentPageParams) => {
return await request.get({ url: '/prison/quick-comment/page', params })
},
// 查询评语详情
get: async (id: number) => {
return await request.get({ url: '/prison/quick-comment/get?id=' + id })
},
// 新增评语
create: async (data: QuickComment) => {
return await request.post({ url: '/prison/quick-comment/create', data })
},
// 修改评语
update: async (data: QuickComment) => {
return await request.put({ url: '/prison/quick-comment/update', data })
},
// 删除评语
delete: async (id: number) => {
return await request.delete({ url: '/prison/quick-comment/delete?id=' + id })
},
// 批量删除评语
deleteList: async (ids: number[]) => {
return await request.delete({ url: '/prison/quick-comment/delete-list?ids=' + ids.join(',') })
},
// 导入评语
importComments: async (data: { categoryId: number; contents: string[] }) => {
return await request.post({ url: '/prison/quick-comment/import', data })
},
// 导出评语
export: async (params: QuickCommentPageParams) => {
return await request.download({ url: '/prison/quick-comment/export', params })
}
}

View File

@ -0,0 +1,386 @@
import request from '@/config/axios'
// ============ 评估报告模板相关类型 ============
/** 评估报告模板分页参数 */
export interface ReportTemplatePageParams {
pageNo: number
pageSize: number
name?: string
type?: number
status?: number
}
/** 评估维度配置 */
export interface ReportDimension {
id?: number
name: string // 维度名称
aiPrompt?: string // AI提示词
dataSources: string[] // 数据源绑定
outputFormat: string // 输出格式text/paragraph/list
enableAi: boolean // 是否AI生成
editorType: string // 编辑器类型text/richtext/select
sort: number // 排序
}
/** 评估报告模板 */
export interface ReportTemplate {
id: number
name: string // 模板名称
type: number // 模板类型1-入监综合评估 2-定期考核报告 3-出监评估 4-减刑假释建议 5-专项评估
titleFormat: string // 报告标题格式
dimensions: ReportDimension[] // 评估维度
aiPromptConfig?: string // AI提示词配置JSON
styleConfig?: string // 样式配置JSON
status: number // 状态0-停用 1-启用
isDefault: boolean // 是否默认
version: number // 版本号
remark?: string // 备注
creator?: string
createTime?: string
}
// ============ 评估报告相关类型 ============
/** 报告分页参数 */
export interface ReportPageParams {
pageNo: number
pageSize: number
reportNo?: string
prisonerNo?: string
templateId?: number
status?: number
reportDate?: string[]
}
/** 报告维度内容 */
export interface ReportDimensionContent {
dimensionId: number
dimensionName: string
content: string // 内容
dataSources?: string // 数据源JSON
isAiGenerated: boolean // 是否AI生成
aiGenerateTime?: string // AI生成时间
lastModifyTime?: string // 最后修改时间
lastModifyBy?: string // 最后修改人
}
/** 评估报告 */
export interface Report {
id: number
reportNo: string // 报告编号
prisonerId: number // 罪犯ID
prisonerNo: string // 罪犯编号
prisonerName?: string // 罪犯姓名
templateId: number // 模板ID
templateName?: string // 模板名称
title: string // 报告标题
reportDate: string // 报告日期
dimensions: ReportDimensionContent[] // 维度内容
conclusion?: string // 综合结论
suggestions?: string // 改造建议
riskLevel?: number // 风险等级1-低风险 2-中风险 3-高风险 4-极高风险
attachments?: string[] // 附件列表
status: number // 状态1-草稿 2-待审核 3-已通过 4-已退回
version: number // 版本号
signature?: string // 数字签名
fingerprint?: string // 报告指纹
submitterId?: number // 提交人ID
submitterName?: string // 提交人姓名
submitTime?: string // 提交时间
reviewerId?: number // 审核人ID
reviewerName?: string // 审核人姓名
reviewTime?: string // 审核时间
reviewComment?: string // 审核意见
remark?: string // 备注
creator?: string
createTime?: string
}
// ============ 快捷评语相关类型 ============
/** 快捷评语分类 */
export interface CommentCategory {
id: number
name: string
type: number // 评估类型
sort: number
status: number
}
/** 快捷评语 */
export interface QuickComment {
id: number
categoryId: number
categoryName?: string // 分类名称
content: string
usageCount: number // 使用次数
sort: number
status: number
}
// ============ 报告版本历史 ============
/** 报告版本历史 */
export interface ReportVersion {
id: number
reportId: number
version: number
content: string // 内容快照JSON
modifierId: number
modifierName: string
modifyTime: string
comment?: string // 版本备注
}
// ============ 评估报告模板 API ============
export const ReportTemplateApi = {
// 查询模板分页
getTemplatePage: async (params: ReportTemplatePageParams) => {
return await request.get({ url: '/prison/report-template/page', params })
},
// 查询模板详情
getTemplate: async (id: number) => {
return await request.get({ url: '/prison/report-template/get?id=' + id })
},
// 新增模板
createTemplate: async (data: ReportTemplate) => {
return await request.post({ url: '/prison/report-template/create', data })
},
// 修改模板
updateTemplate: async (data: ReportTemplate) => {
return await request.put({ url: '/prison/report-template/update', data })
},
// 删除模板
deleteTemplate: async (id: number) => {
return await request.delete({ url: '/prison/report-template/delete?id=' + id })
},
// 批量删除模板
deleteTemplateList: async (ids: number[]) => {
return await request.delete({ url: '/prison/report-template/delete-list?ids=' + ids.join(',') })
},
// 复制模板
copyTemplate: async (id: number) => {
return await request.post({ url: '/prison/report-template/copy?id=' + id })
},
// 启用/停用模板
updateStatus: async (id: number, status: number) => {
return await request.put({ url: '/prison/report-template/update-status', params: { id, status } })
},
// 设为默认
setDefault: async (id: number) => {
return await request.put({ url: '/prison/report-template/set-default?id=' + id })
},
// 导出模板
exportTemplate: async (params: ReportTemplatePageParams) => {
return await request.download({ url: '/prison/report-template/export-excel', params })
}
}
// ============ 评估报告 API ============
export const ReportApi = {
// 查询报告分页
getReportPage: async (params: ReportPageParams) => {
return await request.get({ url: '/prison/report/page', params })
},
// 查询报告详情
getReport: async (id: number) => {
return await request.get({ url: '/prison/report/get?id=' + id })
},
// 根据报告编号查询
getReportByNo: async (reportNo: string) => {
return await request.get({ url: '/prison/report/get-by-no?reportNo=' + reportNo })
},
// 新增报告
createReport: async (data: Report) => {
return await request.post({ url: '/prison/report/create', data })
},
// 修改报告
updateReport: async (data: Report) => {
return await request.put({ url: '/prison/report/update', data })
},
// 删除报告
deleteReport: async (id: number) => {
return await request.delete({ url: '/prison/report/delete?id=' + id })
},
// 批量删除报告
deleteReportList: async (ids: number[]) => {
return await request.delete({ url: '/prison/report/delete-list?ids=' + ids.join(',') })
},
// 提交审核
submitReport: async (id: number) => {
return await request.post({ url: '/prison/report/submit?id=' + id })
},
// 审核通过
approveReport: async (id: number, comment?: string) => {
return await request.post({ url: '/prison/report/approve', data: { id, comment } })
},
// 审核退回
rejectReport: async (id: number, comment: string) => {
return await request.post({ url: '/prison/report/reject', data: { id, comment } })
},
// AI生成报告
generateReportByAi: async (id: number, dimensionIds?: number[]) => {
return await request.post({
url: '/prison/report/generate-by-ai',
data: { id, dimensionIds }
})
},
// 批量生成报告
batchGenerateReports: async (data: { templateId: number; prisonerIds: number[] }) => {
return await request.post({ url: '/prison/report/batch-generate', data })
},
// 获取AI生成进度
getGenerateProgress: async (taskId: string) => {
return await request.get({ url: '/prison/report/generate-progress?taskId=' + taskId })
},
// 验证报告签名
verifySignature: async (id: number) => {
return await request.get({ url: '/prison/report/verify-signature?id=' + id })
},
// 导出报告
exportReport: async (id: number, format: 'pdf' | 'word') => {
return await request.download({ url: '/prison/report/export', params: { id, format } })
},
// 批量导出报告
batchExportReports: async (ids: number[], format: 'pdf' | 'word') => {
return await request.download({ url: '/prison/report/batch-export', params: { ids: ids.join(','), format } })
},
// 归档报告
archiveReport: async (id: number) => {
return await request.post({ url: '/prison/report/archive?id=' + id })
},
// 导出报告 Excel
exportReportExcel: async (params: ReportPageParams) => {
return await request.download({ url: '/prison/report/export-excel', params })
}
}
// ============ 快捷评语 API ============
export const QuickCommentApi = {
// 查询评语分类列表
getCategoryList: async (params: { type?: number; status?: number }) => {
return await request.get({ url: '/prison/quick-comment/category/list', params })
},
// 查询评语分页
getCommentPage: async (params: { pageNo: number; pageSize: number; categoryId?: number; keyword?: string }) => {
return await request.get({ url: '/prison/quick-comment/page', params })
},
// 新增评语
createComment: async (data: QuickComment) => {
return await request.post({ url: '/prison/quick-comment/create', data })
},
// 修改评语
updateComment: async (data: QuickComment) => {
return await request.put({ url: '/prison/quick-comment/update', data })
},
// 删除评语
deleteComment: async (id: number) => {
return await request.delete({ url: '/prison/quick-comment/delete?id=' + id })
},
// 批量删除评语
deleteCommentList: async (ids: number[]) => {
return await request.delete({ url: '/prison/quick-comment/delete-list?ids=' + ids.join(',') })
},
// 导入评语
importComments: async (data: { categoryId: number; comments: string[] }) => {
return await request.post({ url: '/prison/quick-comment/import', data })
},
// 导出评语
exportComments: async (categoryId: number) => {
return await request.download({ url: '/prison/quick-comment/export', params: { categoryId } })
}
}
// ============ 报告版本历史 API ============
export const ReportVersionApi = {
// 查询版本历史
getVersionList: async (reportId: number) => {
return await request.get({ url: '/prison/report-version/list?reportId=' + reportId })
},
// 获取版本详情
getVersion: async (id: number) => {
return await request.get({ url: '/prison/report-version/get?id=' + id })
},
// 恢复版本
restoreVersion: async (id: number) => {
return await request.post({ url: '/prison/report-version/restore?id=' + id })
},
// 对比版本
compareVersions: async (versionId1: number, versionId2: number) => {
return await request.get({ url: '/prison/report-version/compare', params: { versionId1, versionId2 } })
}
}
// ============ 罪犯选择相关 API ============
/** 罪犯简要信息 */
export interface PrisonerBrief {
id: number
prisonerNo: string
name: string
areaId: number
areaName: string
riskLevel?: number // 风险等级
}
/** 罪犯分页参数 */
export interface PrisonerPageParams {
pageNo: number
pageSize: number
name?: string
prisonerNo?: string
areaId?: number
}
export const PrisonerSelectApi = {
// 查询罪犯简要信息分页
getPrisonerPage: async (params: PrisonerPageParams) => {
return await request.get({ url: '/prison/prisoner/brief/page', params })
},
// 查询所有罪犯简要信息(用于选择列表)
getAllPrisoners: async (params: { name?: string; prisonerNo?: string }) => {
return await request.get({ url: '/prison/prisoner/brief/list', params })
}
}

View File

@ -0,0 +1,123 @@
import request from '@/config/axios'
/** 风险评估信息 */
export interface Risk {
id: number // 评估ID
prisonerId?: number // 罪犯ID
prisonerCode?: string // 罪犯编号
prisonerName?: string // 罪犯姓名
assessmentType?: number // 评估类型1-入监评估 2-定期评估 3-专项评估 4-出监评估
assessmentDate?: Date // 评估日期
assessMethod?: number // 评估方式1-量表评估 2-民警评估 3-综合评估
overallScore?: number // 综合风险得分
riskLevel?: number // 风险等级1-低风险 2-中风险 3-高风险 4-极高风险
mentalState?: number // 精神状态1-正常 2-异常
escapeRisk?: number // 脱逃风险1-低 2-中 3-高
violenceRisk?: number // 暴力倾向1-低 2-中 3-高
revoltRisk?: number // 抗改风险1-低 2-中 3-高
selfHarmRisk?: number // 自杀自伤1-低 2-中 3-高
recommendation?: string // 建议
assessor?: string // 评估人
conclusion?: string // 结论
itemScores?: string // 项目得分JSON
remark?: string // 备注
createTime?: Date // 创建时间
}
// 风险评估创建/更新请求
export interface RiskSaveReqVO {
id?: number
prisonerId?: number
prisonerCode?: string
prisonerName?: string
assessmentType?: number
assessmentDate?: string
assessMethod?: number
overallScore?: number
riskLevel?: number
mentalState?: number
escapeRisk?: number
violenceRisk?: number
revoltRisk?: number
selfHarmRisk?: number
recommendation?: string
assessor?: string
conclusion?: string
itemScores?: string
remark?: string
}
// 风险评估分页查询
export interface RiskPageReqVO {
pageNo: number
pageSize: number
prisonerId?: number
prisonerCode?: string // 罪犯编号
prisonerName?: string // 罪犯姓名
assessmentType?: number
riskLevel?: number
assessor?: string
assessmentDate?: string
}
// 风险评估详情响应
export interface RiskRespVO {
id: number
prisonerId: number
prisonerCode: string // 罪犯编号
prisonerName: string // 罪犯姓名
assessmentType: number
assessmentDate: string
assessMethod: number
overallScore: number
riskLevel: number
mentalState: string
escapeRisk: string
violenceRisk: string
revoltRisk: string
selfHarmRisk: string
recommendation: string
assessor: string
conclusion: string
itemScores: string
remark: string
createTime: string
}
/** 风险评估 API */
export const RiskApi = {
// 查询风险评估分页
getRiskPage: async (params: RiskPageReqVO) => {
return await request.get({ url: `/prison/risk/page`, params })
},
// 查询风险评估详情
getRisk: async (id: number) => {
return await request.get({ url: `/prison/risk/get?id=` + id })
},
// 新增风险评估
createRisk: async (data: RiskSaveReqVO) => {
return await request.post({ url: `/prison/risk/create`, data })
},
// 修改风险评估
updateRisk: async (data: RiskSaveReqVO) => {
return await request.put({ url: `/prison/risk/update`, data })
},
// 删除风险评估
deleteRisk: async (id: number) => {
return await request.delete({ url: `/prison/risk/delete?id=` + id })
},
/** 批量删除风险评估 */
deleteRiskList: async (ids: number[]) => {
return await request.delete({ url: `/prison/risk/delete-list?ids=${ids.join(',')}` })
},
// 导出风险评估 Excel
exportRisk: async (params) => {
return await request.download({ url: `/prison/risk/export-excel`, params })
}
}

View File

@ -4,7 +4,9 @@ import request from '@/config/axios'
export interface RiskAssessmentPageParams {
pageNo: number
pageSize: number
prisonerId?: number
prisonerNo?: string
prisonerName?: string
assessmentType?: number
riskLevel?: number
status?: number
@ -15,6 +17,7 @@ export interface RiskAssessment {
id: number // 评估ID
prisonerId?: number // 罪犯ID
prisonerNo?: string // 罪犯编号
prisonerName?: string // 罪犯姓名
assessmentType?: number // 评估类型1-入狱评估 2-定期评估 3-专项评估
assessmentDate?: string // 评估日期
violenceScore: number // 暴力倾向得分

View File

@ -5,6 +5,9 @@ export interface ScorePageParams {
pageNo: number
pageSize: number
prisonerNo?: string
prisonerName?: string // 罪犯姓名
prisonAreaId?: number // 监区ID
prisonCellId?: number // 监室ID
year?: number
month?: number
level?: number
@ -16,6 +19,11 @@ export interface Score {
id: number // 记录ID
prisonerId?: number // 罪犯ID
prisonerNo?: string // 罪犯编号
prisonerName?: string // 罪犯姓名
prisonAreaId?: number // 监区ID
prisonAreaName?: string // 监区名称
prisonCellId?: number // 监室ID
prisonCellName?: string // 监室名称
year?: number // 考核年份
month?: number // 考核月份
baseScore: number // 基础分
@ -27,6 +35,8 @@ export interface Score {
assessorName: string // 考核人姓名
status?: number // 状态1-待审核 2-已通过 3-已驳回
remark: string // 备注
createTime?: Date // 创建时间
updateTime?: Date // 更新时间
}
// 计分考核 API

View File

@ -0,0 +1,119 @@
import request from '@/config/axios'
import { AreaApi } from '@/api/prison/area'
/** 狱情收集信息 */
export interface Situation {
id: number // 狱情ID
title?: string // 标题
content?: string // 详情内容
category?: number // 分类1-监管安全 2-生产安全 3-生活卫生 4-教育改造 5-民警队伍 6-其他
level?: number // 等级1-一般 2-重要 3-紧急
source?: number // 来源1-现场发现 2-耳目反映 3-技术监控 4-他人举报 5-主动报告 6-其他
status?: number // 状态1-待处理 2-处理中 3-已处理 4-已归档
areaId?: number // 监区ID
cellId?: number // 监室ID
reporter?: string // 报告人
handler?: string // 处理人
occurTime?: Date // 发生时间
remark?: string // 备注
createTime?: Date // 创建时间
}
// 狱情收集创建/更新请求
export interface SituationSaveReqVO {
id?: number
title: string
content?: string
category?: number
level?: number
source?: number
status?: number
areaId?: number
cellId?: number
reporter?: string
handler?: string
occurTime?: string
remark?: string
}
// 狱情收集分页查询
export interface SituationPageReqVO {
pageNo: number
pageSize: number
title?: string
category?: number
level?: number
source?: number
status?: number
areaId?: number
cellId?: number
reporter?: string
handler?: string
occurTime?: string
}
// 狱情收集详情响应
export interface SituationRespVO {
id: number
title: string
content: string
category: number
level: number
source: number
status: number
areaId: number
cellId: number
reporter: string
handler: string
occurTime: string
remark: string
createTime: string
areaName?: string // 监区名称
cellName?: string // 监室名称
}
/** 狱情收集 API */
export const SituationApi = {
// 查询狱情收集分页
getSituationPage: async (params: SituationPageReqVO) => {
return await request.get({ url: `/prison/situation/page`, params })
},
// 查询狱情收集详情
getSituation: async (id: number) => {
return await request.get({ url: `/prison/situation/get?id=` + id })
},
// 查询监室列表(用于新增/编辑时选择)
getCellList: async (params?: { areaId?: number; status?: number }) => {
return await request.get({ url: `/prison/cell/list`, params })
},
// 新增狱情收集
createSituation: async (data: SituationSaveReqVO) => {
return await request.post({ url: `/prison/situation/create`, data })
},
// 修改狱情收集
updateSituation: async (data: SituationSaveReqVO) => {
return await request.put({ url: `/prison/situation/update`, data })
},
// 删除狱情收集
deleteSituation: async (id: number) => {
return await request.delete({ url: `/prison/situation/delete?id=` + id })
},
/** 批量删除狱情收集 */
deleteSituationList: async (ids: number[]) => {
return await request.delete({ url: `/prison/situation/delete-list?ids=${ids.join(',')}` })
},
// 导出狱情收集 Excel
exportSituation: async (params) => {
return await request.download({ url: `/prison/situation/export-excel`, params })
},
// 导出 AreaApi 供页面使用
AreaApi
}

View File

@ -0,0 +1,174 @@
import request from '@/config/axios'
import { AreaApi } from '@/api/prison/area'
/** 预警管理 */
export interface Warning {
id: number // 预警ID
title?: string // 标题
content?: string // 内容
type?: number // 类型
level?: number // 等级
status?: number // 状态
source?: number // 来源
situationId?: number // 关联狱情ID
areaId?: number // 监区ID
cellId?: number // 监室ID
alertTime?: Date // 预警时间
verifyTime?: Date // 核实时间
verifier?: string // 核实人
verifyResult?: string // 核实结果
handleTime?: Date // 处置时间
handler?: string // 处置人
handleResult?: string // 处置结果
releaseTime?: Date // 解除时间
releaser?: string // 解除人
releaseReason?: string // 解除原因
occurTime?: Date // 发生时间
remark?: string // 备注
createTime?: Date // 创建时间
}
// 预警创建/更新请求
export interface WarningSaveReqVO {
id?: number
title: string
content?: string
type?: number
level?: number
status?: number
source?: number
situationId?: number
areaId?: number
cellId?: number
alertTime?: string
occurTime?: string
remark?: string
}
// 预警核实请求
export interface WarningVerifyReqVO {
id: number
verifyResult: string
}
// 预警处置请求
export interface WarningHandleReqVO {
id: number
handleResult: string
}
// 预警解除请求
export interface WarningReleaseReqVO {
id: number
releaseReason: string
}
// 预警分页查询
export interface WarningPageReqVO {
pageNo: number
pageSize: number
title?: string
type?: number
level?: number
status?: number
source?: number
areaId?: number
cellId?: number
occurTime?: string
}
// 预警详情响应
export interface WarningRespVO {
id: number
title: string
content: string
type: number
level: number
status: number
source: number
situationId: number
areaId: number
cellId: number
alertTime: string
verifyTime: string
verifier: string
verifyResult: string
handleTime: string
handler: string
handleResult: string
releaseTime: string
releaser: string
releaseReason: string
occurTime: string
remark: string
createTime: string
areaName?: string // 监区名称
cellName?: string // 监室名称
situationTitle?: string // 关联狱情标题
}
/** 预警管理 API */
export const WarningApi = {
// 查询预警分页
getWarningPage: async (params: WarningPageReqVO) => {
return await request.get({ url: `/prison/warning/page`, params })
},
// 查询预警详情
getWarning: async (id: number) => {
return await request.get({ url: `/prison/warning/get?id=` + id })
},
// 查询监室列表
getCellList: async (params?: { areaId?: number; status?: number }) => {
return await request.get({ url: `/prison/cell/list`, params })
},
// 查询狱情列表
getSituationList: async (params?: { status?: number }) => {
return await request.get({ url: `/prison/situation/list`, params })
},
// 新增预警
createWarning: async (data: WarningSaveReqVO) => {
return await request.post({ url: `/prison/warning/create`, data })
},
// 修改预警
updateWarning: async (data: WarningSaveReqVO) => {
return await request.put({ url: `/prison/warning/update`, data })
},
// 预警核实
verifyWarning: async (data: WarningVerifyReqVO) => {
return await request.put({ url: `/prison/warning/verify`, data })
},
// 预警处置
handleWarning: async (data: WarningHandleReqVO) => {
return await request.put({ url: `/prison/warning/handle`, data })
},
// 预警解除
releaseWarning: async (data: WarningReleaseReqVO) => {
return await request.put({ url: `/prison/warning/release`, data })
},
// 删除预警
deleteWarning: async (id: number) => {
return await request.delete({ url: `/prison/warning/delete?id=` + id })
},
/** 批量删除预警 */
deleteWarningList: async (ids: number[]) => {
return await request.delete({ url: `/prison/warning/delete-list?ids=${ids.join(',')}` })
},
// 导出预警 Excel
exportWarning: async (params) => {
return await request.download({ url: `/prison/warning/export-excel`, params })
},
// 导出 AreaApi 供页面使用
AreaApi
}

View File

@ -0,0 +1,41 @@
<template>
<svg :width="size" :height="size" :viewBox="viewBox || '0 0 100 100'" xmlns="http://www.w3.org/2000/svg">
<!-- 天平底座 -->
<rect x="35" y="75" width="30" height="6" rx="2" :fill="mainColor" />
<rect x="48" y="45" width="4" height="30" :fill="accentColor" />
<!-- 天平横杆 -->
<rect x="10" y="42" width="80" height="5" rx="2" :fill="accentColor" />
<!-- 天平左边盘子往中间移 -->
<path d="M28 47 L38 47 L35 58 L31 58 Z" :fill="accentColor" />
<path d="M25 58 Q33 65 41 58" fill="none" :stroke="accentColor" stroke-width="2" />
<!-- 天平右边盘子 -->
<path d="M62 47 L72 47 L69 58 L65 58 Z" :fill="accentColor" />
<path d="M59 58 Q67 65 75 58" fill="none" :stroke="accentColor" stroke-width="2" />
<!-- 天平顶装饰 -->
<circle cx="50" cy="38" r="5" :fill="mainColor" />
<polygon points="50,30 52,36 58,36 53,40 55,46 50,42 45,46 47,40 42,36 48,36" :fill="accentColor" />
<!-- 书本放在天平左侧 -->
<rect x="8" y="48" width="10" height="22" rx="1" :fill="mainColor" :stroke="accentColor" stroke-width="1.5" />
<line x1="13" y1="52" x2="13" y2="68" :stroke="accentColor" stroke-width="0.5" />
<line x1="9" y1="56" x2="17" y2="56" :stroke="accentColor" stroke-width="0.5" />
<line x1="9" y1="62" x2="17" y2="62" :stroke="accentColor" stroke-width="0.5" />
</svg>
</template>
<script lang="ts" setup>
defineOptions({ name: 'JusticeIcon' })
interface Props {
size?: number | string
viewBox?: string
mainColor?: string
accentColor?: string
}
withDefaults(defineProps<Props>(), {
size: 80,
viewBox: '0 0 100 100',
mainColor: '#c41e3a', //
accentColor: '#ffd700' //
})
</script>

View File

@ -1,6 +1,8 @@
import type { App } from 'vue'
import { Icon } from './Icon'
import JusticeIcon from './Icon/JusticeIcon.vue'
export const setupGlobCom = (app: App<Element>): void => {
app.component('Icon', Icon)
app.component('JusticeIcon', JusticeIcon)
}

View File

@ -768,6 +768,32 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true,
canTo: true
}
},
{
path: 'situation-platform',
component: () => import('@/views/prison/situation/index.vue'),
name: 'PrisonSituationPlatform',
meta: {
title: '狱情收集',
icon: 'ep:warning',
permission: 'prison:situation:query',
noCache: false,
hidden: true,
canTo: true
}
},
{
path: 'report/edit',
component: () => import('@/views/prison/report/edit/index.vue'),
name: 'PrisonReportEdit',
meta: {
title: '评估报告编辑',
icon: 'ep:document-checked',
permission: 'prison:report:update',
noCache: true,
hidden: true,
canTo: true
}
}
]
}

View File

@ -1,10 +1,13 @@
/**
*
*/
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useDictStoreWithOut as _useDictStoreWithOut } from '@/store/modules/dict'
import { ElementPlusInfoType } from '@/types/elementPlus'
const dictStore = useDictStoreWithOut()
const dictStore = _useDictStoreWithOut()
// Re-export for convenience
export const useDictStoreWithOut = _useDictStoreWithOut
/**
* dictType
@ -276,5 +279,10 @@ export enum DICT_TYPE {
PRISON_CERTIFICATE_TYPE = 'prison_certificate_type', // 证件类型1-身份证 2-户口簿 3-其他
PRISON_ASSESSMENT_STATUS = 'prison_assessment_status', // 测评状态1-待测评 2-测评中 3-已完成 4-已过期
PRISON_ASSESSMENT_PASS_STATUS = 'prison_assessment_pass_status', // 测评及格状态1-及格 2-不及格 3-待评分
PRISON_ASSESSMENT_ANSWER_STATUS = 'prison_assessment_answer_status' // 测评答题状态1-待评分 2-已评分
PRISON_ASSESSMENT_ANSWER_STATUS = 'prison_assessment_answer_status', // 测评答题状态1-待评分 2-已评分
// ========== 评估报告模块 ==========
PRISON_REPORT_STATUS = 'prison_report_status', // 报告状态1-草稿 2-待审核 3-已通过 4-已退回
PRISON_REPORT_TEMPLATE_TYPE = 'prison_report_template_type', // 报告模板类型1-入监综合评估 2-定期考核报告 3-出监评估 4-减刑假释建议 5-专项评估
PRISON_COMMON_STATUS = 'prison_common_status' // 通用状态0-停用 1-启用
}

View File

@ -72,6 +72,17 @@ export function formatDate(date: Date, format?: string): string {
return date ? dayjs(date).format(format ?? 'YYYY-MM-DD HH:mm:ss') : ''
}
/**
* YYYY-MM-DD HH:mm:ss
* @param dateTime
*/
export function formatDateTime(dateTime: any): string {
if (!dateTime) {
return ''
}
return dayjs(dateTime).format('YYYY-MM-DD HH:mm:ss')
}
/**
* +
*/
@ -198,7 +209,16 @@ export function formatPast2(ms: number): string {
* @param cellValue
*/
export function dateFormatter(_row: any, _column: TableColumnCtx<any>, cellValue: any): string {
return cellValue ? formatDate(cellValue) : ''
if (!cellValue) {
return ''
}
// 直接返回已有格式的日期字符串
if (typeof cellValue === 'string' && cellValue.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) {
return cellValue
}
// 处理其他格式
const date = dayjs(cellValue)
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : ''
}
/**

View File

@ -1,4 +1,14 @@
<template>
<!-- 自定义监狱风格加载动画 -->
<Transition name="fade">
<PrisonLoginLoading
v-if="customLoadingVisible"
:text="loadingText"
:background="loadingBackground"
:lock="loadingLock"
:system-name="loadingSystemName"
/>
</Transition>
<el-form
v-show="getShow"
ref="formLogin"
@ -150,6 +160,7 @@
<script lang="ts" setup>
import { ElLoading } from 'element-plus'
import LoginFormTitle from './LoginFormTitle.vue'
import PrisonLoginLoading from './PrisonLoginLoading.vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useIcon } from '@/hooks/web/useIcon'
@ -245,7 +256,14 @@ const getTenantByWebsite = async () => {
}
}
}
const loading = ref() // ElLoading.service
const loading = ref() // Loading
const customLoadingVisible = ref(false) //
// ElLoading
const loadingText = ref('正在加载系统中...') //
const loadingBackground = ref('rgba(0, 0, 0, 0.7)') //
const loadingLock = ref(true) //
const loadingSystemName = ref('') // store
//
const handleLogin = async (params: any) => {
loginLoading.value = true
@ -261,11 +279,8 @@ const handleLogin = async (params: any) => {
if (!res) {
return
}
loading.value = ElLoading.service({
lock: true,
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)'
})
// 使
customLoadingVisible.value = true
if (loginDataLoginForm.rememberMe) {
authUtil.setLoginForm(loginDataLoginForm)
} else {
@ -283,7 +298,7 @@ const handleLogin = async (params: any) => {
}
} finally {
loginLoading.value = false
loading.value.close()
customLoadingVisible.value = false
}
}
@ -357,4 +372,15 @@ onMounted(() => {
cursor: pointer;
}
}
//
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<div class="prison-loading-overlay" :style="{ background: background }">
<div class="prison-loading-container">
<!-- 盾牌图标 + 旋转光环 -->
<div class="shield-wrapper">
<div class="shield-glow"></div>
<svg class="shield-icon" viewBox="0 0 100 120" xmlns="http://www.w3.org/2000/svg">
<!-- 盾牌主体 -->
<defs>
<linearGradient id="shieldGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e3a5f;stop-opacity:1" />
<stop offset="50%" style="stop-color:#2c5282;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1e3a5f;stop-opacity:1" />
</linearGradient>
<linearGradient id="goldGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f6e05e;stop-opacity:1" />
<stop offset="50%" style="stop-color:#d69e2e;stop-opacity:1" />
<stop offset="100%" style="stop-color:#b7791f;stop-opacity:1" />
</linearGradient>
</defs>
<!-- 盾牌轮廓 -->
<path
d="M50 5 L90 25 L90 65 Q90 95 50 115 Q10 95 10 65 L10 25 Z"
fill="url(#shieldGradient)"
stroke="url(#goldGradient)"
stroke-width="3"
/>
<!-- 盾牌内部装饰 -->
<path
d="M50 20 L75 35 L75 60 Q75 80 50 95 Q25 80 25 60 L25 35 Z"
fill="none"
stroke="#d69e2e"
stroke-width="1.5"
opacity="0.6"
/>
<!-- 警徽中心 -->
<circle cx="50" cy="55" r="18" fill="none" stroke="url(#goldGradient)" stroke-width="2" />
<!-- 五角星 -->
<polygon
points="50,40 53,50 63,50 55,57 58,67 50,61 42,67 45,57 37,50 47,50"
fill="url(#goldGradient)"
/>
</svg>
<!-- 旋转的外圈 -->
<div class="rotating-ring"></div>
</div>
<!-- 系统名称和加载信息 -->
<div class="loading-text">
<div class="system-name">{{ systemName }}</div>
<div class="loading-message">{{ message }}</div>
<!-- 进度条 -->
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<div class="loading-dots">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '@/store/modules/app'
defineOptions({ name: 'PrisonLoginLoading' })
interface Props {
text?: string //
background?: string //
lock?: boolean //
systemName?: string //
}
const props = withDefaults(defineProps<Props>(), {
text: '正在加载系统中...',
background: 'rgba(0, 0, 0, 0.7)',
lock: true,
systemName: ''
})
// store
const appStore = useAppStore()
const systemName = computed(() => props.systemName || appStore.getTitle || 'XL监狱综合管理平台')
//
const message = computed(() => props.text)
</script>
<style lang="scss" scoped>
.prison-loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.prison-loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 30px;
}
.shield-wrapper {
position: relative;
width: 120px;
height: 144px;
display: flex;
align-items: center;
justify-content: center;
}
.shield-glow {
position: absolute;
width: 140px;
height: 164px;
background: radial-gradient(ellipse at center, rgba(214, 158, 46, 0.3) 0%, transparent 70%);
animation: pulse 2s ease-in-out infinite;
}
.shield-icon {
width: 100px;
height: 120px;
animation: shieldFloat 3s ease-in-out infinite;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
}
.rotating-ring {
position: absolute;
width: 160px;
height: 160px;
border: 3px solid transparent;
border-top-color: #d69e2e;
border-radius: 50%;
animation: rotate 3s linear infinite;
opacity: 0.5;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
}
@keyframes shieldFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
.loading-text {
text-align: center;
color: #fff;
}
.system-name {
font-size: 28px;
font-weight: bold;
color: #f6e05e;
letter-spacing: 4px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
margin-bottom: 15px;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
.loading-message {
font-size: 16px;
color: #a0aec0;
margin-bottom: 20px;
letter-spacing: 2px;
}
.progress-bar {
width: 300px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
margin: 0 auto 15px;
}
.progress-fill {
height: 100%;
width: 30%;
background: linear-gradient(90deg, #d69e2e, #f6e05e, #d69e2e);
border-radius: 2px;
animation: progressMove 1.5s ease-in-out infinite;
}
@keyframes progressMove {
0% {
width: 10%;
margin-left: 0;
}
50% {
width: 50%;
margin-left: 25%;
}
100% {
width: 10%;
margin-left: 90%;
}
}
.loading-dots {
display: flex;
justify-content: center;
gap: 8px;
}
.dot {
width: 8px;
height: 8px;
background: #d69e2e;
border-radius: 50%;
animation: dotBounce 1.4s ease-in-out infinite;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
@keyframes dotBounce {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1.2);
opacity: 1;
}
}
</style>

View File

@ -100,10 +100,10 @@
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ dateFormatter(scope.row.createTime) }}
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<el-table-column label="操作" align="center" width="120" fixed="right">
<template #default="scope">
<el-button
type="primary"
@ -138,7 +138,7 @@
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { CellApi } from '@/api/prison/cell'
import { AreaApi } from '@/api/prison/area'

View File

@ -77,7 +77,7 @@
<el-divider content-position="left">消费明细</el-divider>
<el-table :data="formData.details" border>
<el-table-column label="商品名称" prop="goodsName" width="150">
<template #default="{ row, $index }">
<template #default="{ row }">
<el-input v-model="row.goodsName" placeholder="商品名称" />
</template>
</el-table-column>
@ -102,7 +102,7 @@
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="{ row, $index }">
<template #default="{ $index }">
<el-button type="danger" link @click="removeDetail($index)" :disabled="formData.details.length <= 1">删除</el-button>
</template>
</el-table-column>

View File

@ -17,6 +17,15 @@
class="!w-140px"
/>
</el-form-item>
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input
v-model="queryParams.prisonerName"
placeholder="请输入罪犯姓名"
clearable
@keyup.enter="handleQuery"
class="!w-120px"
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="queryParams.type"
@ -89,8 +98,11 @@
>
<el-table-column type="selection" width="55" />
<el-table-column label="订单ID" align="center" prop="id" width="80" />
<el-table-column label="订单号" align="center" prop="orderNo" width="180" />
<el-table-column label="罪犯姓名" align="center" prop="prisonerName" width="100" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
<el-table-column label="监区" align="center" prop="prisonAreaName" width="100" />
<el-table-column label="监室" align="center" prop="prisonCellName" width="100" />
<el-table-column label="订单号" align="center" prop="orderNo" width="180" />
<el-table-column label="类型" align="center" prop="type" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_CONSUMPTION_TYPE" :value="scope.row.type" />
@ -113,7 +125,7 @@
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-button
type="primary"
@ -176,6 +188,7 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
prisonerNo: undefined,
prisonerName: undefined,
type: undefined,
status: undefined
})

View File

@ -75,7 +75,7 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import type { EChartsOption } from 'echarts'
import { StatCard } from './components'
import StatCard from './components/StatCard.vue'
import { DashboardApi, type DashboardStatisticsVO } from '@/api/prison/dashboard'
import EChart from '@/components/Echart/src/Echart.vue'
import ChinaMap from './components/ChinaMap.vue'
@ -129,6 +129,7 @@ const ageChartOptions = computed<EChartsOption>(() => ({
top: 'center',
icon: 'circle'
},
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272'],
series: [
{
type: 'pie',
@ -170,6 +171,7 @@ const sentenceChartOptions = computed<EChartsOption>(() => ({
top: 'center',
icon: 'circle'
},
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'],
series: [
{
type: 'pie',
@ -211,6 +213,7 @@ const educationChartOptions = computed<EChartsOption>(() => ({
top: 'center',
icon: 'circle'
},
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'],
series: [
{
type: 'pie',

View File

@ -6,14 +6,16 @@
<el-descriptions-item label="罪犯编号">{{ data.prisonerNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ data.name }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="data.gender" />
<dict-tag v-if="data.gender" :type="DICT_TYPE.SYSTEM_USER_SEX" :value="data.gender" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ data.idCard }}</el-descriptions-item>
<el-descriptions-item label="出生日期">{{ data.birthday }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ data.ethnicity }}</el-descriptions-item>
<el-descriptions-item label="籍贯" :span="2">{{ data.nativePlace }}</el-descriptions-item>
<el-descriptions-item label="文化程度">
<dict-tag :type="DICT_TYPE.PRISON_EDUCATION" :value="data.education" />
<dict-tag v-if="data.education" :type="DICT_TYPE.PRISON_EDUCATION" :value="data.education" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="职业">{{ data.occupation }}</el-descriptions-item>
<el-descriptions-item label="家庭住址" :span="2">{{ data.address }}</el-descriptions-item>
@ -35,15 +37,18 @@
<!-- 监管信息 -->
<el-descriptions title="监管信息" :column="2" border style="margin-top: 20px">
<el-descriptions-item label="监管等级">
<dict-tag :type="DICT_TYPE.PRISON_SUPERVISION_LEVEL" :value="data.supervisionLevel" />
<dict-tag v-if="data.supervisionLevel" :type="DICT_TYPE.PRISON_SUPERVISION_LEVEL" :value="data.supervisionLevel" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="风险等级">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="data.riskLevel" />
<dict-tag v-if="data.riskLevel" :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="data.riskLevel" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="当前监区">{{ data.prisonAreaName }}</el-descriptions-item>
<el-descriptions-item label="当前监室">{{ data.prisonCellName }}</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :type="DICT_TYPE.PRISONER_STATUS" :value="data.status" />
<dict-tag v-if="data.status" :type="DICT_TYPE.PRISONER_STATUS" :value="data.status" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="备注">{{ data.remark }}</el-descriptions-item>
</el-descriptions>
@ -55,7 +60,7 @@
<el-timeline-item
v-for="(log, index) in areaHistory"
:key="index"
:timestamp="log.createTime"
:timestamp="formatDateTime(log.createTime)"
placement="top"
>
<el-card>
@ -74,7 +79,8 @@
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import * as PrisonerApi from '@/api/prison/prisoner'
import { formatDateTime } from '@/utils/formatTime'
import { PrisonerApi } from '@/api/prison/prisoner'
defineOptions({ name: 'PrisonPrisonerDetail' })
@ -89,9 +95,9 @@ const open = async (id: number) => {
loading.value = true
try {
//
data.value = await PrisonerApi.getPrisoner(id)
data.value = await PrisonerApi.get(id)
//
areaHistory.value = await PrisonerApi.getPrisonerAreaHistory(id)
areaHistory.value = await PrisonerApi.getAreaHistory(id)
} finally {
loading.value = false
}

View File

@ -273,7 +273,7 @@
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { PrisonerCreateVO } from '@/api/prison/prisoner'
import * as PrisonerApi from '@/api/prison/prisoner'
import { PrisonerApi } from '@/api/prison/prisoner'
import { AreaApi } from '@/api/prison/area'
import { CellApi } from '@/api/prison/cell'
@ -447,7 +447,7 @@ const resetForm = () => {
const getPrisonerDetail = async (id: number) => {
formLoading.value = true
try {
const data = await PrisonerApi.getPrisoner(id)
const data = await PrisonerApi.get(id)
formData.value = {
...formData.value,
...data
@ -468,10 +468,10 @@ const submitForm = async () => {
formLoading.value = true
try {
if (dialogType.value === 'create') {
await PrisonerApi.createPrisoner(formData.value)
await PrisonerApi.create(formData.value)
message.success('入监登记成功')
} else {
await PrisonerApi.updatePrisoner(formData.value)
await PrisonerApi.update(formData.value)
message.success('修改成功')
}
dialogVisible.value = false

View File

@ -0,0 +1,613 @@
<template>
<el-drawer v-model="dialogVisible" title="罪犯工作台" size="900px">
<div v-loading="loading" class="prisoner-workbench">
<!-- 顶部犯人基本信息卡片 -->
<el-card class="info-card" shadow="never">
<template #header>
<div class="card-header">
<span>基本信息</span>
<el-tag :type="statusType">{{ statusText }}</el-tag>
</div>
</template>
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="罪犯编号">{{ data.prisonerNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ data.name }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="data.gender" />
</el-descriptions-item>
<el-descriptions-item label="监管等级">
<dict-tag :type="DICT_TYPE.PRISON_SUPERVISION_LEVEL" :value="data.supervisionLevel" />
</el-descriptions-item>
<el-descriptions-item label="当前监区">{{ data.prisonAreaName || '-' }}</el-descriptions-item>
<el-descriptions-item label="当前监室">{{ data.prisonCellName || '-' }}</el-descriptions-item>
<el-descriptions-item label="入狱日期">{{ formatDateTime(data.imprisonmentDate) }}</el-descriptions-item>
<el-descriptions-item label="释放日期">{{ formatDateTime(data.releaseDate) }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- Tab页签相关记录 -->
<el-card class="tab-card" shadow="never" style="margin-top: 16px">
<el-tabs v-model="activeTab">
<!-- 消费记录Tab -->
<el-tab-pane label="消费记录" name="consumption">
<div class="tab-header">
<span class="stat-item">
<span class="label">总消费</span>
<span class="value amount">¥{{ consumptionStats.totalAmount?.toFixed(2) || '0.00' }}</span>
</span>
<span class="stat-item">
<span class="label">订单数</span>
<span class="value">{{ consumptionStats.orderCount || 0 }}</span>
</span>
</div>
<el-table :data="consumptionList" stripe style="width: 100%">
<el-table-column prop="orderNo" label="订单号" width="180" />
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_CONSUMPTION_TYPE" :value="row.type" />
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="金额" width="100">
<template #default="{ row }">
<span :class="{ 'amount': true, 'negative': row.totalAmount < 0 }">
¥{{ row.totalAmount?.toFixed(2) }}
</span>
</template>
</el-table-column>
<el-table-column prop="balance" label="余额" width="120">
<template #default="{ row }">
<span class="amount">¥{{ row.balance?.toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column prop="tradeTime" label="交易时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.tradeTime) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_CONSUMPTION_STATUS" :value="row.status" />
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="consumptionTotal > 0"
v-model:current-page="consumptionPage"
v-model:page-size="consumptionPageSize"
:page-sizes="[10, 20, 50]"
:total="consumptionTotal"
layout="total, sizes, prev, pager, next"
size="small"
@size-change="loadConsumptionList"
@current-change="loadConsumptionList"
/>
</el-tab-pane>
<!-- 计分考核Tab -->
<el-tab-pane label="计分考核" name="score">
<div class="tab-header">
<span class="stat-item">
<span class="label">累计总分</span>
<span class="value">{{ scoreStats.totalScore || 0 }}</span>
</span>
<span class="stat-item">
<span class="label">平均分</span>
<span class="value">{{ scoreStats.avgScore?.toFixed(1) || '0' }}</span>
</span>
<span class="stat-item">
<span class="label">考核次数</span>
<span class="value">{{ scoreStats.recordCount || 0 }}</span>
</span>
</div>
<el-table :data="scoreList" stripe style="width: 100%">
<el-table-column prop="year" label="年份" width="80" />
<el-table-column prop="month" label="月份" width="80">
<template #default="{ row }">
{{ row.year }}-{{ String(row.month).padStart(2, '0') }}
</template>
</el-table-column>
<el-table-column prop="baseScore" label="基础分" width="80" />
<el-table-column prop="rewardScore" label="加分" width="70">
<template #default="{ row }">
<span class="positive">+{{ row.rewardScore }}</span>
</template>
</el-table-column>
<el-table-column prop="penaltyScore" label="扣分" width="70">
<template #default="{ row }">
<span class="negative">-{{ row.penaltyScore }}</span>
</template>
</el-table-column>
<el-table-column prop="totalScore" label="总分" width="80">
<template #default="{ row }">
<span class="amount" style="font-weight: bold">{{ row.totalScore }}</span>
</template>
</el-table-column>
<el-table-column prop="level" label="等级" width="80">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_SCORE_LEVEL" :value="row.level" />
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_SCORE_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column prop="assessorName" label="考核人" width="100" />
<el-table-column prop="createTime" label="考核时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.createTime) }}
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="scoreTotal > 0"
v-model:current-page="scorePage"
v-model:page-size="scorePageSize"
:page-sizes="[10, 20, 50]"
:total="scoreTotal"
layout="total, sizes, prev, pager, next"
size="small"
@size-change="loadScoreList"
@current-change="loadScoreList"
/>
</el-tab-pane>
<!-- 问卷记录Tab -->
<el-tab-pane label="问卷记录" name="questionnaire">
<div class="tab-header">
<el-button type="primary" size="small" @click="openInitiateDialog">
发起问卷测评
</el-button>
<span class="stat-item">
<span class="label">已完成</span>
<span class="value">{{ questionnaireStats.completedCount || 0 }}</span>
</span>
<span class="stat-item">
<span class="label">待测评</span>
<span class="value">{{ questionnaireStats.pendingCount || 0 }}</span>
</span>
</div>
<el-table :data="questionnaireList" stripe style="width: 100%">
<el-table-column prop="questionnaireName" label="问卷名称" min-width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_RECORD_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column prop="totalScore" label="得分" width="80">
<template #default="{ row }">
<span v-if="row.totalScore !== undefined" class="amount">{{ row.totalScore }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="passStatus" label="及格状态" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_RECORD_PASS_STATUS" :value="row.passStatus" />
</template>
</el-table-column>
<el-table-column prop="riskLevel" label="风险等级" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="row.riskLevel" />
</template>
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.startTime) }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="完成时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.endTime) }}
</template>
</el-table-column>
<el-table-column prop="deadline" label="截止日期" width="160">
<template #default="{ row }">
{{ formatDateTime(row.deadline) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.status === 1"
type="success"
size="small"
link
@click="handleStartAssessment(row)"
>
开始测评
</el-button>
<el-button
v-if="row.status === 1"
type="danger"
size="small"
link
@click="handleCancelAssessment(row)"
>
取消
</el-button>
<el-button
v-if="row.status === 3 && row.passStatus === 3"
type="warning"
size="small"
link
@click="handleManualScore(row)"
>
人工评分
</el-button>
<span v-if="row.status !== 1 && !(row.status === 3 && row.passStatus === 3)" class="text-gray">-</span>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="questionnaireTotal > 0"
v-model:current-page="questionnairePage"
v-model:page-size="questionnairePageSize"
:page-sizes="[10, 20, 50]"
:total="questionnaireTotal"
layout="total, sizes, prev, pager, next"
size="small"
@size-change="loadQuestionnaireList"
@current-change="loadQuestionnaireList"
/>
</el-tab-pane>
<!-- 风险评估Tab -->
<el-tab-pane label="风险评估" name="risk">
<div class="tab-header">
<span class="stat-item">
<span class="label">当前风险等级</span>
<span class="value">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="data.riskLevel" />
</span>
</span>
<span class="stat-item">
<span class="label">最近评估</span>
<span class="value">{{ riskStats.lastAssessmentTime || '暂无' }}</span>
</span>
</div>
<el-table :data="riskList" stripe style="width: 100%">
<el-table-column prop="assessDate" label="评估日期" width="160">
<template #default="{ row }">
{{ formatDateTime(row.assessDate) }}
</template>
</el-table-column>
<el-table-column prop="riskLevel" label="风险等级" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="row.riskLevel" />
</template>
</el-table-column>
<el-table-column prop="riskScore" label="风险分值" width="100" />
<el-table-column prop="assessmentType" label="评估类型" width="120" />
<el-table-column prop="assessorName" label="评估人" width="100" />
<el-table-column prop="remark" label="备注" min-width="200" />
</el-table>
<el-pagination
v-if="riskTotal > 0"
v-model:current-page="riskPage"
v-model:page-size="riskPageSize"
:page-sizes="[10, 20, 50]"
:total="riskTotal"
layout="total, sizes, prev, pager, next"
size="small"
@size-change="loadRiskList"
@current-change="loadRiskList"
/>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
<!-- 发起问卷测评弹窗 -->
<InitiateAssessmentDialog ref="initiateDialogRef" @success="loadQuestionnaireList" />
<!-- 人工评分弹窗 -->
<ManualScoreDialog ref="manualScoreDialogRef" @success="loadQuestionnaireList" />
</el-drawer>
</template>
<script lang="ts" setup>
import { DICT_TYPE, useDictStoreWithOut } from '@/utils/dict'
import { dictKeys } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import { PrisonerApi } from '@/api/prison/prisoner'
import { ConsumptionApi } from '@/api/prison/consumption'
import { ScoreApi } from '@/api/prison/score'
import { QuestionnaireRecordApi } from '@/api/prison/questionnairerecord'
import InitiateAssessmentDialog from '@/views/prison/questionnairerecord/InitiateAssessmentDialog.vue'
import ManualScoreDialog from '@/views/prison/questionnairerecord/ManualScoreDialog.vue'
defineOptions({ name: 'PrisonerWorkbench' })
const dialogVisible = ref(false)
const loading = ref(false)
const activeTab = ref('consumption')
const data = ref<any>({})
//
const dictStore = useDictStoreWithOut()
const loadDictTypes = async () => {
//
await dictStore.setDictMap()
}
//
const consumptionList = ref([])
const consumptionTotal = ref(0)
const consumptionPage = ref(1)
const consumptionPageSize = ref(10)
const consumptionStats = ref<any>({})
//
const scoreList = ref([])
const scoreTotal = ref(0)
const scorePage = ref(1)
const scorePageSize = ref(10)
const scoreStats = ref<any>({})
//
const questionnaireList = ref([])
const questionnaireTotal = ref(0)
const questionnairePage = ref(1)
const questionnairePageSize = ref(10)
const questionnaireStats = ref<any>({})
//
const riskList = ref([])
const riskTotal = ref(0)
const riskPage = ref(1)
const riskPageSize = ref(10)
const riskStats = ref<any>({})
// ref
const initiateDialogRef = ref()
const manualScoreDialogRef = ref()
//
const statusType = computed(() => {
const statusMap: Record<number, string> = {
1: 'success', //
2: 'warning', //
3: 'info', //
4: 'danger' //
}
return statusMap[data.value.status] || 'info'
})
const statusText = computed(() => {
const statusMap: Record<number, string> = {
1: '在狱',
2: '出狱',
3: '假释',
4: '释放'
}
return statusMap[data.value.status] || '未知'
})
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
loading.value = true
try {
//
await loadDictTypes()
//
data.value = await PrisonerApi.get(id)
//
resetPages()
//
await Promise.all([
loadConsumptionList(),
loadScoreList(),
loadQuestionnaireList(),
loadRiskList()
])
} finally {
loading.value = false
}
}
/** 重置分页 */
const resetPages = () => {
consumptionPage.value = 1
scorePage.value = 1
questionnairePage.value = 1
riskPage.value = 1
}
/** 加载消费记录 */
const loadConsumptionList = async () => {
if (!data.value.prisonerNo) return
try {
const res = await ConsumptionApi.getConsumptionPage({
pageNo: consumptionPage.value,
pageSize: consumptionPageSize.value,
prisonerNo: data.value.prisonerNo
})
consumptionList.value = res.list
consumptionTotal.value = res.total
//
if (res.list.length > 0) {
const totalAmount = res.list.reduce((sum: number, item: any) => sum + (item.totalAmount || 0), 0)
consumptionStats.value = { totalAmount, orderCount: res.total }
}
} catch {}
}
/** 加载计分记录 */
const loadScoreList = async () => {
if (!data.value.prisonerNo) return
try {
const res = await ScoreApi.getScorePage({
pageNo: scorePage.value,
pageSize: scorePageSize.value,
prisonerNo: data.value.prisonerNo
})
scoreList.value = res.list
scoreTotal.value = res.total
//
if (res.list.length > 0) {
const totalScore = res.list.reduce((sum: number, item: any) => sum + (item.totalScore || 0), 0)
const avgScore = res.list.length > 0 ? totalScore / res.list.length : 0
scoreStats.value = { totalScore, avgScore, recordCount: res.total }
}
} catch {}
}
/** 加载问卷记录 */
const loadQuestionnaireList = async () => {
if (!data.value.id) return
try {
const res = await QuestionnaireRecordApi.getQuestionnaireRecordPage({
pageNo: questionnairePage.value,
pageSize: questionnairePageSize.value,
prisonerId: data.value.id
})
questionnaireList.value = res.list
questionnaireTotal.value = res.total
//
const completedCount = res.list.filter((item: any) => item.status === 3).length
const pendingCount = res.list.filter((item: any) => item.status === 1).length
questionnaireStats.value = { completedCount, pendingCount, total: res.total }
} catch {}
}
/** 加载风险评估记录 */
const loadRiskList = async () => {
if (!data.value.id) return
try {
// TODO: RiskAssessment
// const res = await RiskAssessmentApi.getRiskAssessmentPage({...})
// riskList.value = res.list
// riskTotal.value = res.total
riskStats.value = { lastAssessmentTime: '暂无数据' }
} catch {}
}
/** 打开发起问卷弹窗 */
const openInitiateDialog = () => {
initiateDialogRef.value?.open({
id: data.value.id,
prisonerNo: data.value.prisonerNo,
name: data.value.name
})
}
/** 开始测评 */
const handleStartAssessment = async (row: any) => {
try {
await QuestionnaireRecordApi.startAssessment(row.id, data.value.id)
message.success('测评已开始')
loadQuestionnaireList()
} catch {}
}
/** 取消测评 */
const handleCancelAssessment = async (row: any) => {
try {
await QuestionnaireRecordApi.cancelAssessment(row.id)
message.success('测评已取消')
loadQuestionnaireList()
} catch {}
}
/** 人工评分 */
const handleManualScore = (row: any) => {
manualScoreDialogRef.value?.open(row)
}
const message = useMessage()
defineExpose({ open })
</script>
<style lang="scss" scoped>
.prisoner-workbench {
.info-card {
:deep(.el-card__header) {
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
}
.tab-card {
:deep(.el-card__body) {
padding: 0 20px 20px;
}
}
.tab-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
.stat-item {
display: flex;
align-items: center;
gap: 8px;
.label {
color: #909399;
font-size: 14px;
}
.value {
font-size: 16px;
font-weight: 500;
&.amount {
color: #f56c6c;
font-size: 18px;
font-weight: bold;
}
.positive {
color: #67c23a;
}
.negative {
color: #f56c6c;
}
}
}
}
.el-table {
margin-bottom: 16px;
.amount {
color: #f56c6c;
font-weight: 500;
&.negative {
color: #f56c6c;
}
}
.positive {
color: #67c23a;
}
.negative {
color: #f56c6c;
}
}
.el-pagination {
display: flex;
justify-content: flex-end;
}
.text-gray {
color: #c0c4cc;
}
}
</style>

View File

@ -80,7 +80,7 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import * as PrisonerApi from '@/api/prison/prisoner'
import { PrisonerApi } from '@/api/prison/prisoner'
import { AreaApi } from '@/api/prison/area'
import { CellApi } from '@/api/prison/cell'
@ -117,7 +117,7 @@ const open = async (prisonerId: number) => {
resetForm()
try {
//
prisoner.value = await PrisonerApi.getPrisoner(prisonerId)
prisoner.value = await PrisonerApi.get(prisonerId)
formData.prisonerId = prisonerId
//
const treeData = await AreaApi.getAreaTree()

View File

@ -205,8 +205,16 @@
<dict-tag :type="DICT_TYPE.PRISONER_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="240">
<el-table-column label="操作" align="center" width="300" fixed="right">
<template #default="scope">
<el-button
type="primary"
link
@click="handleWorkbench(scope.row)"
v-hasPermi="['prison:prisoner:query']"
>
工作台
</el-button>
<el-button
type="primary"
link
@ -257,20 +265,24 @@
<!-- 详情弹窗 -->
<PrisonerDetail ref="detailRef" />
<!-- 工作台弹窗 -->
<PrisonerWorkbench ref="workbenchRef" />
<!-- 调监弹窗 -->
<TransferForm ref="transferRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { checkPermi } from '@/utils/permission'
import PrisonerForm from './PrisonerForm.vue'
import PrisonerDetail from './PrisonerDetail.vue'
import PrisonerWorkbench from './PrisonerWorkbench.vue'
import TransferForm from './TransferForm.vue'
import download from '@/utils/download'
import * as PrisonerApi from '@/api/prison/prisoner'
import { AreaApi } from '@/api/prison/area'
import { CellApi } from '@/api/prison/cell'
import { PrisonerApi } from '@/api/prison/prisoner'
import type { PrisonerVO, PrisonerCreateVO } from '@/api/prison/prisoner'
import { AreaApi, type AreaNode } from '@/api/prison/area'
import { CellApi, type CellVO } from '@/api/prison/cell'
import { ref, reactive, onMounted } from 'vue'
defineOptions({ name: 'PrisonPrisoner' })
@ -280,9 +292,9 @@ const message = useMessage() // 消息弹窗
const loading = ref(true) //
const total = ref(0) //
const list = ref([]) //
const areaTreeList = ref([]) //
const cellList = ref([]) //
const list = ref<PrisonerVO[]>([]) //
const areaTreeList = ref<AreaNode[]>([]) //
const cellList = ref<CellVO[]>([]) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
@ -303,6 +315,7 @@ const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) //
const formRef = ref() //
const detailRef = ref() //
const workbenchRef = ref() //
const transferRef = ref() //
/** 查询列表 */
@ -317,7 +330,7 @@ const getList = async () => {
queryParams.imprisonmentDateStart = undefined
queryParams.imprisonmentDateEnd = undefined
}
const data = await PrisonerApi.getPrisonerPage(queryParams)
const data = await PrisonerApi.getPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
@ -348,7 +361,7 @@ const handleExport = async () => {
await message.exportConfirm()
//
exportLoading.value = true
const data = await PrisonerApi.exportPrisoner(queryParams)
const data = await PrisonerApi.export(queryParams)
download.excel(data, '罪犯信息.xls')
} catch {
} finally {
@ -362,10 +375,15 @@ const openForm = (type: string, id?: number) => {
}
/** 查看详情 */
const handleDetail = (row: any) => {
const handleDetail = (row: PrisonerVO) => {
detailRef.value.open(row.id)
}
/** 打开工作台 */
const handleWorkbench = (row: PrisonerVO) => {
workbenchRef.value.open(row.id)
}
/** 打开调监弹窗 */
const openTransferForm = () => {
if (checkedIds.value.length !== 1) {
@ -376,7 +394,7 @@ const openTransferForm = () => {
}
/** 调监操作 */
const handleTransfer = (row: any) => {
const handleTransfer = (row: PrisonerVO) => {
transferRef.value.open(row.id)
}
@ -385,7 +403,7 @@ const handleAreaChange = async (areaId: number) => {
queryParams.prisonCellId = undefined
cellList.value = []
if (areaId) {
cellList.value = await CellApi.getCellPage({ areaId, pageNo: 1, pageSize: 200 }).then((res: any) => res.list || [])
cellList.value = await CellApi.getCellPage({ areaId, pageNo: 1, pageSize: 200 }).then((res: PageResult<CellVO>) => res.list || [])
}
}
@ -401,7 +419,7 @@ const formatIdCard = (idCard: string) => {
}
/** 刑期显示格式化 */
const formatSentence = (row: any) => {
const formatSentence = (row: PrisonerVO) => {
if (row.lifeImprisonment === 1) {
return '无期'
}
@ -422,7 +440,7 @@ const handleDelete = async (id: number) => {
//
await message.delConfirm()
//
await PrisonerApi.deletePrisoner(id)
await PrisonerApi.delete(id)
message.success(t('common.delSuccess'))
//
await getList()
@ -431,7 +449,7 @@ const handleDelete = async (id: number) => {
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: any[]) => {
const handleRowCheckboxChange = (rows: PrisonerVO[]) => {
checkedIds.value = rows.map((row) => row.id)
}
@ -440,7 +458,7 @@ const handleDeleteBatch = async () => {
//
await message.delConfirm()
//
await PrisonerApi.deletePrisonerList(checkedIds.value)
await PrisonerApi.deleteList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
//

File diff suppressed because it is too large Load Diff

View File

@ -1,236 +0,0 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="所属问卷" prop="questionnaireId">
<el-input
v-model="queryParams.questionnaireId"
placeholder="请输入问卷ID"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="问题标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入问题标题"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="问题类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择"
clearable
class="!w-140px"
>
<el-option
v-for="dict in questionTypeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['prison:question:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:question:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:question:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
<el-table-column type="selection" width="55" />
<el-table-column label="问题标题" align="center" prop="title" width="200" />
<el-table-column label="所属问卷ID" align="center" prop="questionnaireId" width="100" />
<el-table-column label="问题类型" align="center" prop="type" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_QUESTION_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="分值" align="center" prop="score" width="80" />
<el-table-column label="排序" align="center" prop="sort" width="80" />
<el-table-column label="是否必答" align="center" prop="isRequired" width="100">
<template #default="scope">
<el-tag :type="scope.row.isRequired ? 'danger' : 'success'">
{{ scope.row.isRequired ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ dateFormatter(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:question:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:question:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<QuestionForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { QuestionApi, Question } from '@/api/prison/question'
import QuestionForm from './QuestionForm.vue'
defineOptions({ name: 'PrisonQuestion' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<Question[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
questionnaireId: undefined,
title: undefined,
type: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
// (使)
const questionTypeOptions = getIntDictOptions(DICT_TYPE.PRISON_QUESTION_TYPE)
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await QuestionApi.getQuestionPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await QuestionApi.deleteQuestion(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: Question[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await QuestionApi.deleteQuestionList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await QuestionApi.exportQuestion(queryParams)
download.excel(data, '问卷问题.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -23,12 +23,26 @@
<el-form-item label="问卷说明" prop="description">
<Editor v-model="formData.description" height="150px" />
</el-form-item>
<el-form-item label="封面图片" prop="coverImage">
<UploadImgs v-model="formData.coverImage" :limit="1" />
</el-form-item>
<el-form-item label="填写说明" prop="instruction">
<el-input v-model="formData.instruction" type="textarea" placeholder="请输入问卷填写说明" :rows="3" />
</el-form-item>
<el-form-item label="预计耗时" prop="estimatedTime">
<el-input-number v-model="formData.estimatedTime" :min="1" :max="300" placeholder="请输入预计完成时间(分钟)" />
<span class="ml-5px">分钟</span>
</el-form-item>
<el-form-item label="总分" prop="totalScore">
<el-input v-model="formData.totalScore" placeholder="请输入总分" />
</el-form-item>
<el-form-item label="及格分" prop="passScore">
<el-input v-model="formData.passScore" placeholder="请输入及格分" />
</el-form-item>
<el-form-item label="是否匿名" prop="allowAnonymous">
<el-switch v-model="formData.allowAnonymous" />
<span class="ml-5px">允许匿名填写</span>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option
@ -65,8 +79,12 @@ const formData = ref({
title: undefined,
type: undefined,
description: undefined,
coverImage: [] as string[],
instruction: undefined,
estimatedTime: undefined,
totalScore: undefined,
passScore: undefined,
allowAnonymous: false,
status: undefined
})
const formRules = reactive({
@ -125,8 +143,12 @@ const resetForm = () => {
title: undefined,
type: undefined,
description: undefined,
coverImage: [] as string[],
instruction: undefined,
estimatedTime: undefined,
totalScore: undefined,
passScore: undefined,
allowAnonymous: false,
status: undefined
}
formRef.value?.resetFields()

View File

@ -0,0 +1,973 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="850px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<!-- 使用折叠面板分组 -->
<el-collapse v-model="activeCollapse" accordion>
<!-- 基本信息 -->
<el-collapse-item title="基本信息" name="basic">
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="问题标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入问题标题" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="问题类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择问题类型" @change="handleTypeChange" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_QUESTION_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="所属分区" prop="partName">
<el-select
v-model="formData.partName"
placeholder="请选择或创建分区"
style="width: 100%"
allow-create
filterable
default-first-option
>
<el-option label="默认分区" value="" />
<el-option
v-for="part in partitionOptions"
:key="part.value"
:label="part.label"
:value="part.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="是否必答">
<el-switch v-model="formData.isRequired" active-text="是" inactive-text="否" />
</el-form-item>
</el-col>
</el-row>
</el-collapse-item>
<!-- 选项配置单选/多选 -->
<el-collapse-item title="选项配置" name="options" v-if="formData.type === 1 || formData.type === 2">
<!-- 快速粘贴 -->
<div class="quick-paste-section">
<div class="quick-paste-header">
<span>快速导入选项</span>
<el-button type="success" size="small" @click="showPasteDialog = true">
<Icon icon="ep:document-copy" /> 粘贴导入
</el-button>
</div>
</div>
<!-- 选项列表 -->
<div class="options-container">
<div class="options-header">
<span class="col-score">分值</span>
<span class="col-label">选项文字</span>
<span class="col-actions">操作</span>
</div>
<draggable
v-model="optionList"
item-key="index"
handle=".drag-handle"
:animation="200"
class="option-drag-list"
>
<template #item="{ element: option, index }">
<div class="option-item" :class="{ 'is-other': option.isOther }">
<el-icon class="drag-handle"><Rank /></el-icon>
<el-input-number
v-model="option.score"
:min="0"
:max="999"
size="small"
class="col-score-input"
/>
<template v-if="!option.isOther">
<el-input
v-model="option.label"
placeholder="请输入选项文字"
size="small"
class="col-label-input"
:class="{ 'is-error': option._error }"
@blur="validateOption(option)"
/>
</template>
<template v-else>
<el-tag type="warning" size="small">其他</el-tag>
<el-input
v-model="option.label"
placeholder="提示文字,如:其他,请说明"
size="small"
class="col-label-input"
/>
<el-tag type="info" size="small">用户输入</el-tag>
</template>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
@click="removeOption(index)"
/>
</div>
</template>
</draggable>
<!-- 添加按钮 -->
<div class="add-buttons">
<el-button type="primary" plain size="small" :icon="Plus" @click="addOption">
添加选项
</el-button>
<el-button type="warning" plain size="small" :icon="Edit" @click="addOtherOption" :disabled="hasOtherOption">
添加"其他"选项
</el-button>
<el-button type="info" plain size="small" @click="batchSetScore">
批量设置分值
</el-button>
</div>
<!-- 选项统计 -->
<div class="options-stats">
<el-icon><InfoFilled /></el-icon>
{{ validOptionsCount }} 个有效选项
<span v-if="optionList.length < 2 && formData.type !== 3" class="warning-text">
至少需要2个选项
</span>
</div>
</div>
</el-collapse-item>
<!-- 验证设置 -->
<el-collapse-item title="验证设置" name="validation">
<el-row :gutter="20">
<!-- 填空题 -->
<template v-if="formData.type === 3">
<el-col :span="12">
<el-form-item label="占位提示">
<el-input v-model="formData.placeholder" placeholder="请输入占位提示文字" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="默认值">
<el-input v-model="formData.defaultValue" placeholder="请输入默认值(可选)" />
</el-form-item>
</el-col>
</template>
<!-- 评分题 -->
<template v-else-if="formData.type === 4">
<el-col :span="8">
<el-form-item label="最低分">
<el-input-number v-model="formData.minValue" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="最高分">
<el-input-number v-model="formData.maxValue" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="默认分值">
<el-input-number v-model="formData.score" :min="0" style="width: 100%" />
</el-form-item>
</el-col>
</template>
<!-- 日期题 -->
<template v-else-if="formData.type === 5">
<el-col :span="12">
<el-form-item label="最早日期">
<el-date-picker
v-model="formData.minValue"
type="date"
placeholder="选择最早日期(可选)"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最晚日期">
<el-date-picker
v-model="formData.maxValue"
type="date"
placeholder="选择最晚日期(可选)"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</template>
<!-- 数字题 -->
<template v-else-if="formData.type === 6">
<el-col :span="8">
<el-form-item label="最小值">
<el-input-number v-model="formData.minValue" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="最大值">
<el-input-number v-model="formData.maxValue" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="默认值">
<el-input-number v-model="formData.defaultValue" style="width: 100%" />
</el-form-item>
</el-col>
</template>
<!-- 其他题型 -->
<template v-else>
<el-col :span="24">
<el-form-item label="分值">
<el-input-number v-model="formData.score" :min="0" style="width: 200px" />
</el-form-item>
</el-col>
</template>
</el-row>
<!-- 排序字段始终显示 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="排序序号">
<el-input-number v-model="formData.sort" :min="0" style="width: 100%" />
<div class="form-tip">数字越小越靠前</div>
</el-form-item>
</el-col>
</el-row>
</el-collapse-item>
<!-- 高级设置 -->
<el-collapse-item title="高级设置" name="advanced">
<!-- 帮助说明 -->
<el-form-item label="帮助说明">
<el-input
v-model="formData.helpText"
type="textarea"
placeholder="请输入帮助说明文字,将显示在问题下方辅助填写"
:rows="2"
maxlength="200"
show-word-limit
/>
</el-form-item>
<!-- 自动填充 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="自动填充">
<el-select v-model="formData.autoFillType" style="width: 100%">
<el-option label="无" value="NONE" />
<el-option label="系统自动" value="AUTO" />
<el-option label="手动输入" value="MANUAL" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="填充来源" v-if="formData.autoFillType !== 'NONE'">
<el-select v-model="formData.autoFillSource" placeholder="请选择填充来源" style="width: 100%" clearable>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.PRISON_QUESTION_AUTO_FILL_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 显示条件 -->
<el-form-item label="显示条件">
<div class="condition-builder">
<div class="condition-row">
<span class="condition-text"></span>
<el-select v-model="conditionForm.field" placeholder="选择字段" style="width: 140px" clearable>
<el-option label="风险等级" value="riskLevel" />
<el-option label="评估次数" value="assessmentCount" />
<el-option label="总分" value="totalScore" />
<el-option label="年龄" value="age" />
<el-option label="在押时长(月)" value="months" />
</el-select>
<el-select v-model="conditionForm.operator" placeholder="运算符" style="width: 100px">
<el-option label="等于" value="=" />
<el-option label="不等于" value="!=" />
<el-option label="大于" value=">" />
<el-option label="大于等于" value=">=" />
<el-option label="小于" value="<" />
<el-option label="小于等于" value="<=" />
</el-select>
<el-input v-model="conditionForm.value" placeholder="输入值" style="width: 120px" />
<el-switch
v-model="conditionForm.enabled"
active-text="启用"
inactive-text="禁用"
:disabled="!canEnableCondition"
/>
</div>
<div v-if="conditionForm.enabled" class="condition-preview">
预览{{ getConditionPreview() }}
</div>
</div>
</el-form-item>
</el-collapse-item>
</el-collapse>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
</template>
</Dialog>
<!-- 快速粘贴弹窗 -->
<Dialog title="快速导入选项" v-model="showPasteDialog" width="550px">
<el-input
v-model="pasteText"
type="textarea"
:rows="10"
placeholder="请粘贴选项内容支持以下分隔方式
每行一个选项
逗号分隔选项1,选项2,选项3
分号分隔选项1;选项2;选项3
顿号分隔选项1选项2选项3"
/>
<div class="paste-preview">
<div class="preview-header">
<span>预览识别到 {{ parsedOptions.length }} 个选项</span>
<el-button v-if="parsedOptions.length > 0" type="primary" link @click="pasteText = ''">
清空
</el-button>
</div>
<div class="preview-tags">
<el-tag
v-for="(opt, idx) in parsedOptions"
:key="idx"
type="info"
size="small"
effect="plain"
>
{{ opt }}
</el-tag>
</div>
</div>
<template #footer>
<el-button @click="showPasteDialog = false"> </el-button>
<el-button
@click="confirmPaste"
type="primary"
:disabled="parsedOptions.length === 0"
>
导入 {{ parsedOptions.length }}
</el-button>
</template>
</Dialog>
<!-- 批量设置分值弹窗 -->
<Dialog title="批量设置分值" v-model="showBatchDialog" width="400px">
<el-form>
<el-form-item label="分值">
<el-input-number v-model="batchScore" :min="0" :max="999" style="width: 100%" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showBatchDialog = false"> </el-button>
<el-button @click="applyBatchScore" type="primary"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import { QuestionApi, Question } from '@/api/prison/question'
import { Plus, Delete, Edit, InfoFilled, Rank } from '@element-plus/icons-vue'
import draggable from 'vuedraggable'
defineOptions({ name: 'QuestionForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const activeCollapse = ref('basic') //
//
const partitionOptions = ref<Array<{ label: string; value: string }>>([])
//
const showPasteDialog = ref(false)
const pasteText = ref('')
//
const showBatchDialog = ref(false)
const batchScore = ref(0)
//
const conditionForm = reactive({
enabled: false,
field: '',
operator: '=',
value: ''
})
//
interface OptionItem {
label: string
score: number
isOther?: boolean
_error?: boolean //
}
//
const canEnableCondition = computed(() => {
return conditionForm.field && conditionForm.operator && conditionForm.value
})
//
const optionList = ref<OptionItem[]>([])
// ""
const hasOtherOption = computed(() => {
return optionList.value.some(o => o.isOther)
})
//
const validOptionsCount = computed(() => {
return optionList.value.filter(o => o.label.trim()).length
})
const formData = ref({
id: undefined as number | undefined,
questionnaireId: undefined as number | undefined,
title: undefined as string | undefined,
type: undefined as number | undefined,
options: undefined as string | undefined,
score: undefined as number | undefined,
sort: undefined as number | undefined,
isRequired: true as boolean,
partName: undefined as string | undefined,
helpText: undefined as string | undefined,
placeholder: undefined as string | undefined,
defaultValue: undefined as string | undefined,
autoFillType: 'NONE' as string,
autoFillSource: undefined as string | undefined,
displayCondition: undefined as string | undefined,
minValue: undefined as number | undefined,
maxValue: undefined as number | undefined
})
const formRules = reactive({
title: [{ required: true, message: '问题标题不能为空', trigger: 'blur' }],
type: [{ required: true, message: '请选择问题类型', trigger: 'change' }]
})
const formRef = ref()
/** 添加选项 */
const addOption = () => {
optionList.value.push({ label: '', score: 0, isOther: false })
}
/** 添加"其他"选项 */
const addOtherOption = () => {
if (hasOtherOption.value) {
message.warning('已存在"其他"选项')
return
}
optionList.value.push({ label: '其他,请说明', score: 0, isOther: true })
}
/** 删除选项 */
const removeOption = (index: number) => {
optionList.value.splice(index, 1)
}
/** 验证选项 */
const validateOption = (option: OptionItem) => {
option._error = !option.label.trim()
}
/** 批量设置分值 */
const batchSetScore = () => {
batchScore.value = 0
showBatchDialog.value = true
}
/** 应用批量分值 */
const applyBatchScore = () => {
optionList.value.forEach(o => {
if (!o.isOther) {
o.score = batchScore.value
}
})
showBatchDialog.value = false
message.success(`已将所有选项分值设为 ${batchScore.value}`)
}
/** 问题类型变化 */
const handleTypeChange = (val: number) => {
optionList.value = []
//
if (val === 4) { //
formData.value.minValue = 1
formData.value.maxValue = 5
formData.value.score = 5
} else if (val === 6) { //
formData.value.minValue = 0
formData.value.maxValue = 100
formData.value.score = 0
} else if (val === 5) { //
formData.value.minValue = undefined
formData.value.maxValue = undefined
} else {
formData.value.minValue = undefined
formData.value.maxValue = undefined
}
}
/** 解析选项 JSON */
const parseOptions = (optionsStr: string | undefined): OptionItem[] => {
if (!optionsStr) return []
try {
return JSON.parse(optionsStr)
} catch {
return []
}
}
/** 将选项转为 JSON */
const stringifyOptions = (options: OptionItem[]): string => {
if (options.length === 0) return ''
return JSON.stringify(options.filter(o => o.label.trim()))
}
/** 解析粘贴文本 */
const parsedOptions = computed(() => {
if (!pasteText.value.trim()) return []
const text = pasteText.value.trim()
let items: string[]
if (text.includes('\n')) {
items = text.split('\n').map(s => s.trim()).filter(s => s)
} else if (text.includes('') || text.includes(',')) {
items = text.split(/[,]/).map(s => s.trim()).filter(s => s)
} else if (text.includes('') || text.includes(';')) {
items = text.split(/[;]/).map(s => s.trim()).filter(s => s)
} else if (text.includes('、')) {
items = text.split('、').map(s => s.trim()).filter(s => s)
} else {
items = text.split(/\s+/).map(s => s.trim()).filter(s => s)
}
return [...new Set(items)]
})
/** 确认粘贴 */
const confirmPaste = () => {
if (parsedOptions.value.length === 0) return
parsedOptions.value.forEach(label => {
optionList.value.push({ label, score: 0, isOther: false })
})
pasteText.value = ''
showPasteDialog.value = false
message.success(`已添加 ${parsedOptions.value.length} 个选项`)
}
/** 条件预览 */
const getConditionPreview = () => {
const fieldLabels: Record<string, string> = {
riskLevel: '风险等级',
assessmentCount: '评估次数',
totalScore: '总分',
age: '年龄',
months: '在押时长'
}
const opLabels: Record<string, string> = {
'=': '等于', '!=': '不等于', '>': '大于',
'>=': '大于等于', '<': '小于', '<=': '小于等于'
}
return `${fieldLabels[conditionForm.field] || conditionForm.field} ${opLabels[conditionForm.operator]} ${conditionForm.value}时显示`
}
/** 更新显示条件 */
const updateDisplayCondition = () => {
if (conditionForm.enabled && conditionForm.field && conditionForm.operator && conditionForm.value) {
formData.value.displayCondition = JSON.stringify({
field: conditionForm.field,
operator: conditionForm.operator,
value: conditionForm.value
})
} else {
formData.value.displayCondition = undefined
}
}
/** 监听条件启用状态 */
watch(() => conditionForm.enabled, (val) => {
if (val) {
updateDisplayCondition()
} else {
formData.value.displayCondition = undefined
}
})
/** 打开弹窗 */
const open = async (type: string, id?: number, questionnaireId?: number, partitions?: Array<{ label: string; value: string }>) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
activeCollapse.value = 'basic' //
resetForm()
if (partitions) {
partitionOptions.value = partitions
} else {
partitionOptions.value = []
}
if (questionnaireId) {
formData.value.questionnaireId = questionnaireId
}
if (id) {
formLoading.value = true
try {
const data = await QuestionApi.getQuestion(id)
formData.value = { ...data } as any
if (data.type === 1 || data.type === 2) {
optionList.value = parseOptions(data.options)
}
//
if (data.displayCondition) {
try {
const obj = JSON.parse(data.displayCondition)
conditionForm.enabled = true
conditionForm.field = obj.field || ''
conditionForm.operator = obj.operator || '='
conditionForm.value = obj.value || ''
} catch {}
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
//
if (formData.value.type === 1 || formData.value.type === 2) {
const normalOptions = optionList.value.filter(o => !o.isOther)
const otherOption = optionList.value.find(o => o.isOther)
//
if (otherOption) {
if (normalOptions.length < 1) {
message.error('"其他"选项外至少需要1个普通选项')
return
}
} else {
if (normalOptions.length < 2) {
message.error('请至少添加2个选项')
return
}
}
//
const emptyOption = normalOptions.find(o => !o.label.trim())
if (emptyOption) {
message.error('请完善所有选项的文字')
return
}
if (otherOption && !otherOption.label.trim()) {
message.error('请输入"其他"选项的提示文字')
return
}
formData.value.options = stringifyOptions(optionList.value)
}
//
if (formData.value.type === 5) {
formData.value.options = JSON.stringify({
min: formData.value.minValue || '',
max: formData.value.maxValue || ''
})
}
//
if (formData.value.type === 6) {
formData.value.options = JSON.stringify({
min: formData.value.minValue ?? '',
max: formData.value.maxValue ?? ''
})
}
//
if (formData.value.type === 4) {
formData.value.options = JSON.stringify({
min: formData.value.minValue ?? 1,
max: formData.value.maxValue ?? 5
})
}
formLoading.value = true
try {
const data = { ...formData.value } as any
if (formType.value === 'create') {
await QuestionApi.createQuestion(data)
message.success(t('common.createSuccess'))
} else {
await QuestionApi.updateQuestion(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
questionnaireId: formData.value.questionnaireId,
title: undefined,
type: undefined,
options: undefined,
score: undefined,
sort: optionList.value.length, //
isRequired: true,
partName: undefined,
helpText: undefined,
placeholder: undefined,
defaultValue: undefined,
autoFillType: 'NONE',
autoFillSource: undefined,
displayCondition: undefined,
minValue: undefined,
maxValue: undefined
}
optionList.value = []
conditionForm.enabled = false
conditionForm.field = ''
conditionForm.operator = '='
conditionForm.value = ''
pasteText.value = ''
formRef.value?.resetFields()
}
</script>
<style scoped>
/* 折叠面板样式 */
:deep(.el-collapse-item__header) {
font-weight: 600;
font-size: 15px;
padding-left: 16px;
}
:deep(.el-collapse-item__content) {
padding-bottom: 20px;
}
/* 快速粘贴区域 */
.quick-paste-section {
margin-bottom: 16px;
padding: 12px 16px;
background: linear-gradient(135deg, #f0f9eb 0%, #e8f5e9 100%);
border: 1px solid #c2e7b0;
border-radius: 6px;
}
.quick-paste-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
color: #67c23a;
}
/* 选项容器 */
.options-container {
padding: 0 16px;
}
.options-header {
display: flex;
align-items: center;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
margin-bottom: 8px;
font-size: 12px;
color: #909399;
}
.col-score {
width: 80px;
}
.col-label {
flex: 1;
}
.col-actions {
width: 60px;
text-align: center;
}
.option-drag-list {
min-height: 40px;
}
.option-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
margin-bottom: 8px;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 4px;
transition: all 0.2s;
}
.option-item:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.option-item.is-other {
background: #fffbf0;
border-color: #e6a23c;
}
.drag-handle {
cursor: grab;
color: #c0c4cc;
font-size: 16px;
}
.drag-handle:active {
cursor: grabbing;
}
.col-score-input {
width: 80px;
}
.col-label-input {
flex: 1;
}
.col-label-input.is-error {
--el-input-border-color: #f56c6c;
}
.add-buttons {
display: flex;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #e4e7ed;
}
.options-stats {
margin-top: 12px;
padding: 8px 12px;
background: #f4f4f5;
border-radius: 4px;
font-size: 12px;
color: #909399;
display: flex;
align-items: center;
gap: 6px;
}
.warning-text {
color: #e6a23c;
margin-left: 8px;
}
/* 条件构建器 */
.condition-builder {
width: 100%;
}
.condition-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.condition-text {
color: #606266;
font-size: 14px;
}
.condition-preview {
margin-top: 12px;
padding: 10px 14px;
background: #ecf5ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
font-size: 13px;
color: #409eff;
width: 100%;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.paste-preview {
margin-top: 16px;
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
color: #606266;
}
.preview-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 120px;
overflow-y: auto;
}
</style>

View File

@ -193,11 +193,11 @@
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" plain :icon="Plus" @click="addPartition">添加分区</el-button>
<el-button type="primary" plain :icon="Plus" @click="addPartition" v-hasPermi="['prison:question:create']">添加分区</el-button>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="savePartitions" type="primary">保存设置</el-button>
<el-button @click="savePartitions" type="primary" v-hasPermi="['prison:question:update']">保存设置</el-button>
<el-button @click="partDialogVisible = false">取消</el-button>
</template>
</Dialog>
@ -206,7 +206,7 @@
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { QuestionApi, Question } from '@/api/prison/question'
import QuestionForm from '../../question/QuestionForm.vue'
import QuestionForm from './QuestionForm.vue'
import { Folder, InfoFilled, Rank, Plus, Delete } from '@element-plus/icons-vue'
import draggable from 'vuedraggable'

View File

@ -110,10 +110,10 @@
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ dateFormatter(scope.row.createTime) }}
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<el-table-column label="操作" align="center" width="150" fixed="right">
<template #default="scope">
<el-button
type="primary"
@ -157,7 +157,7 @@
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { QuestionnaireApi, Questionnaire } from '@/api/prison/questionnaire'
import QuestionnaireForm from './QuestionnaireForm.vue'

View File

@ -21,7 +21,13 @@
/>
</el-select>
</el-form-item>
<el-form-item label="选择罪犯" prop="prisonerIds">
<!-- 预选罪犯模式只读显示 -->
<el-form-item v-if="preselectedPrisoner" label="选择罪犯">
<el-tag>{{ preselectedPrisoner.prisonerNo }} - {{ preselectedPrisoner.name }}</el-tag>
<span class="text-tip ml-10px">已选定</span>
</el-form-item>
<!-- 正常选择模式 -->
<el-form-item v-else label="选择罪犯" prop="prisonerIds">
<el-select
v-model="formData.prisonerIds"
placeholder="请选择罪犯"
@ -93,6 +99,9 @@ const formData = reactive({
remark: ''
})
//
const preselectedPrisoner = ref<{ id: number; prisonerNo: string; name: string } | null>(null)
const rules = {
questionnaireId: [{ required: true, message: '请选择问卷', trigger: 'change' }],
prisonerIds: [{ required: true, message: '请选择罪犯', trigger: 'change' }]
@ -101,7 +110,7 @@ const rules = {
/** 获取问卷列表 */
const getQuestionnaireList = async () => {
try {
const data = await QuestionnaireApi.getQuestionnairePage({ pageNo: 1, pageSize: 1000 })
const data = await QuestionnaireApi.getQuestionnairePage({ pageNo: 1, pageSize: 200 })
questionnaireList.value = data.list
} catch {}
}
@ -127,19 +136,32 @@ const searchPrisoners = async (keyword: string) => {
}
}
/** 打开弹窗 */
const open = () => {
/**
* @param prisoner 可选的预选罪犯信息从工作台打开时传入
*/
const open = (prisoner?: { id: number; prisonerNo: string; name: string }) => {
dialogVisible.value = true
title.value = '发起测评'
//
formData.questionnaireId = undefined
formData.prisonerIds = []
formData.deadline = ''
formData.remark = ''
//
if (prisoner) {
preselectedPrisoner.value = prisoner
formData.prisonerIds = [prisoner.id]
} else {
preselectedPrisoner.value = null
formData.prisonerIds = []
}
//
getQuestionnaireList()
//
prisonerList.value = []
//
if (!prisoner) {
prisonerList.value = []
}
}
/** 提交 */

View File

@ -153,10 +153,13 @@
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="160"
/>
<el-table-column label="操作" align="center" min-width="200px">
>
<template #default="scope">
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" min-width="200px" fixed="right">
<template #default="scope">
<el-button
v-if="scope.row.status === 1"
@ -235,7 +238,7 @@
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { QuestionnaireRecordApi, QuestionnaireRecord, QuestionnaireRecordPageParams } from '@/api/prison/questionnairerecord'
import { QuestionnaireApi } from '@/api/prison/questionnaire'
@ -286,7 +289,7 @@ const getList = async () => {
/** 获取问卷列表 */
const getQuestionnaireList = async () => {
try {
const data = await QuestionnaireApi.getQuestionnairePage({ pageNo: 1, pageSize: 1000 })
const data = await QuestionnaireApi.getQuestionnairePage({ pageNo: 1, pageSize: 200 })
questionnaireList.value = data.list
} catch {}
}

View File

@ -0,0 +1,113 @@
<template>
<el-dialog
v-model="dialogVisible"
title="导入评语"
width="500px"
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="评语分类" prop="categoryId">
<el-select
v-model="formData.categoryId"
placeholder="请选择分类"
class="!w-100%"
>
<el-option
v-for="item in categoryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="评语内容" prop="contents">
<el-input
v-model="formData.contentsStr"
type="textarea"
:rows="8"
placeholder="每行一条评语"
@blur="handleContentsChange"
/>
<div class="tip">每行输入一条评语支持批量导入</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">
导入
</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { QuickCommentApi } from '@/api/prison/quick-comment'
defineOptions({ name: 'ImportDialog' })
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const loading = ref(false)
const formRef = ref()
const categoryOptions = ref<{ id: number; name: string }[]>([])
const formData = reactive({
categoryId: undefined as number | undefined,
contents: [] as string[],
contentsStr: ''
})
const rules = {
categoryId: [{ required: true, message: '请选择评语分类', trigger: 'change' }],
contents: [{ required: true, message: '请输入评语内容', trigger: 'blur' }]
}
/** 处理内容变化 */
const handleContentsChange = () => {
formData.contents = formData.contentsStr
.split('\n')
.map((s) => s.trim())
.filter((s) => s.length > 0)
}
/** 打开弹窗 */
const open = (options: { id: number; name: string }[]) => {
dialogVisible.value = true
categoryOptions.value = options
formData.categoryId = undefined
formData.contentsStr = ''
formData.contents = []
}
/** 提交 */
const handleSubmit = async () => {
await formRef.value?.validate()
handleContentsChange()
if (formData.contents.length === 0) {
return message.warning('请输入评语内容')
}
loading.value = true
try {
await QuickCommentApi.importComments({
categoryId: formData.categoryId!,
contents: formData.contents
})
message.success(`成功导入 ${formData.contents.length} 条评语`)
dialogVisible.value = false
emit('success')
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>
<style lang="scss" scoped>
.tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑评语' : '新增评语'"
width="600px"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="100px"
>
<el-form-item label="评语分类" prop="categoryId">
<el-select
v-model="formData.categoryId"
placeholder="请选择分类"
class="!w-100%"
>
<el-option
v-for="item in categoryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="评语内容" prop="content">
<el-input
v-model="formData.content"
type="textarea"
:rows="5"
placeholder="请输入评语内容"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="9999" controls-position="right" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { QuickCommentApi, QuickComment, CommentCategoryApi } from '@/api/prison/quick-comment'
defineOptions({ name: 'QuickCommentForm' })
const emit = defineEmits(['success'])
const dialogVisible = ref(false)
const loading = ref(false)
const isEdit = ref(false)
const formRef = ref()
const categoryOptions = ref<{ id: number; name: string }[]>([])
const formData = reactive({
id: undefined as number | undefined,
categoryId: undefined as number | undefined,
content: '',
sort: 0,
status: 1
})
const rules = {
categoryId: [{ required: true, message: '请选择评语分类', trigger: 'change' }],
content: [{ required: true, message: '请输入评语内容', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
/** 获取分类列表 */
const getCategoryList = async () => {
const data = await CommentCategoryApi.getList({ status: 1 })
categoryOptions.value = data.map((item: any) => ({
id: item.id,
name: item.name
}))
}
/** 打开弹窗 */
const open = async (id?: number) => {
dialogVisible.value = true
isEdit.value = !!id
formData.id = id
formData.categoryId = undefined
formData.content = ''
formData.sort = 0
formData.status = 1
await getCategoryList()
if (id) {
const data = await QuickCommentApi.get(id)
formData.categoryId = data.categoryId
formData.content = data.content
formData.sort = data.sort
formData.status = data.status
}
}
/** 提交 */
const handleSubmit = async () => {
await formRef.value?.validate()
loading.value = true
try {
if (isEdit.value) {
await QuickCommentApi.update(formData)
} else {
await QuickCommentApi.create(formData)
}
message.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false
emit('success')
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,264 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="评语分类" prop="categoryId">
<el-select
v-model="queryParams.categoryId"
placeholder="请选择分类"
clearable
class="!w-160px"
>
<el-option
v-for="item in categoryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="评语内容" prop="content">
<el-input
v-model="queryParams.content"
placeholder="请输入评语内容"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="handleCreate"
v-hasPermi="['prison:quick-comment:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增评语
</el-button>
<el-button
type="success"
plain
@click="handleImport"
v-hasPermi="['prison:quick-comment:import']"
>
<Icon icon="ep:upload" class="mr-5px" /> 导入
</el-button>
<el-button
type="warning"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:quick-comment:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 评语列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="分类" prop="categoryName" width="120" align="center" />
<el-table-column label="评语内容" prop="content" min-width="400" show-overflow-tooltip />
<el-table-column label="使用次数" prop="usageCount" width="100" align="center" />
<el-table-column label="排序" prop="sort" width="80" align="center" />
<el-table-column label="状态" prop="status" width="80" align="center">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_COMMON_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" width="180" align="center" />
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button
link
type="primary"
@click="handleUpdate(row)"
v-hasPermi="['prison:quick-comment:update']"
>
<Icon icon="ep:edit" /> 编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(row.id)"
v-hasPermi="['prison:quick-comment:delete']"
>
<Icon icon="ep:delete" /> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 评语表单弹窗 -->
<QuickCommentForm ref="formRef" @success="getList" />
<!-- 导入弹窗 -->
<ImportDialog ref="importDialogRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { QuickCommentApi, QuickCommentPageParams, CommentCategoryApi, QuickComment } from '@/api/prison/quick-comment'
import QuickCommentForm from './QuickCommentForm.vue'
import ImportDialog from './ImportDialog.vue'
defineOptions({ name: 'PrisonQuickComment' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const exportLoading = ref(false)
const list = ref<QuickComment[]>([])
const total = ref(0)
const ids = ref<number[]>([])
const categoryOptions = ref<{ id: number; name: string }[]>([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
categoryId: undefined as number | undefined,
content: undefined as string | undefined,
status: undefined as number | undefined
})
const queryFormRef = ref()
/** 获取评语列表 */
const getList = async () => {
loading.value = true
try {
const data = await QuickCommentApi.getPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 获取分类列表 */
const getCategoryList = async () => {
const data = await CommentCategoryApi.getList({ status: 1 })
categoryOptions.value = data.map((item: CommentCategoryApi) => ({
id: item.id,
name: item.name
}))
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
getList()
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: QuickComment[]) => {
ids.value = selection.map((item) => item.id)
}
/** 新增评语 */
const formRef = ref()
const handleCreate = () => {
formRef.value?.open()
}
/** 编辑评语 */
const handleUpdate = (row: QuickComment) => {
formRef.value?.open(row.id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await QuickCommentApi.delete(id)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 批量删除 */
const handleBatchDelete = async () => {
if (ids.value.length === 0) {
return message.warning('请选择要删除的数据')
}
try {
await message.delConfirm()
await QuickCommentApi.deleteList(ids.value)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 导入评语 */
const importDialogRef = ref()
const handleImport = () => {
importDialogRef.value?.open(categoryOptions.value)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await QuickCommentApi.export(queryParams)
download.excel(data, '快捷评语.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
getCategoryList()
})
</script>

View File

@ -70,7 +70,7 @@
<template #header>
<div class="card-header">
<span>释放登记列表</span>
<el-button type="primary" @click="handleCreate" v-hasPermi="'prison:release:create'">
<el-button type="primary" @click="handleCreate" v-hasPermi="['prison:release:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增释放登记
</el-button>
</div>
@ -95,7 +95,7 @@
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="'prison:release:query'">
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="['prison:release:query']">
详情
</el-button>
<el-button
@ -103,7 +103,7 @@
type="primary"
@click="handleEdit(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:update'"
v-hasPermi="['prison:release:update']"
>
编辑
</el-button>
@ -112,7 +112,7 @@
type="success"
@click="handleDoRelease(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:update'"
v-hasPermi="['prison:release:update']"
>
执行释放
</el-button>
@ -121,7 +121,7 @@
type="warning"
@click="handleCancel(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:update'"
v-hasPermi="['prison:release:update']"
>
取消
</el-button>
@ -130,7 +130,7 @@
type="danger"
@click="handleDelete(row)"
v-if="row.status === 1"
v-hasPermi="'prison:release:delete'"
v-hasPermi="['prison:release:delete']"
>
删除
</el-button>
@ -153,16 +153,18 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import ReleaseForm from './ReleaseForm.vue'
import { ReleaseApi, type ReleasePageReqVO } from '@/api/prison/release'
import { ReleaseForm } from './ReleaseForm.vue'
import { ReleaseApi, type ReleasePageReqVO, type Release } from '@/api/prison/release'
defineOptions({ name: 'PrisonRelease' })
const { t } = useI18n()
const message = useMessage() // 使 useMessage
const loading = ref(false)
const tableData = ref([])
const tableData = ref<Release[]>([])
const total = ref(0)
const dateRange = ref([])
const dateRange = ref<string[]>([])
const searchForm = reactive<ReleasePageReqVO>({
prisonerNo: '',
@ -208,7 +210,7 @@ const handleReset = () => {
}
/** 日期范围变化 */
const handleDateChange = (val: any) => {
const handleDateChange = (val: string[] | null) => {
if (val) {
searchForm.actualReleaseDateStart = val[0]
searchForm.actualReleaseDateEnd = val[1]
@ -224,46 +226,43 @@ const handleCreate = () => {
}
/** 编辑 */
const handleEdit = (row: any) => {
const handleEdit = (row: Release) => {
formRef.value.open(row.id)
}
/** 详情 */
const handleDetail = (row: any) => {
const handleDetail = (row: Release) => {
formRef.value.open(row.id, true)
}
/** 执行释放 */
const handleDoRelease = (row: any) => {
ElMessageBox.confirm(`确认要执行释放罪犯【${row.prisonerName}】吗?`, '操作确认', {
type: 'warning'
}).then(async () => {
const handleDoRelease = async (row: Release) => {
try {
await message.confirm(`确认要执行释放罪犯【${row.prisonerName}】吗?`)
await ReleaseApi.doRelease(row.id)
ElMessage.success('执行释放成功')
message.success('执行释放成功')
getPage()
})
} catch {}
}
/** 取消释放 */
const handleCancel = (row: any) => {
ElMessageBox.confirm(`确认要取消释放登记吗?`, '操作确认', {
type: 'warning'
}).then(async () => {
const handleCancel = async (row: Release) => {
try {
await message.confirm('确认要取消释放登记吗?')
await ReleaseApi.cancelRelease(row.id)
ElMessage.success('取消释放成功')
message.success('取消释放成功')
getPage()
})
} catch {}
}
/** 删除 */
const handleDelete = (row: any) => {
ElMessageBox.confirm(`确认要删除该释放登记吗?`, '操作确认', {
type: 'warning'
}).then(async () => {
const handleDelete = async (row: Release) => {
try {
await message.confirm('确认要删除该释放登记吗?')
await ReleaseApi.deleteRelease(row.id)
ElMessage.success('删除成功')
message.success('删除成功')
getPage()
})
} catch {}
}
/** 状态样式 */

View File

@ -0,0 +1,311 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="900px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="模板名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入模板名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="模板类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择模板类型" class="!w-100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_REPORT_TEMPLATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="标题格式" prop="titleFormat">
<el-input v-model="formData.titleFormat" placeholder="如:{罪犯姓名}服刑期间综合评估报告" />
<div class="form-tip">可使用变量{罪犯姓名}{评估日期}{监区名称}</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="设为默认" prop="isDefault">
<el-switch v-model="formData.isDefault" />
</el-form-item>
</el-col>
</el-row>
<!-- 评估维度配置 -->
<el-form-item label="评估维度" prop="dimensions">
<div class="dimension-container">
<div class="dimension-header">
<el-button type="primary" link @click="handleAddDimension">
<Icon icon="ep:plus" class="mr-5px" /> 添加维度
</el-button>
</div>
<el-table :data="formData.dimensions" border style="width: 100%">
<el-table-column label="排序" width="80">
<template #default="{ $index }">
<el-input-number
v-model="formData.dimensions[$index].sort"
:min="0"
:max="999"
size="small"
/>
</template>
</el-table-column>
<el-table-column label="维度名称" min-width="150">
<template #default="{ row }">
<el-input v-model="row.name" placeholder="如:犯罪情况分析" size="small" />
</template>
</el-table-column>
<el-table-column label="数据源" min-width="200">
<template #default="{ row }">
<el-select
v-model="row.dataSources"
multiple
placeholder="选择数据源"
size="small"
class="!w-100%"
>
<el-option
v-for="ds in dataSourceOptions"
:key="ds.value"
:label="ds.label"
:value="ds.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="AI生成" width="100" align="center">
<template #default="{ row }">
<el-switch v-model="row.enableAi" size="small" />
</template>
</el-table-column>
<el-table-column label="输出格式" width="120">
<template #default="{ row }">
<el-select v-model="row.outputFormat" size="small" class="!w-100%">
<el-option label="文本" value="text" />
<el-option label="段落" value="paragraph" />
<el-option label="列表" value="list" />
</el-select>
</template>
</el-table-column>
<el-table-column label="编辑器" width="120">
<template #default="{ row }">
<el-select v-model="row.editorType" size="small" class="!w-100%">
<el-option label="文本框" value="text" />
<el-option label="富文本" value="richtext" />
<el-option label="下拉选择" value="select" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="handleRemoveDimension($index)">
<Icon icon="ep:delete" />
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-form-item>
<!-- AI提示词配置 -->
<el-form-item label="AI提示词" prop="aiPromptConfig">
<el-input
v-model="formData.aiPromptConfig"
type="textarea"
:rows="6"
placeholder="请输入AI提示词模板用于指导AI生成评估内容"
/>
<div class="form-tip">
提示可以使用变量 {罪犯档案数据}{考核数据}{消费数据}
</div>
</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="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { ReportTemplateApi, ReportTemplate, ReportDimension } from '@/api/prison/report'
/** 评估报告模板 表单 */
defineOptions({ name: 'ReportTemplateForm' })
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formRef = ref()
//
const dataSourceOptions = [
{ value: 'prisoner', label: '罪犯档案' },
{ value: 'consumption', label: '消费记录' },
{ value: 'score', label: '计分考核' },
{ value: 'questionnaire_record', label: '问卷答题' },
{ value: 'risk_assessment', label: '风险评估' },
{ value: 'violation', label: '违规记录' },
{ value: 'reward', label: '奖励记录' },
{ value: 'visit', label: '会见记录' },
{ value: 'labor', label: '劳动数据' },
{ value: 'family', label: '家庭帮教' },
{ value: 'psychology', label: '心理测评' }
]
const formData = ref({
id: undefined,
name: '',
type: undefined,
titleFormat: '',
dimensions: [] as ReportDimension[],
aiPromptConfig: '',
styleConfig: '',
status: 1,
isDefault: false,
version: 1,
remark: ''
})
const formRules = reactive({
name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
type: [{ required: true, message: '模板类型不能为空', trigger: 'change' }],
titleFormat: [{ required: true, message: '标题格式不能为空', trigger: 'blur' }]
})
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
if (id) {
formLoading.value = true
try {
const data = await ReportTemplateApi.getTemplate(id)
formData.value = {
...data,
dimensions: data.dimensions || []
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open })
/** 提交表单 */
const emit = defineEmits(['success'])
const submitForm = async () => {
await formRef.value.validate()
formLoading.value = true
try {
const data = formData.value
if (formType.value === 'create') {
await ReportTemplateApi.createTemplate(data)
message.success(t('common.createSuccess'))
} else {
await ReportTemplateApi.updateTemplate(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: '',
type: undefined,
titleFormat: '',
dimensions: [
{ name: '基本信息', dataSources: ['prisoner'], outputFormat: 'text', enableAi: false, editorType: 'text', sort: 1 },
{ name: '服刑表现评估', dataSources: ['score'], outputFormat: 'paragraph', enableAi: true, editorType: 'richtext', sort: 2 },
{ name: '消费行为分析', dataSources: ['consumption'], outputFormat: 'paragraph', enableAi: true, editorType: 'richtext', sort: 3 },
{ name: '综合评估结论', dataSources: ['prisoner', 'score', 'risk_assessment'], outputFormat: 'paragraph', enableAi: true, editorType: 'richtext', sort: 4 }
],
aiPromptConfig: '',
styleConfig: '',
status: 1,
isDefault: false,
version: 1,
remark: ''
}
formRef.value?.resetFields()
}
/** 添加维度 */
const handleAddDimension = () => {
formData.value.dimensions.push({
name: '',
dataSources: [],
outputFormat: 'paragraph',
enableAi: true,
editorType: 'richtext',
sort: formData.value.dimensions.length + 1
})
}
/** 删除维度 */
const handleRemoveDimension = (index: number) => {
formData.value.dimensions.splice(index, 1)
}
</script>
<style lang="scss" scoped>
.dimension-container {
width: 100%;
.dimension-header {
margin-bottom: 10px;
}
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 5px;
line-height: 1.4;
}
</style>

View File

@ -0,0 +1,309 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="90px"
>
<el-form-item label="模板名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入模板名称"
clearable
@keyup.enter="handleQuery"
class="!w-200px"
/>
</el-form-item>
<el-form-item label="模板类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择"
clearable
class="!w-150px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_REPORT_TEMPLATE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_COMMON_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['prison:report-template:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增模板
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:report-template:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 模板列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="模板名称" prop="name" min-width="180" />
<el-table-column label="模板类型" prop="type" width="150">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_REPORT_TEMPLATE_TYPE" :value="row.type" />
</template>
</el-table-column>
<el-table-column label="评估维度" prop="dimensionCount" width="100" align="center">
<template #default="{ row }">
<el-tag>{{ row.dimensions?.length || 0 }} </el-tag>
</template>
</el-table-column>
<el-table-column label="版本号" prop="version" width="100" align="center">
<template #default="{ row }">
<el-tag type="info" size="small">v{{ row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="100" align="center">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_COMMON_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="默认模板" prop="isDefault" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.isDefault" type="success" size="small"></el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" width="180" align="center" />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button
link
type="primary"
@click="openForm('update', row.id)"
v-hasPermi="['prison:report-template:update']"
>
<Icon icon="ep:edit" /> 修改
</el-button>
<el-button
link
type="success"
@click="handleCopy(row.id)"
v-hasPermi="['prison:report-template:create']"
>
<Icon icon="ep:copy-document" /> 复制
</el-button>
<el-button
link
:type="row.status === 1 ? 'warning' : 'success'"
@click="handleToggleStatus(row)"
v-hasPermi="['prison:report-template:update']"
>
<Icon :icon="row.status === 1 ? 'ep:video-pause' : 'ep:video-play'" />
{{ row.status === 1 ? '停用' : '启用' }}
</el-button>
<el-button
link
v-if="!row.isDefault"
type="primary"
@click="handleSetDefault(row.id)"
v-hasPermi="['prison:report-template:update']"
>
设为默认
</el-button>
<el-button
link
type="danger"
@click="handleDelete(row.id)"
v-hasPermi="['prison:report-template:delete']"
>
<Icon icon="ep:delete" /> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改模板 -->
<ReportTemplateForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { ReportTemplateApi, ReportTemplate } from '@/api/prison/report'
import ReportTemplateForm from './ReportTemplateForm.vue'
defineOptions({ name: 'PrisonReportTemplate' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const exportLoading = ref(false)
const list = ref<ReportTemplate[]>([])
const total = ref(0)
const ids = ref<number[]>([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
type: undefined,
status: undefined
})
const queryFormRef = ref()
/** 获取模板列表 */
const getList = async () => {
loading.value = true
try {
const data = await ReportTemplateApi.getTemplatePage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
getList()
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: ReportTemplate[]) => {
ids.value = selection.map((item) => item.id)
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 复制模板 */
const handleCopy = async (id: number) => {
try {
await message.confirm(t('prison.reportTemplate.copyConfirm'))
await ReportTemplateApi.copyTemplate(id)
message.success(t('common.copySuccess'))
getList()
} catch {}
}
/** 切换状态 */
const handleToggleStatus = async (row: ReportTemplate) => {
try {
await message.confirm(
row.status === 1
? t('prison.reportTemplate.disableConfirm')
: t('prison.reportTemplate.enableConfirm')
)
await ReportTemplateApi.updateStatus(row.id, row.status === 1 ? 0 : 1)
message.success(t('common.operationSuccess'))
getList()
} catch {}
}
/** 设为默认 */
const handleSetDefault = async (id: number) => {
try {
await message.confirm(t('prison.reportTemplate.setDefaultConfirm'))
await ReportTemplateApi.setDefault(id)
message.success(t('prison.reportTemplate.setDefaultSuccess'))
getList()
} catch {}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await ReportTemplateApi.deleteTemplate(id)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 批量删除 */
const handleBatchDelete = async () => {
if (ids.value.length === 0) {
return message.warning('请选择要删除的数据')
}
try {
await message.delConfirm()
await ReportTemplateApi.deleteTemplateList(ids.value)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await ReportTemplateApi.exportTemplate(queryParams)
download.excel(data, '评估报告模板.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,230 @@
<template>
<Dialog title="创建评估报告" v-model="dialogVisible" width="600px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="选择罪犯" prop="prisonerId">
<el-select
v-model="formData.prisonerId"
placeholder="请选择罪犯"
filterable
remote
:remote-method="searchPrisoners"
:loading="prisonerLoading"
class="!w-100%"
>
<el-option
v-for="item in prisonerList"
:key="item.id"
:label="`${item.name} (${item.prisonerNo})`"
:value="item.id"
>
<div class="prisoner-option">
<span>{{ item.name }}</span>
<span class="no">{{ item.prisonerNo }}</span>
<el-tag size="small" :type="getRiskLevelType(item.riskLevel)">
{{ getRiskLevelLabel(item.riskLevel) }}
</el-tag>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="选择模板" prop="templateId">
<el-select
v-model="formData.templateId"
placeholder="请选择报告模板"
class="!w-100%"
>
<el-option
v-for="item in templateList"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span>{{ item.name }}</span>
<el-tag size="small" type="info" class="ml-10px">
{{ getTemplateTypeLabel(item.type) }}
</el-tag>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="报告日期" prop="reportDate">
<el-date-picker
v-model="formData.reportDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择报告日期"
class="!w-100%"
/>
</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="dialogVisible = false"> </el-button>
<el-button @click="handleCreate" type="primary" :loading="formLoading" :disabled="!canCreate">
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { ReportTemplateApi, ReportTemplate, ReportApi } from '@/api/prison/report'
import { PrisonerSelectApi, PrisonerBrief } from '@/api/prison/report'
/** 创建报告弹窗 */
defineOptions({ name: 'PrisonCreateReportDialog' })
const message = useMessage()
const dialogVisible = ref(false)
const formLoading = ref(false)
const prisonerLoading = ref(false)
const prisonerList = ref<PrisonerBrief[]>([])
const templateList = ref<ReportTemplate[]>([])
const formData = ref({
prisonerId: undefined as number | undefined,
templateId: undefined as number | undefined,
reportDate: '',
remark: ''
})
const formRules = {
prisonerId: [{ required: true, message: '请选择罪犯', trigger: 'change' }],
templateId: [{ required: true, message: '请选择模板', trigger: 'change' }],
reportDate: [{ required: true, message: '请选择报告日期', trigger: 'change' }]
}
const formRef = ref()
//
const canCreate = computed(() => {
return formData.value.prisonerId && formData.value.templateId && formData.value.reportDate
})
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
resetForm()
loadTemplates()
}
defineExpose({ open })
/** 加载模板列表 */
const loadTemplates = async () => {
const data = await ReportTemplateApi.getTemplatePage({ pageNo: 1, pageSize: 100, status: 1 })
templateList.value = data.list
}
/** 搜索罪犯 */
const searchPrisoners = async (keyword: string) => {
if (!keyword) {
prisonerList.value = []
return
}
prisonerLoading.value = true
try {
prisonerList.value = await PrisonerSelectApi.getAllPrisoners({
name: keyword,
prisonerNo: keyword
})
} finally {
prisonerLoading.value = false
}
}
/** 获取风险等级类型 */
const getRiskLevelType = (level?: number): string => {
const map: Record<number, string> = {
1: 'success',
2: 'warning',
3: 'danger',
4: 'danger'
}
return level ? map[level] || 'info' : 'info'
}
/** 获取风险等级标签 */
const getRiskLevelLabel = (level?: number): string => {
const map: Record<number, string> = {
1: '低风险',
2: '中风险',
3: '高风险',
4: '极高风险'
}
return level ? map[level] || '-' : '未评估'
}
/** 获取模板类型标签 */
const getTemplateTypeLabel = (type: number): string => {
const dict = getIntDictOptions(DICT_TYPE.PRISON_REPORT_TEMPLATE_TYPE).find((d: any) => d.value === type)
return dict?.label || '-'
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
prisonerId: undefined,
templateId: undefined,
reportDate: '',
remark: ''
}
formRef.value?.resetFields()
}
/** 创建报告 */
const handleCreate = async () => {
await formRef.value.validate()
formLoading.value = true
try {
const data = await ReportApi.createReport({
prisonerId: formData.value.prisonerId!,
templateId: formData.value.templateId!,
reportDate: formData.value.reportDate,
remark: formData.value.remark
} as any)
dialogVisible.value = false
message.success('创建成功')
emit('success', data)
} finally {
formLoading.value = false
}
}
const emit = defineEmits(['success'])
</script>
<style lang="scss" scoped>
.prisoner-option {
display: flex;
align-items: center;
gap: 10px;
.no {
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
.ml-10px {
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,222 @@
<template>
<Dialog title="快捷评语" v-model="dialogVisible" width="700px">
<div class="quick-comment-dialog">
<!-- 搜索区 -->
<div class="search-area">
<el-input
v-model="searchKeyword"
placeholder="搜索评语关键词"
prefix-icon="Search"
clearable
@input="handleSearch"
class="search-input"
/>
</div>
<!-- 分类标签 -->
<div class="category-tabs">
<el-tag
:type="selectedCategoryId === null ? 'primary' : 'info'"
@click="selectCategory(null)"
class="category-tag"
>
全部
</el-tag>
<el-tag
v-for="category in categoryList"
:key="category.id"
:type="selectedCategoryId === category.id ? 'primary' : 'info'"
@click="selectCategory(category.id)"
class="category-tag"
>
{{ category.name }}
</el-tag>
</div>
<!-- 评语列表 -->
<div class="comment-list" v-loading="loading">
<div
v-for="comment in commentList"
:key="comment.id"
class="comment-item"
@click="handleSelect(comment)"
>
<div class="comment-content">{{ comment.content }}</div>
<div class="comment-meta">
<span class="usage-count">使用 {{ comment.usageCount }} </span>
<el-tag size="small" type="info" v-if="comment.categoryName">
{{ comment.categoryName }}
</el-tag>
</div>
</div>
<el-empty v-if="!loading && commentList.length === 0" description="暂无评语" />
</div>
<!-- 分页 -->
<div class="pagination-area" v-if="commentList.length > 0">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
small
layout="total, sizes, prev, pager, next"
@size-change="loadComments"
@current-change="loadComments"
/>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { QuickCommentApi, QuickComment, CommentCategory } from '@/api/prison/report'
/** 快捷评语弹窗 */
defineOptions({ name: 'PrisonQuickCommentDialog' })
const props = defineProps<{
categoryType?: number //
}>()
const message = useMessage()
const dialogVisible = ref(false)
const loading = ref(false)
const searchKeyword = ref('')
const selectedCategoryId = ref<number | null>(null)
const categoryList = ref<CommentCategory[]>([])
const commentList = ref<QuickComment[]>([])
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
/** 打开弹窗 */
const open = () => {
dialogVisible.value = true
searchKeyword.value = ''
selectedCategoryId.value = null
page.value = 1
loadCategories()
loadComments()
}
defineExpose({ open })
/** 加载分类列表 */
const loadCategories = async () => {
try {
const data = await QuickCommentApi.getCategoryList({ type: props.categoryType })
categoryList.value = data
} catch {}
}
/** 加载评语列表 */
const loadComments = async () => {
loading.value = true
try {
const data = await QuickCommentApi.getCommentPage({
pageNo: page.value,
pageSize: pageSize.value,
categoryId: selectedCategoryId.value || undefined,
keyword: searchKeyword.value
})
commentList.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索 */
const handleSearch = () => {
page.value = 1
loadComments()
}
/** 选择分类 */
const selectCategory = (categoryId: number | null) => {
selectedCategoryId.value = categoryId
page.value = 1
loadComments()
}
/** 选择评语 */
const handleSelect = (comment: QuickComment) => {
emit('insert', comment.content)
dialogVisible.value = false
}
const emit = defineEmits(['insert'])
</script>
<style lang="scss" scoped>
.quick-comment-dialog {
.search-area {
margin-bottom: 15px;
.search-input {
width: 100%;
}
}
.category-tabs {
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 8px;
.category-tag {
cursor: pointer;
}
}
.comment-list {
max-height: 400px;
overflow-y: auto;
.comment-item {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #409eff;
background: #ecf5ff;
}
.comment-content {
font-size: 14px;
color: #303133;
line-height: 1.6;
margin-bottom: 8px;
}
.comment-meta {
display: flex;
justify-content: space-between;
align-items: center;
.usage-count {
font-size: 12px;
color: #909399;
}
}
}
}
.pagination-area {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@ -0,0 +1,349 @@
<template>
<Dialog title="评估报告预览" v-model="dialogVisible" width="900px">
<div v-loading="loading" class="report-preview">
<!-- 报告信息头部 -->
<div class="report-header" v-if="report">
<h1 class="report-title">{{ report.title }}</h1>
<div class="report-meta">
<span class="meta-item">
<span class="label">报告编号</span>
<span class="value">{{ report.reportNo }}</span>
</span>
<span class="meta-item">
<span class="label">罪犯姓名</span>
<span class="value">{{ report.prisonerName }}</span>
</span>
<span class="meta-item">
<span class="label">罪犯编号</span>
<span class="value">{{ report.prisonerNo }}</span>
</span>
<span class="meta-item">
<span class="label">评估日期</span>
<span class="value">{{ report.reportDate }}</span>
</span>
<span class="meta-item">
<span class="label">模板类型</span>
<span class="value">{{ report.templateName }}</span>
</span>
<span class="meta-item">
<span class="label">风险等级</span>
<el-tag v-if="report.riskLevel" :type="getRiskLevelType(report.riskLevel)">
{{ getRiskLevelLabel(report.riskLevel) }}
</el-tag>
<span v-else>-</span>
</span>
</div>
<!-- 状态标签 -->
<div class="status-bar">
<el-tag :type="getStatusType(report.status)" size="large">
{{ getStatusLabel(report.status) }}
</el-tag>
<el-tag v-if="report.isAiGenerated" type="success" size="small" class="ai-tag">
AI生成
</el-tag>
<el-tag v-if="report.signature" type="primary" size="small" class="signed-tag">
已签名
</el-tag>
</div>
</div>
<!-- 报告内容 -->
<div class="report-content" v-if="report">
<!-- 维度内容 -->
<div
v-for="dimension in report.dimensions"
:key="dimension.dimensionId"
class="dimension-section"
>
<h3 class="dimension-title">
{{ dimension.dimensionName }}
<el-tag
v-if="dimension.isAiGenerated"
type="success"
size="small"
class="ai-badge"
>
AI生成
</el-tag>
<el-tag
v-else-if="dimension.lastModifyTime"
type="info"
size="small"
>
已修改
</el-tag>
</h3>
<div class="dimension-content">
{{ dimension.content || '暂无内容' }}
</div>
</div>
<!-- 综合结论 -->
<div v-if="report.conclusion" class="conclusion-section">
<h3 class="section-title">综合结论</h3>
<div class="section-content">{{ report.conclusion }}</div>
</div>
<!-- 改造建议 -->
<div v-if="report.suggestions" class="suggestions-section">
<h3 class="section-title">改造建议</h3>
<div class="section-content">{{ report.suggestions }}</div>
</div>
<!-- 附件列表 -->
<div v-if="report.attachments?.length" class="attachments-section">
<h3 class="section-title">附件</h3>
<el-upload
:file-list="attachmentList"
:disabled="true"
list-type="picture-card"
/>
</div>
<!-- 审核信息 -->
<div v-if="report.reviewerName" class="review-info">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="审核人">
{{ report.reviewerName }}
</el-descriptions-item>
<el-descriptions-item label="审核时间">
{{ report.reviewTime }}
</el-descriptions-item>
<el-descriptions-item v-if="report.reviewComment" label="审核意见" :span="2">
{{ report.reviewComment }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button
v-if="report"
type="primary"
@click="handleExport('pdf')"
:loading="exportLoading"
>
<Icon icon="ep:download" class="mr-5px" /> 导出PDF
</el-button>
<el-button
v-if="report"
type="success"
@click="handleExport('word')"
:loading="exportLoading"
>
<Icon icon="ep:download" class="mr-5px" /> 导出Word
</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { ReportApi, type Report } from '@/api/prison/report'
import type { UploadUserFile } from 'element-plus'
/** 报告预览弹窗 */
defineOptions({ name: 'PrisonReportPreviewDialog' })
const message = useMessage()
const dialogVisible = ref(false)
const loading = ref(false)
const exportLoading = ref(false)
const report = ref<Report | null>(null)
const attachmentList = ref<UploadUserFile[]>([])
/** 打开弹窗 */
const open = async (id: number) => {
dialogVisible.value = true
loadReport(id)
}
defineExpose({ open })
/** 加载报告详情 */
const loadReport = async (id: number) => {
loading.value = true
try {
report.value = await ReportApi.getReport(id)
} finally {
loading.value = false
}
}
/** 获取风险等级类型 */
const getRiskLevelType = (level: number): string => {
const map: Record<number, string> = {
1: 'success',
2: 'warning',
3: 'danger',
4: 'danger'
}
return map[level] || 'info'
}
/** 获取风险等级标签 */
const getRiskLevelLabel = (level: number): string => {
const map: Record<number, string> = {
1: '低风险',
2: '中风险',
3: '高风险',
4: '极高风险'
}
return map[level] || '-'
}
/** 获取状态类型 */
const getStatusType = (status: number): string => {
const map: Record<number, string> = {
1: 'info',
2: 'warning',
3: 'success',
4: 'danger'
}
return map[status] || 'info'
}
/** 获取状态标签 */
const getStatusLabel = (status: number): string => {
const dict = getIntDictOptions(DICT_TYPE.PRISON_REPORT_STATUS).find((d: any) => d.value === status)
return dict?.label || '-'
}
/** 导出报告 */
const handleExport = async (format: 'pdf' | 'word') => {
if (!report.value) return
exportLoading.value = true
try {
const data = await ReportApi.exportReport(report.value.id, format)
const fileName = `${report.value.reportNo}_${report.value.prisonerName}.${format === 'pdf' ? 'pdf' : 'docx'}`
// 使
message.success('导出成功')
} catch {
} finally {
exportLoading.value = false
}
}
</script>
<style lang="scss" scoped>
.report-preview {
max-height: 70vh;
overflow-y: auto;
padding: 20px;
background: #fafafa;
border-radius: 4px;
}
.report-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
.report-title {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 20px;
}
.report-meta {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
margin-bottom: 15px;
.meta-item {
.label {
color: #666;
}
.value {
color: #333;
font-weight: 500;
}
}
}
.status-bar {
display: flex;
justify-content: center;
gap: 10px;
.ai-tag {
background: #e8f5e9;
color: #2e7d32;
}
.signed-tag {
background: #e3f2fd;
color: #1565c0;
}
}
}
.report-content {
background: #fff;
padding: 30px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.dimension-section {
margin-bottom: 25px;
.dimension-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
.ai-badge {
background: #e8f5e9;
color: #2e7d32;
}
}
.dimension-content {
font-size: 14px;
line-height: 1.8;
color: #555;
text-indent: 2em;
white-space: pre-wrap;
}
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #e0e0e0;
}
.section-content {
font-size: 14px;
line-height: 1.8;
color: #555;
white-space: pre-wrap;
}
.attachments-section {
margin-top: 25px;
}
.review-info {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
}
</style>

View File

@ -0,0 +1,284 @@
<template>
<Dialog title="历史版本" v-model="dialogVisible" width="900px">
<div class="version-history-dialog">
<!-- 版本列表 -->
<div class="version-list" v-loading="loading">
<el-table
:data="versionList"
stripe
@row-click="handleSelectVersion"
:row-class-name="getRowClassName"
>
<el-table-column label="版本号" width="100" align="center">
<template #default="{ row }">
<el-tag type="info" size="small">v{{ row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column label="修改人" prop="modifierName" width="120" align="center" />
<el-table-column label="修改时间" prop="modifyTime" width="180" align="center" />
<el-table-column label="版本备注" prop="comment" min-width="150" show-overflow-tooltip />
<el-table-column label="操作" width="150" align="center">
<template #default="{ row }">
<el-button
type="primary"
link
size="small"
@click.stop="handleCompare(row)"
:disabled="!selectedVersion"
>
对比
</el-button>
<el-button
type="success"
link
size="small"
@click.stop="handleRestore(row)"
:disabled="row.version === currentVersion"
>
恢复
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && versionList.length === 0" description="暂无历史版本" />
</div>
<!-- 版本对比区 -->
<div v-if="compareMode" class="version-compare">
<div class="compare-header">
<h4>版本对比</h4>
<el-button type="primary" link @click="compareMode = false">关闭对比</el-button>
</div>
<div class="compare-content">
<div class="compare-panel left-panel">
<div class="panel-header">
<span>v{{ selectedVersion?.version }}</span>
<span class="modifier">{{ selectedVersion?.modifierName }}</span>
<span class="time">{{ selectedVersion?.modifyTime }}</span>
</div>
<div class="panel-content" v-html="diffHtml.oldContent"></div>
</div>
<div class="compare-divider">
<Icon icon="ep:arrow-right" />
</div>
<div class="compare-panel right-panel">
<div class="panel-header">
<span>v{{ currentVersion }}</span>
<span class="modifier">当前版本</span>
</div>
<div class="panel-content" v-html="diffHtml.newContent"></div>
</div>
</div>
<div class="compare-actions">
<el-button @click="handleRestore(selectedVersion!)">
<Icon icon="ep:refresh-left" class="mr-5px" /> 恢复到 v{{ selectedVersion?.version }}
</el-button>
</div>
</div>
</div>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ReportVersionApi, ReportVersion } from '@/api/prison/report'
/** 历史版本弹窗 */
defineOptions({ name: 'PrisonVersionHistoryDialog' })
const props = defineProps<{
reportId?: number
}>()
const message = useMessage()
const dialogVisible = ref(false)
const loading = ref(false)
const versionList = ref<ReportVersion[]>([])
const currentVersion = ref(0)
const selectedVersion = ref<ReportVersion | null>(null)
const compareMode = ref(false)
const diffHtml = ref({ oldContent: '', newContent: '' })
/** 打开弹窗 */
const open = async () => {
if (!props.reportId) {
message.warning('请先保存报告')
return
}
dialogVisible.value = true
compareMode.value = false
selectedVersion.value = null
loadVersions()
}
defineExpose({ open })
/** 加载版本历史 */
const loadVersions = async () => {
if (!props.reportId) return
loading.value = true
try {
const data = await ReportVersionApi.getVersionList(props.reportId)
versionList.value = data
if (data.length > 0) {
currentVersion.value = data[0].version
}
} finally {
loading.value = false
}
}
/** 选择版本查看 */
const handleSelectVersion = async (row: ReportVersion) => {
selectedVersion.value = row
}
/** 获取行样式 */
const getRowClassName = ({ row }: { row: ReportVersion }) => {
return selectedVersion.value?.id === row.id ? 'selected-row' : ''
}
/** 对比版本 */
const handleCompare = async (version: ReportVersion) => {
selectedVersion.value = version
compareMode.value = true
//
try {
// const data = await ReportVersionApi.compareVersions(version.id, currentVersionId)
// diffHtml.value = data
//
diffHtml.value = {
oldContent: `<div style="padding: 10px; line-height: 1.6;">
<p><span style="background: #ffcccc;">该犯在服刑期间表现良好</span>遵守监规纪律</p>
<p><span style="background: #ffcccc;">积极参加劳动</span>完成劳动任务</p>
<p><span style="background: #ffcccc;">月度考核成绩均在85分以上</span>无违规记录</p>
</div>`,
newContent: `<div style="padding: 10px; line-height: 1.6;">
<p><span style="background: #ccffcc;">该犯在服刑期间表现优秀严格遵守监规纪律</span>遵守监规纪律</p>
<p><span style="background: #ccffcc;">积极参加劳动生产</span>完成劳动任务</p>
<p><span style="background: #ccffcc;">月度考核成绩均在90分以上</span>无违规记录</p>
</div>`
}
} catch {}
}
/** 恢复版本 */
const handleRestore = async (version: ReportVersion) => {
try {
await message.confirm(`确认要恢复到 v${version.version} 版本吗?当前版本内容将被覆盖。`)
await ReportVersionApi.restoreVersion(version.id)
message.success('版本已恢复')
emit('restore', version.id)
loadVersions()
} catch {}
}
const emit = defineEmits(['restore'])
</script>
<style lang="scss" scoped>
.version-history-dialog {
.version-list {
margin-bottom: 20px;
:deep(.selected-row) {
background: #ecf5ff !important;
}
:deep(.el-table__row) {
cursor: pointer;
}
}
.version-compare {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
.compare-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
h4 {
margin: 0;
font-size: 14px;
color: #303133;
}
}
.compare-content {
display: flex;
min-height: 300px;
.compare-panel {
flex: 1;
display: flex;
flex-direction: column;
.panel-header {
padding: 10px 15px;
background: #fafafa;
border-bottom: 1px solid #e4e7ed;
font-size: 13px;
color: #606266;
.modifier {
margin-left: 10px;
color: #909399;
}
.time {
margin-left: 10px;
color: #909399;
}
}
.panel-content {
flex: 1;
padding: 15px;
overflow-y: auto;
font-size: 14px;
line-height: 1.8;
}
&.left-panel {
border-right: 1px solid #e4e7ed;
}
}
.compare-divider {
display: flex;
align-items: center;
justify-content: center;
width: 50px;
background: #f5f7fa;
color: #909399;
}
}
.compare-actions {
padding: 15px;
background: #f5f7fa;
border-top: 1px solid #e4e7ed;
text-align: center;
}
}
}
</style>

View File

@ -0,0 +1,899 @@
<template>
<div class="report-edit-container">
<!-- 左侧罪犯列表 -->
<div class="prisoner-sidebar">
<div class="sidebar-header">
<h3>服刑人员列表</h3>
<el-input
v-model="searchKeyword"
placeholder="搜索姓名/编号"
prefix-icon="Search"
clearable
@input="handleSearch"
@clear="loadPrisonerList"
class="search-input"
/>
</div>
<div class="prisoner-list" v-loading="prisonerLoading">
<div
v-for="prisoner in prisonerList"
:key="prisoner.id"
:class="['prisoner-card', { active: currentPrisoner?.id === prisoner.id }]"
@click="selectPrisoner(prisoner)"
>
<div class="card-header">
<span class="area-name">{{ prisoner.areaName }}</span>
<el-tag
v-if="prisoner.riskLevel"
:type="getRiskLevelType(prisoner.riskLevel)"
size="small"
>
{{ getRiskLevelLabel(prisoner.riskLevel) }}
</el-tag>
</div>
<div class="card-body">
<span class="prisoner-name">{{ prisoner.name }}</span>
<span class="prisoner-no">{{ prisoner.prisonerNo }}</span>
</div>
</div>
<el-empty v-if="!prisonerLoading && prisonerList.length === 0" description="暂无数据" />
</div>
<div class="sidebar-footer">
<el-pagination
v-model:current-page="prisonerPage"
v-model:page-size="prisonerPageSize"
:total="prisonerTotal"
:page-sizes="[10, 20, 50]"
small
layout="prev, pager, next, total"
@current-change="loadPrisonerList"
/>
</div>
</div>
<!-- 右侧报告编辑区 -->
<div class="report-editor">
<!-- 顶部操作栏 -->
<div class="editor-toolbar">
<div class="toolbar-left">
<el-button @click="goBack">
<Icon icon="ep:arrow-left" class="mr-5px" /> 返回
</el-button>
<span class="toolbar-divider">|</span>
<el-button
type="primary"
@click="handleAiGenerateAll"
:loading="aiGenerating"
:disabled="!currentReport"
>
<Icon icon="ep:magic-stick" class="mr-5px" /> AI生成全部
</el-button>
<el-button
@click="handleSaveDraft"
:loading="saving"
:disabled="!currentReport"
>
<Icon icon="ep:document-checked" class="mr-5px" /> 保存草稿
<span v-if="lastSaveTime" class="save-time"> ({{ lastSaveTime }})</span>
</el-button>
</div>
<div class="toolbar-center">
<span class="report-title">{{ currentReport?.title || '未选择报告' }}</span>
</div>
<div class="toolbar-right">
<el-button
@click="handleShowQuickComment"
:disabled="!currentReport"
>
<Icon icon="ep:tickets" class="mr-5px" /> 快捷评语
</el-button>
<el-button
@click="handleShowVersionHistory"
:disabled="!currentReport"
>
<Icon icon="ep:clock" class="mr-5px" /> 历史版本
</el-button>
<el-button
type="success"
@click="handleSubmitReview"
:disabled="!canSubmit"
v-hasPermi="['prison:report:submit']"
>
<Icon icon="ep:position" class="mr-5px" /> 提交审核
</el-button>
<el-button
@click="handlePreview"
:disabled="!currentReport"
>
<Icon icon="ep:view" class="mr-5px" /> 预览
</el-button>
<el-button
type="primary"
@click="handleExport('pdf')"
:disabled="!currentReport"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</div>
</div>
<!-- 基本信息区 -->
<div class="basic-info" v-if="currentReport">
<el-descriptions :column="4" border size="small">
<el-descriptions-item label="服刑人员">
{{ currentReport.prisonerName }} ({{ currentReport.prisonerNo }})
</el-descriptions-item>
<el-descriptions-item label="监区">
{{ currentReport.areaName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="评估日期">
{{ currentReport.reportDate }}
</el-descriptions-item>
<el-descriptions-item label="模板">
{{ currentReport.templateName }}
</el-descriptions-item>
<el-descriptions-item label="风险等级">
<el-tag
v-if="currentReport.riskLevel"
:type="getRiskLevelType(currentReport.riskLevel)"
>
{{ getRiskLevelLabel(currentReport.riskLevel) }}
</el-tag>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :type="DICT_TYPE.PRISON_REPORT_STATUS" :value="currentReport.status" />
</el-descriptions-item>
<el-descriptions-item label="版本">
v{{ currentReport.version }}
</el-descriptions-item>
<el-descriptions-item label="数据源状态">
<el-tag v-if="dataSourceStatus.complete" type="success" size="small">
已加载 {{ dataSourceStatus.loaded }}/{{ dataSourceStatus.total }}
</el-tag>
<el-tag v-else type="warning" size="small" loading>
加载中...
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 审核退回提示 -->
<div v-if="currentReport?.status === 4 && currentReport.reviewComment" class="reject-notice">
<el-alert
type="warning"
:closable="false"
show-icon
>
<template #title>
审核退回原因{{ currentReport.reviewComment }}
<el-button type="primary" link size="small" @click="scrollToContent">
立即修改
</el-button>
</template>
</el-alert>
</div>
<!-- 编辑内容区 -->
<div class="editor-content" v-if="currentReport" ref="contentRef">
<!-- 维度分析区 -->
<div class="dimension-editor">
<el-collapse v-model="activeDimensions">
<el-collapse-item
v-for="(dimension, index) in currentReport.dimensions"
:key="dimension.dimensionId"
:name="dimension.dimensionId"
:title="dimension.dimensionName"
>
<template #title>
<div class="dimension-title-bar">
<span class="dimension-name">{{ index + 1 }}. {{ dimension.dimensionName }}</span>
<div class="dimension-tags">
<el-tag
v-if="dimension.isAiGenerated"
type="success"
size="small"
>
AI生成
</el-tag>
<el-tag
v-else-if="dimension.lastModifyTime"
type="info"
size="small"
>
已修改
</el-tag>
<el-tag
v-if="dimension.aiGenerateTime"
type="info"
size="small"
>
{{ dimension.aiGenerateTime }}
</el-tag>
</div>
</div>
</template>
<div class="dimension-content">
<el-input
v-model="dimension.content"
type="textarea"
:rows="6"
placeholder="请输入评估内容"
@change="markDimensionModified(dimension)"
/>
<div class="dimension-actions">
<el-button
v-if="dimension.enableAi"
type="primary"
size="small"
@click="handleAiGenerate(dimension)"
:loading="aiGenerating && generatingDimensionId === dimension.dimensionId"
>
<Icon icon="ep:magic-stick" class="mr-5px" />
{{ dimension.isAiGenerated ? '重新生成' : 'AI生成' }}
</el-button>
<el-button
v-if="dimension.lastModifyTime && dimension.isAiGenerated"
size="small"
@click="handleRestoreAiContent(dimension)"
>
恢复AI内容
</el-button>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
<!-- 综合结论与建议 -->
<div class="conclusion-section">
<h3 class="section-title">综合结论与建议</h3>
<el-form label-width="100px" size="default">
<el-form-item label="风险等级">
<el-select v-model="currentReport.riskLevel" placeholder="请选择风险等级">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="综合结论">
<el-input
v-model="currentReport.conclusion"
type="textarea"
:rows="4"
placeholder="请输入综合结论"
/>
</el-form-item>
<el-form-item label="改造建议">
<el-input
v-model="currentReport.suggestions"
type="textarea"
:rows="4"
placeholder="请输入改造建议"
/>
</el-form-item>
</el-form>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<el-empty description="请从左侧选择服刑人员开始编辑报告">
<el-button type="primary" @click="openCreateDialog">创建报告</el-button>
</el-empty>
</div>
</div>
<!-- 快捷评语弹窗 -->
<QuickCommentDialog
ref="quickCommentDialogRef"
:category-type="1"
@insert="insertComment"
/>
<!-- 历史版本弹窗 -->
<VersionHistoryDialog
ref="versionDialogRef"
:report-id="currentReport?.id"
@restore="handleRestoreVersion"
/>
<!-- 报告预览弹窗 -->
<ReportPreviewDialog ref="previewDialogRef" />
</div>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { useRoute, useRouter } from 'vue-router'
import { ReportApi, Report, ReportDimensionContent } from '@/api/prison/report'
import { PrisonerSelectApi, PrisonerBrief } from '@/api/prison/report'
import QuickCommentDialog from '@/views/prison/report/components/QuickCommentDialog.vue'
import VersionHistoryDialog from '@/views/prison/report/components/VersionHistoryDialog.vue'
import ReportPreviewDialog from '@/views/prison/report/components/ReportPreviewDialog.vue'
/** 评估报告编辑页面 */
defineOptions({ name: 'PrisonReportEdit' })
const route = useRoute()
const router = useRouter()
const message = useMessage()
//
const prisonerLoading = ref(false)
const prisonerList = ref<PrisonerBrief[]>([])
const prisonerPage = ref(1)
const prisonerPageSize = ref(20)
const prisonerTotal = ref(0)
const searchKeyword = ref('')
//
const currentPrisoner = ref<PrisonerBrief | null>(null)
const currentReport = ref<Report | null>(null)
const activeDimensions = ref<number[]>([])
const lastSaveTime = ref<string | null>(null)
//
const saving = ref(false)
const aiGenerating = ref(false)
const generatingDimensionId = ref<number | null>(null)
const dataSourceStatus = ref({ total: 6, loaded: 0, complete: false })
//
const contentRef = ref()
const quickCommentDialogRef = ref()
const versionDialogRef = ref()
const previewDialogRef = ref()
/** 加载罪犯列表 */
const loadPrisonerList = async () => {
prisonerLoading.value = true
try {
const data = await PrisonerSelectApi.getPrisonerPage({
pageNo: prisonerPage.value,
pageSize: prisonerPageSize.value,
name: searchKeyword.value,
prisonerNo: searchKeyword.value
})
prisonerList.value = data.list
prisonerTotal.value = data.total
} finally {
prisonerLoading.value = false
}
}
/** 搜索罪犯 */
const handleSearch = () => {
prisonerPage.value = 1
if (searchKeyword.value) {
//
setTimeout(() => loadPrisonerList(), 300)
} else {
loadPrisonerList()
}
}
/** 选择罪犯 */
const selectPrisoner = async (prisoner: PrisonerBrief) => {
currentPrisoner.value = prisoner
//
await loadReportForPrisoner(prisoner.id)
}
/** 为罪犯加载/创建报告 */
const loadReportForPrisoner = async (prisonerId: number) => {
try {
// API
// 使
// const data = await ReportApi.getLatestReportByPrisoner(prisonerId)
// currentReport.value = data
//
currentReport.value = {
id: Date.now(),
reportNo: `BG${new Date().toISOString().slice(0, 10).replace(/-/g, '')}${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`,
prisonerId: prisonerId,
prisonerNo: currentPrisoner.value?.prisonerNo || '',
prisonerName: currentPrisoner.value?.name || '',
templateId: 1,
templateName: '入监综合评估报告',
title: `${currentPrisoner.value?.name}服刑期间综合评估报告`,
reportDate: new Date().toISOString().slice(0, 10),
dimensions: [
{
dimensionId: 1,
dimensionName: '基本信息',
content: '',
isAiGenerated: false,
enableAi: false,
sort: 1
},
{
dimensionId: 2,
dimensionName: '犯罪情况分析',
content: '',
isAiGenerated: true,
enableAi: true,
sort: 2
},
{
dimensionId: 3,
dimensionName: '服刑表现评估',
content: '',
isAiGenerated: true,
enableAi: true,
sort: 3
},
{
dimensionId: 4,
dimensionName: '消费行为分析',
content: '',
isAiGenerated: true,
enableAi: true,
sort: 4
},
{
dimensionId: 5,
dimensionName: '综合评估结论',
content: '',
isAiGenerated: true,
enableAi: true,
sort: 5
}
],
status: 1,
version: 1
}
//
activeDimensions.value = currentReport.value.dimensions.map(d => d.dimensionId)
updateDataSourceStatus()
} catch {}
}
/** 更新数据源状态 */
const updateDataSourceStatus = () => {
//
dataSourceStatus.value = { total: 6, loaded: 6, complete: true }
}
/** 获取风险等级类型 */
const getRiskLevelType = (level?: number): string => {
const map: Record<number, string> = {
1: 'success',
2: 'warning',
3: 'danger',
4: 'danger'
}
return level ? map[level] || 'info' : 'info'
}
/** 获取风险等级标签 */
const getRiskLevelLabel = (level?: number): string => {
const map: Record<number, string> = {
1: '低风险',
2: '中风险',
3: '高风险',
4: '极高风险'
}
return level ? map[level] || '-' : '未评估'
}
/** 标记维度已修改 */
const markDimensionModified = (dimension: ReportDimensionContent) => {
dimension.isAiGenerated = false
dimension.lastModifyTime = new Date().toISOString().slice(0, 19).replace('T', ' ')
}
/** AI生成单个维度 */
const handleAiGenerate = async (dimension: ReportDimensionContent) => {
if (!currentReport.value) return
generatingDimensionId.value = dimension.dimensionId
aiGenerating.value = true
try {
// AI
await ReportApi.generateReportByAi(currentReport.value.id, [dimension.dimensionId])
//
const data = await ReportApi.getReport(currentReport.value.id)
currentReport.value = data
message.success(t('prison.report.aiGenerateComplete'))
} catch {
message.error(t('prison.report.aiGenerateFailed'))
} finally {
aiGenerating.value = false
generatingDimensionId.value = null
}
}
/** AI生成全部 */
const handleAiGenerateAll = async () => {
if (!currentReport.value) return
aiGenerating.value = true
try {
// AI
await ReportApi.generateReportByAi(currentReport.value.id)
//
const data = await ReportApi.getReport(currentReport.value.id)
currentReport.value = data
message.success(t('prison.report.aiGenerateAllComplete'))
} catch {
message.error(t('prison.report.aiGenerateFailed'))
} finally {
aiGenerating.value = false
generatingDimensionId.value = null
}
}
/** 保存草稿 */
const handleSaveDraft = async () => {
if (!currentReport.value) return
saving.value = true
try {
//
await ReportApi.updateReport(currentReport.value)
lastSaveTime.value = new Date().toLocaleTimeString()
message.success(t('common.saveSuccess'))
} catch {
message.error(t('common.saveFailed'))
} finally {
saving.value = false
}
}
/** 提交审核 */
const handleSubmitReview = async () => {
if (!currentReport.value) return
try {
await message.confirm(t('prison.report.submitConfirm'))
await ReportApi.submitReport(currentReport.value.id)
message.success(t('prison.report.submitSuccess'))
//
currentReport.value.status = 2
} catch {}
}
/** 恢复AI原始内容 */
const handleRestoreAiContent = async (dimension: ReportDimensionContent) => {
try {
await message.confirm(t('prison.report.restoreAiConfirm'))
// AI
await ReportApi.generateReportByAi(currentReport.value!.id, [dimension.dimensionId])
//
const data = await ReportApi.getReport(currentReport.value!.id)
currentReport.value = data
message.success(t('prison.report.restoreAiSuccess'))
} catch {}
}
/** 预览报告 */
const handlePreview = () => {
if (currentReport.value) {
previewDialogRef.value?.open(currentReport.value.id)
}
}
/** 导出报告 */
const handleExport = async (format: 'pdf' | 'word') => {
if (!currentReport.value) return
try {
const data = await ReportApi.exportReport(currentReport.value.id, format)
const fileName = `${currentReport.value.reportNo}_${currentReport.value.prisonerName}.${format}`
message.success('导出成功')
} catch {}
}
/** 显示快捷评语 */
const handleShowQuickComment = () => {
quickCommentDialogRef.value?.open()
}
/** 插入快捷评语 */
const insertComment = (comment: string) => {
message.success(t('prison.report.commentInserted'))
}
/** 显示历史版本 */
const handleShowVersionHistory = () => {
versionDialogRef.value?.open()
}
/** 恢复版本 */
const handleRestoreVersion = async (versionId: number) => {
try {
await ReportVersionApi.restoreVersion(versionId)
message.success(t('prison.report.versionRestored'))
//
loadReportForPrisoner(currentPrisoner.value!.id)
} catch {}
}
/** 跳转到内容区 */
const scrollToContent = () => {
contentRef.value?.scrollIntoView({ behavior: 'smooth' })
}
/** 返回列表 */
const goBack = () => {
router.push('/prison/report')
}
/** 打开创建报告弹窗 */
const openCreateDialog = () => {
//
router.push('/prison/report?action=create')
}
/** 是否可以提交 */
const canSubmit = computed(() => {
if (!currentReport.value) return false
if (currentReport.value.status !== 1 && currentReport.value.status !== 4) return false
//
return currentReport.value.dimensions.every(d => d.content)
})
//
onMounted(() => {
loadPrisonerList()
// ID
const reportId = route.query.id
if (reportId) {
// loadReport(Number(reportId))
}
})
//
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
})
/** 处理键盘快捷键 */
const handleKeydown = (e: KeyboardEvent) => {
// Ctrl + S
if (e.ctrlKey && e.key === 's') {
e.preventDefault()
handleSaveDraft()
}
}
</script>
<style lang="scss" scoped>
.report-edit-container {
display: flex;
height: calc(100vh - 84px);
background: #f5f7fa;
}
/* 左侧罪犯列表 */
.prisoner-sidebar {
width: 280px;
background: #fff;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
.sidebar-header {
padding: 15px;
border-bottom: 1px solid #e4e7ed;
h3 {
margin: 0 0 10px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.search-input {
width: 100%;
}
}
.prisoner-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.prisoner-card {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #409eff;
background: #ecf5ff;
}
&.active {
border-color: #409eff;
background: #ecf5ff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.area-name {
font-size: 12px;
color: #909399;
}
}
.card-body {
display: flex;
flex-direction: column;
gap: 4px;
.prisoner-name {
font-size: 15px;
font-weight: 500;
color: #303133;
}
.prisoner-no {
font-size: 12px;
color: #909399;
}
}
}
.sidebar-footer {
padding: 10px;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: center;
}
}
/* 右侧报告编辑区 */
.report-editor {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.editor-toolbar {
height: 50px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
.toolbar-left,
.toolbar-right {
display: flex;
align-items: center;
gap: 10px;
}
.toolbar-divider {
margin: 0 10px;
color: #e4e7ed;
}
.toolbar-center {
.report-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
.save-time {
font-size: 12px;
color: #909399;
}
}
.basic-info {
padding: 15px 20px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
}
.reject-notice {
padding: 10px 20px;
background: #fdf6ec;
}
.editor-content {
flex: 1;
overflow-y: auto;
padding: 20px;
.dimension-editor {
background: #fff;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
.dimension-title-bar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.dimension-name {
font-weight: 600;
color: #303133;
}
.dimension-tags {
display: flex;
gap: 8px;
}
}
.dimension-content {
.dimension-actions {
margin-top: 10px;
display: flex;
gap: 10px;
}
}
}
.conclusion-section {
background: #fff;
border-radius: 4px;
padding: 20px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
}
}
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
/* 动画 */
.prisoner-card {
transition: all 0.2s ease;
}
</style>

View File

@ -0,0 +1,421 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="90px"
>
<el-form-item label="报告编号" prop="reportNo">
<el-input
v-model="queryParams.reportNo"
placeholder="请输入报告编号"
clearable
@keyup.enter="handleQuery"
class="!w-180px"
/>
</el-form-item>
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input
v-model="queryParams.prisonerNo"
placeholder="请输入罪犯编号"
clearable
@keyup.enter="handleQuery"
class="!w-140px"
/>
</el-form-item>
<el-form-item label="模板类型" prop="templateId">
<el-select
v-model="queryParams.templateId"
placeholder="请选择模板"
clearable
class="!w-160px"
>
<el-option
v-for="item in templateOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="报告状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-110px"
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_REPORT_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="报告日期" prop="reportDate">
<el-date-picker
v-model="queryParams.reportDate"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-220px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openCreateDialog"
v-hasPermi="['prison:report:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 创建报告
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:report:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 报告列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报告编号" prop="reportNo" width="180" align="center" />
<el-table-column label="罪犯信息" min-width="200">
<template #default="{ row }">
<div class="prisoner-info">
<el-avatar :size="32" :src="row.avatarUrl">{{ row.prisonerName?.charAt(0) }}</el-avatar>
<div class="prisoner-detail">
<span class="name">{{ row.prisonerName }}</span>
<span class="no">{{ row.prisonerNo }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="报告标题" prop="title" min-width="250" show-overflow-tooltip />
<el-table-column label="模板类型" prop="templateName" width="150" />
<el-table-column label="报告日期" prop="reportDate" width="120" align="center" />
<el-table-column label="风险等级" prop="riskLevel" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.riskLevel" :type="getRiskLevelType(row.riskLevel)" size="small">
{{ getRiskLevelLabel(row.riskLevel) }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="100" align="center">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_REPORT_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="版本" prop="version" width="80" align="center">
<template #default="{ row }">
<el-tag type="info" size="small">v{{ row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column label="提交人" prop="submitterName" width="100" align="center" />
<el-table-column label="审核人" prop="reviewerName" width="100" align="center" />
<el-table-column label="创建时间" prop="createTime" width="180" align="center" />
<el-table-column label="操作" width="280" fixed="right" align="center">
<template #default="{ row }">
<el-button
link
type="primary"
@click="handleEdit(row)"
v-hasPermi="['prison:report:update']"
>
<Icon icon="ep:edit" /> 编辑
</el-button>
<el-button
link
type="primary"
@click="handlePreview(row)"
>
<Icon icon="ep:view" /> 预览
</el-button>
<el-button
link
type="success"
@click="handleDownload(row, 'pdf')"
v-hasPermi="['prison:report:export']"
>
<Icon icon="ep:download" /> 下载
</el-button>
<el-button
v-if="row.status === 1"
link
type="warning"
@click="handleSubmit(row.id)"
v-hasPermi="['prison:report:submit']"
>
提交审核
</el-button>
<el-button
v-if="row.status === 4"
link
type="primary"
@click="handleResubmit(row.id)"
v-hasPermi="['prison:report:update']"
>
重新提交
</el-button>
<el-button
link
type="danger"
@click="handleDelete(row.id)"
v-hasPermi="['prison:report:delete']"
>
<Icon icon="ep:delete" /> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 创建报告弹窗 -->
<CreateReportDialog ref="createDialogRef" @success="getList" />
<!-- 报告预览弹窗 -->
<ReportPreviewDialog ref="previewDialogRef" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { ReportApi, ReportPageParams, Report, ReportTemplateApi } from '@/api/prison/report'
import CreateReportDialog from './components/CreateReportDialog.vue'
import ReportPreviewDialog from './components/ReportPreviewDialog.vue'
defineOptions({ name: 'PrisonReport' })
const router = useRouter()
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const exportLoading = ref(false)
const list = ref<Report[]>([])
const total = ref(0)
const ids = ref<number[]>([])
const templateOptions = ref<{ id: number; name: string }[]>([])
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
reportNo: undefined,
prisonerNo: undefined,
templateId: undefined,
status: undefined,
reportDate: undefined as string[] | undefined
})
const queryFormRef = ref()
/** 获取风险等级类型 */
const getRiskLevelType = (level: number): string => {
const map: Record<number, string> = {
1: 'success',
2: 'warning',
3: 'danger',
4: 'danger'
}
return map[level] || 'info'
}
/** 获取风险等级标签 */
const getRiskLevelLabel = (level: number): string => {
const map: Record<number, string> = {
1: '低风险',
2: '中风险',
3: '高风险',
4: '极高风险'
}
return map[level] || '-'
}
/** 获取报告列表 */
const getList = async () => {
loading.value = true
try {
const data = await ReportApi.getReportPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 获取模板列表 */
const getTemplateList = async () => {
const data = await ReportTemplateApi.getTemplatePage({ pageNo: 1, pageSize: 100, status: 1 })
templateOptions.value = data.list.map((item: any) => ({ id: item.id, name: item.name }))
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
getList()
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: Report[]) => {
ids.value = selection.map((item) => item.id)
}
/** 打开创建报告弹窗 */
const createDialogRef = ref()
const openCreateDialog = () => {
createDialogRef.value?.open()
}
/** 预览报告弹窗 */
const previewDialogRef = ref()
/** 编辑报告 */
const handleEdit = (row: Report) => {
router.push({
path: '/prison/report/edit',
query: { id: row.id }
})
}
/** 预览报告 */
const handlePreview = (row: Report) => {
previewDialogRef.value?.open(row.id)
}
/** 下载报告 */
const handleDownload = async (row: Report, format: 'pdf' | 'word') => {
try {
const data = await ReportApi.exportReport(row.id, format)
const fileName = `${row.reportNo}_${row.prisonerName}.${format === 'pdf' ? 'pdf' : 'docx'}`
if (format === 'pdf') {
download.excel(data, fileName) // PDF excel Blob
} else {
download.word(data, fileName)
}
message.success(t('common.exportSuccess'))
} catch {
message.error(t('common.exportFailed'))
}
}
/** 提交审核 */
const handleSubmit = async (id: number) => {
try {
await message.confirm(t('prison.report.submitConfirm'))
await ReportApi.submitReport(id)
message.success(t('prison.report.submitSuccess'))
getList()
} catch {}
}
/** 重新提交 */
const handleResubmit = async (id: number) => {
try {
await message.confirm(t('prison.report.resubmitConfirm'))
await ReportApi.submitReport(id)
message.success(t('prison.report.resubmitSuccess'))
getList()
} catch {}
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await ReportApi.deleteReport(id)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 批量删除 */
const handleBatchDelete = async () => {
if (ids.value.length === 0) {
return message.warning('请选择要删除的数据')
}
try {
await message.delConfirm()
await ReportApi.deleteReportList(ids.value)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await ReportApi.exportReportExcel(queryParams)
download.excel(data, '评估报告.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
getTemplateList()
})
</script>
<style lang="scss" scoped>
.prisoner-info {
display: flex;
align-items: center;
gap: 10px;
.prisoner-detail {
display: flex;
flex-direction: column;
.name {
font-weight: 500;
color: var(--el-text-color-primary);
}
.no {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
</style>

View File

@ -0,0 +1,354 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="800px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="罪犯编号" prop="prisonerCode">
<el-input v-model="formData.prisonerCode" placeholder="请输入罪犯编号" :disabled="formType === 'view'" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input v-model="formData.prisonerName" placeholder="请输入罪犯姓名" :disabled="formType === 'view'" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="评估人" prop="assessor">
<el-input v-model="formData.assessor" placeholder="请输入评估人" :disabled="formType === 'view'" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="评估类型" prop="assessmentType">
<el-select v-model="formData.assessmentType" placeholder="请选择评估类型" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_ASSESSMENT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="评估日期" prop="assessmentDate">
<el-date-picker
v-model="formData.assessmentDate"
type="date"
placeholder="请选择评估日期"
value-format="YYYY-MM-DD"
:disabled="formType === 'view'"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="评估方式" prop="assessMethod">
<el-select v-model="formData.assessMethod" placeholder="请选择评估方式" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_ASSESS_METHOD)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">风险评估</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="综合得分" prop="overallScore">
<el-input-number v-model="formData.overallScore" :min="0" :max="100" placeholder="0-100" :disabled="formType === 'view'" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="风险等级" prop="riskLevel">
<el-select v-model="formData.riskLevel" placeholder="请选择风险等级" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="精神状态" prop="mentalState">
<el-select v-model="formData.mentalState" placeholder="请选择精神状态" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_MENTAL_STATE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">风险指标</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="脱逃风险" prop="escapeRisk">
<el-select v-model="formData.escapeRisk" placeholder="请选择脱逃风险等级" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="暴力倾向" prop="violenceRisk">
<el-select v-model="formData.violenceRisk" placeholder="请选择暴力倾向等级" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="抗改风险" prop="revoltRisk">
<el-select v-model="formData.revoltRisk" placeholder="请选择抗改风险等级" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="自杀自伤" prop="selfHarmRisk">
<el-select v-model="formData.selfHarmRisk" placeholder="请选择自杀自伤等级" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">评估结果</el-divider>
<el-form-item label="结论" prop="conclusion">
<el-input
v-model="formData.conclusion"
type="textarea"
placeholder="请输入评估结论"
:rows="3"
:disabled="formType === 'view'"
/>
</el-form-item>
<el-form-item label="建议" prop="recommendation">
<el-input
v-model="formData.recommendation"
type="textarea"
placeholder="请输入管理建议"
:rows="3"
:disabled="formType === 'view'"
/>
</el-form-item>
<el-form-item label="项目得分" prop="itemScores">
<el-input
v-model="formData.itemScores"
type="textarea"
placeholder="请输入各评估项目得分JSON格式"
:rows="3"
:disabled="formType === 'view'"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="请输入备注"
:rows="2"
:disabled="formType === 'view'"
/>
</el-form-item>
</el-form>
<!-- 查看详情时的信息展示 -->
<template v-if="formType === 'view'">
<el-divider content-position="left">详细信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="创建时间">
{{ formData.createTime ? formatDateTime(formData.createTime) : '-' }}
</el-descriptions-item>
</el-descriptions>
</template>
<template #footer>
<el-button v-if="formType !== 'view'" @click="submitForm" type="primary" :disabled="formLoading">
{{ formType === 'create' ? '新增' : '修改' }}
</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import { RiskApi, RiskSaveReqVO } from '@/api/prison/risk'
/** 风险评估 表单 */
defineOptions({ name: 'PrisonRiskForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref<'create' | 'update' | 'view'>('create') //
const formData = ref({
id: undefined,
prisonerId: undefined,
prisonerCode: undefined,
prisonerName: undefined,
assessmentType: undefined,
assessmentDate: undefined,
assessMethod: undefined,
overallScore: undefined,
riskLevel: undefined,
mentalState: undefined,
escapeRisk: undefined,
violenceRisk: undefined,
revoltRisk: undefined,
selfHarmRisk: undefined,
recommendation: undefined,
assessor: undefined,
conclusion: undefined,
itemScores: undefined,
remark: undefined,
createTime: undefined
})
const formRules = reactive({
prisonerCode: [{ required: true, message: '请输入罪犯编号', trigger: 'blur' }],
prisonerName: [{ required: true, message: '请输入罪犯姓名', trigger: 'blur' }],
assessmentType: [{ required: true, message: '请选择评估类型', trigger: 'change' }],
assessmentDate: [{ required: true, message: '请选择评估日期', trigger: 'change' }],
riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
const open = async (type: 'create' | 'update' | 'view', id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'view' ? '查看详情' : t('action.' + type)
formType.value = type
resetForm()
// /
if (id) {
formLoading.value = true
try {
const data = await RiskApi.getRisk(id)
formData.value = {
id: data.id,
prisonerId: data.prisonerId,
prisonerCode: data.prisonerCode,
prisonerName: data.prisonerName,
assessmentType: data.assessmentType,
assessmentDate: data.assessmentDate,
assessMethod: data.assessMethod,
overallScore: data.overallScore,
riskLevel: data.riskLevel,
mentalState: data.mentalState,
escapeRisk: data.escapeRisk,
violenceRisk: data.violenceRisk,
revoltRisk: data.revoltRisk,
selfHarmRisk: data.selfHarmRisk,
recommendation: data.recommendation,
assessor: data.assessor,
conclusion: data.conclusion,
itemScores: data.itemScores,
remark: data.remark,
createTime: data.createTime
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
if (formType.value === 'create') {
await RiskApi.createRisk(formData.value)
message.success(t('common.createSuccess'))
} else {
await RiskApi.updateRisk(formData.value)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
prisonerId: undefined,
prisonerCode: undefined,
prisonerName: undefined,
assessmentType: undefined,
assessmentDate: undefined,
assessMethod: undefined,
overallScore: undefined,
riskLevel: undefined,
mentalState: undefined,
escapeRisk: undefined,
violenceRisk: undefined,
revoltRisk: undefined,
selfHarmRisk: undefined,
recommendation: undefined,
assessor: undefined,
conclusion: undefined,
itemScores: undefined,
remark: undefined,
createTime: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,295 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="90px"
>
<el-form-item label="罪犯编号" prop="prisonerCode">
<el-input
v-model="queryParams.prisonerCode"
placeholder="请输入罪犯编号"
clearable
@keyup.enter="handleQuery"
class="!w-140px"
/>
</el-form-item>
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input
v-model="queryParams.prisonerName"
placeholder="请输入罪犯姓名"
clearable
@keyup.enter="handleQuery"
class="!w-140px"
/>
</el-form-item>
<el-form-item label="评估类型" prop="assessmentType">
<el-select
v-model="queryParams.assessmentType"
placeholder="请选择"
clearable
class="!w-120px"
>
<el-option
v-for="dict in assessmentTypeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="风险等级" prop="riskLevel">
<el-select
v-model="queryParams.riskLevel"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in riskLevelOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="评估人" prop="assessor">
<el-input
v-model="queryParams.assessor"
placeholder="请输入评估人"
clearable
@keyup.enter="handleQuery"
class="!w-120px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['prison:risk:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增评估
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:risk:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编号" align="center" prop="id" width="80" />
<el-table-column label="罪犯编号" align="center" prop="prisonerCode" width="100" />
<el-table-column label="罪犯姓名" align="center" prop="prisonerName" width="100" />
<el-table-column label="评估类型" align="center" prop="assessmentType" width="120">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_RISK_ASSESSMENT_TYPE" :value="row.assessmentType" />
</template>
</el-table-column>
<el-table-column label="评估日期" align="center" prop="assessmentDate" width="120">
<template #default="{ row }">
<span v-if="row.assessmentDate">{{ formatDate(row.assessmentDate) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="综合得分" align="center" prop="overallScore" width="100">
<template #default="{ row }">
<span :class="getScoreClass(row.overallScore)">{{ row.overallScore ?? '-' }}</span>
</template>
</el-table-column>
<el-table-column label="风险等级" align="center" prop="riskLevel" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="row.riskLevel" />
</template>
</el-table-column>
<el-table-column label="评估人" align="center" prop="assessor" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template #default="{ row }">
<span v-if="row.createTime">{{ formatDateTime(row.createTime) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
@click="openForm('view', row.id)"
>
<Icon icon="ep:view" class="mr-5px" /> 查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', row.id)"
v-hasPermi="['prison:risk:update']"
>
<Icon icon="ep:edit" class="mr-5px" /> 修改
</el-button>
<el-button
link
type="danger"
@click="handleDelete(row.id)"
v-hasPermi="['prison:risk:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<RiskForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDateTime, formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import { RiskApi, RiskPageReqVO } from '@/api/prison/risk'
import RiskForm from './RiskForm.vue'
defineOptions({ name: 'PrisonRisk' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const exportLoading = ref(false)
const list = ref<any[]>([])
const total = ref(0)
const selectedIds = ref<number[]>([])
//
const queryParams = reactive<RiskPageReqVO>({
pageNo: 1,
pageSize: 10,
prisonerCode: undefined,
prisonerName: undefined,
assessmentType: undefined,
riskLevel: undefined,
assessor: undefined,
assessmentDate: undefined
})
const queryFormRef = ref()
//
const assessmentTypeOptions = getIntDictOptions(DICT_TYPE.PRISON_ASSESSMENT_TYPE)
const riskLevelOptions = getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)
/** 根据得分返回样式类名 */
const getScoreClass = (score: number | undefined) => {
if (score === undefined || score === null) return ''
if (score >= 80) return 'text-success'
if (score >= 60) return 'text-warning'
return 'text-danger'
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await RiskApi.getRiskPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 多选操作 */
const handleSelectionChange = (selection: any[]) => {
selectedIds.value = selection.map((item) => item.id)
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await RiskApi.deleteRisk(id)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await RiskApi.exportRisk(queryParams)
download.excel(data, '风险评估.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>
<style scoped>
.text-success {
color: #67c23a;
font-weight: bold;
}
.text-warning {
color: #e6a23c;
font-weight: bold;
}
.text-danger {
color: #f56c6c;
font-weight: bold;
}
</style>

View File

@ -7,11 +7,24 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="罪犯ID" prop="prisonerId">
<el-input v-model="formData.prisonerId" placeholder="请输入罪犯ID" />
</el-form-item>
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input v-model="formData.prisonerNo" placeholder="请输入罪犯编号" />
<el-form-item label="罪犯" prop="prisonerId">
<el-select
v-model="formData.prisonerId"
placeholder="请选择罪犯"
filterable
remote
:remote-method="searchPrisoner"
:loading="prisonerLoading"
@change="handlePrisonerChange"
style="width: 100%"
>
<el-option
v-for="prisoner in prisonerList"
:key="prisoner.id"
:label="`${prisoner.name} (${prisoner.prisonerNo})`"
:value="prisoner.id"
/>
</el-select>
</el-form-item>
<el-form-item label="评估类型" prop="assessmentType">
<el-select v-model="formData.assessmentType" placeholder="请选择评估类型">
@ -95,6 +108,7 @@
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { RiskAssessmentApi, RiskAssessment } from '@/api/prison/riskassessment'
import { PrisonerApi, PrisonerVO } from '@/api/prison/prisoner'
/** 危险评估 表单 */
defineOptions({ name: 'RiskAssessmentForm' })
@ -106,35 +120,69 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
//
const prisonerLoading = ref(false)
const prisonerList = ref<PrisonerVO[]>([])
const formData = ref({
id: undefined,
prisonerId: undefined,
prisonerNo: undefined,
assessmentType: undefined,
assessmentDate: undefined,
violenceScore: undefined,
escapeScore: undefined,
suicideScore: undefined,
totalScore: undefined,
riskLevel: undefined,
riskFactors: undefined,
suggestions: undefined,
assessorId: undefined,
assessorName: undefined,
nextAssessmentDate: undefined,
status: undefined,
remark: undefined
prisonerId: undefined as number | undefined,
prisonerNo: undefined as string | undefined,
assessmentType: undefined as number | undefined,
assessmentDate: undefined as number | undefined,
violenceScore: undefined as number | undefined,
escapeScore: undefined as number | undefined,
suicideScore: undefined as number | undefined,
totalScore: undefined as number | undefined,
riskLevel: undefined as number | undefined,
riskFactors: undefined as string | undefined,
suggestions: undefined as string | undefined,
assessorId: undefined as number | undefined,
assessorName: undefined as string | undefined,
nextAssessmentDate: undefined as number | undefined,
status: 1 as number | undefined,
remark: undefined as string | undefined
})
const formRules = reactive({
prisonerId: [{ required: true, message: '罪犯ID不能为空', trigger: 'blur' }],
prisonerNo: [{ required: true, message: '罪犯编号不能为空', trigger: 'blur' }],
prisonerId: [{ required: true, message: '罪犯不能为空', trigger: 'change' }],
assessmentType: [{ required: true, message: '评估类型不能为空', trigger: 'change' }],
assessmentDate: [{ required: true, message: '评估日期不能为空', trigger: 'blur' }],
assessmentDate: [{ required: true, message: '评估日期不能为空', trigger: 'change' }],
riskLevel: [{ required: true, message: '风险等级不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
/** 搜索罪犯 */
const searchPrisoner = async (query: string) => {
if (!query) {
prisonerList.value = []
return
}
prisonerLoading.value = true
try {
const data = await PrisonerApi.getPage({
pageNo: 1,
pageSize: 20,
name: query //
} as any)
prisonerList.value = data.list || []
} catch (error) {
console.error('搜索罪犯失败:', error)
prisonerList.value = []
} finally {
prisonerLoading.value = false
}
}
/** 罪犯选择变化 */
const handlePrisonerChange = async (prisonerId: number) => {
const prisoner = prisonerList.value.find(p => p.id === prisonerId)
if (prisoner) {
formData.value.prisonerNo = prisoner.prisonerNo
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
@ -146,6 +194,11 @@ const open = async (type: string, id?: number) => {
formLoading.value = true
try {
formData.value = await RiskAssessmentApi.getRiskAssessment(id)
//
if (formData.value.prisonerId) {
const prisoner = await PrisonerApi.getPrisoner(formData.value.prisonerId)
prisonerList.value = [prisoner]
}
} finally {
formLoading.value = false
}
@ -195,9 +248,10 @@ const resetForm = () => {
assessorId: undefined,
assessorName: undefined,
nextAssessmentDate: undefined,
status: undefined,
status: 1,
remark: undefined
}
prisonerList.value = []
formRef.value?.resetFields()
}
</script>

View File

@ -14,7 +14,16 @@
placeholder="请输入罪犯编号"
clearable
@keyup.enter="handleQuery"
class="!w-140px"
class="!w-120px"
/>
</el-form-item>
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input
v-model="queryParams.prisonerName"
placeholder="请输入罪犯姓名"
clearable
@keyup.enter="handleQuery"
class="!w-120px"
/>
</el-form-item>
<el-form-item label="评估类型" prop="assessmentType">
@ -105,12 +114,17 @@
<el-table-column type="selection" width="55" />
<el-table-column label="评估ID" align="center" prop="id" width="80" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
<el-table-column label="罪犯姓名" align="center" prop="prisonerName" width="100" />
<el-table-column label="评估类型" align="center" prop="assessmentType" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_ASSESSMENT_TYPE" :value="scope.row.assessmentType" />
</template>
</el-table-column>
<el-table-column label="评估日期" align="center" prop="assessmentDate" width="120" />
<el-table-column label="评估日期" align="center" prop="assessmentDate" width="120">
<template #default="scope">
{{ formatDateTime(scope.row.assessmentDate) }}
</template>
</el-table-column>
<el-table-column label="暴力得分" align="center" prop="violenceScore" width="90" />
<el-table-column label="脱逃得分" align="center" prop="escapeScore" width="90" />
<el-table-column label="自杀得分" align="center" prop="suicideScore" width="90" />
@ -129,10 +143,10 @@
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ dateFormatter(scope.row.createTime) }}
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<el-table-column label="操作" align="center" width="120" fixed="right">
<template #default="scope">
<el-button
type="primary"
@ -167,7 +181,7 @@
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { RiskAssessmentApi, RiskAssessment } from '@/api/prison/riskassessment'
import RiskAssessmentForm from './RiskAssessmentForm.vue'
@ -184,6 +198,7 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
prisonerNo: undefined,
prisonerName: undefined,
assessmentType: undefined,
riskLevel: undefined,
status: undefined

View File

@ -48,7 +48,7 @@
<template #header>
<div class="card-header">
<span>考核规则配置</span>
<el-button type="primary" @click="handleCreate" v-hasPermi="'prison:score-rule:create'">
<el-button type="primary" @click="handleCreate" v-hasPermi="['prison:score-rule:create']">
<Icon icon="ep:plus" class="mr-5px" /> 新增规则
</el-button>
</div>
@ -78,10 +78,10 @@
<el-table-column prop="sort" label="排序" width="80" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)" v-hasPermi="'prison:score-rule:update'">
<el-button link type="primary" @click="handleEdit(row)" v-hasPermi="['prison:score-rule:update']">
编辑
</el-button>
<el-button link type="danger" @click="handleDelete(row)" v-hasPermi="'prison:score-rule:delete'">
<el-button link type="danger" @click="handleDelete(row)" v-hasPermi="['prison:score-rule:delete']">
删除
</el-button>
</template>

View File

@ -7,32 +7,58 @@
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="罪犯ID" prop="prisonerId">
<el-input v-model="formData.prisonerId" placeholder="请输入罪犯ID" />
</el-form-item>
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input v-model="formData.prisonerNo" placeholder="请输入罪犯编号" />
<el-form-item label="罪犯" prop="prisonerId">
<el-select
v-model="formData.prisonerId"
placeholder="请选择罪犯"
filterable
remote
:remote-method="searchPrisoner"
:loading="prisonerLoading"
@change="handlePrisonerChange"
style="width: 100%"
>
<el-option
v-for="prisoner in prisonerList"
:key="prisoner.id"
:label="`${prisoner.name} (${prisoner.prisonerNo})`"
:value="prisoner.id"
/>
</el-select>
</el-form-item>
<el-form-item label="考核年份" prop="year">
<el-input v-model="formData.year" placeholder="请输入考核年份" />
<el-date-picker
v-model="formData.year"
type="year"
placeholder="请选择考核年份"
value-format="YYYY"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="考核月份" prop="month">
<el-input v-model="formData.month" placeholder="请输入考核月份" />
<el-select v-model="formData.month" placeholder="请选择考核月份" style="width: 100%">
<el-option
v-for="month in 12"
:key="month"
:label="`${month}月`"
:value="month"
/>
</el-select>
</el-form-item>
<el-form-item label="基础分" prop="baseScore">
<el-input v-model="formData.baseScore" placeholder="请输入基础分" />
<el-input-number v-model="formData.baseScore" :min="0" :precision="2" placeholder="请输入基础分" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="加分" prop="rewardScore">
<el-input v-model="formData.rewardScore" placeholder="请输入加分" />
<el-input-number v-model="formData.rewardScore" :min="0" :precision="2" placeholder="请输入加分" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="扣分" prop="penaltyScore">
<el-input v-model="formData.penaltyScore" placeholder="请输入扣分" />
<el-input-number v-model="formData.penaltyScore" :min="0" :precision="2" placeholder="请输入扣分" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="总分" prop="totalScore">
<el-input v-model="formData.totalScore" placeholder="请输入总分" />
<el-input-number v-model="formData.totalScore" :min="0" :precision="2" placeholder="请输入总分" controls-position="right" style="width: 100%" readonly />
</el-form-item>
<el-form-item label="考核等级" prop="level">
<el-select v-model="formData.level" placeholder="请选择考核等级">
<el-select v-model="formData.level" placeholder="请选择考核等级" style="width: 100%">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SCORE_LEVEL)"
:key="dict.value"
@ -41,12 +67,6 @@
/>
</el-select>
</el-form-item>
<el-form-item label="考核人ID" prop="assessorId">
<el-input v-model="formData.assessorId" placeholder="请输入考核人ID" />
</el-form-item>
<el-form-item label="考核人姓名" prop="assessorName">
<el-input v-model="formData.assessorName" placeholder="请输入考核人姓名" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
@ -69,6 +89,7 @@
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { ScoreApi, Score } from '@/api/prison/score'
import { PrisonerApi, PrisonerVO } from '@/api/prison/prisoner'
/** 计分考核 表单 */
defineOptions({ name: 'ScoreForm' })
@ -80,31 +101,65 @@ const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
//
const prisonerLoading = ref(false)
const prisonerList = ref<PrisonerVO[]>([])
const formData = ref({
id: undefined,
prisonerId: undefined,
prisonerNo: undefined,
year: undefined,
month: undefined,
baseScore: undefined,
rewardScore: undefined,
penaltyScore: undefined,
totalScore: undefined,
level: undefined,
assessorId: undefined,
assessorName: undefined,
status: undefined,
remark: undefined
prisonerId: undefined as number | undefined,
prisonerNo: undefined as string | undefined,
year: undefined as string | undefined,
month: undefined as number | undefined,
baseScore: 0,
rewardScore: 0,
penaltyScore: 0,
totalScore: 0,
level: undefined as number | undefined,
assessorId: undefined as number | undefined,
assessorName: undefined as string | undefined,
status: 1,
remark: undefined as string | undefined
})
const formRules = reactive({
prisonerId: [{ required: true, message: '罪犯ID不能为空', trigger: 'blur' }],
prisonerNo: [{ required: true, message: '罪犯编号不能为空', trigger: 'blur' }],
year: [{ required: true, message: '考核年份不能为空', trigger: 'blur' }],
month: [{ required: true, message: '考核月份不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
prisonerId: [{ required: true, message: '罪犯不能为空', trigger: 'change' }],
year: [{ required: true, message: '考核年份不能为空', trigger: 'change' }],
month: [{ required: true, message: '考核月份不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
/** 搜索罪犯 */
const searchPrisoner = async (query: string) => {
if (!query) {
prisonerList.value = []
return
}
prisonerLoading.value = true
try {
const data = await PrisonerApi.getPage({
pageNo: 1,
pageSize: 20,
name: query //
} as any)
prisonerList.value = data.list || []
} catch (error) {
console.error('搜索罪犯失败:', error)
prisonerList.value = []
} finally {
prisonerLoading.value = false
}
}
/** 罪犯选择变化 */
const handlePrisonerChange = async (prisonerId: number) => {
const prisoner = prisonerList.value.find(p => p.id === prisonerId)
if (prisoner) {
formData.value.prisonerNo = prisoner.prisonerNo
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
@ -115,7 +170,13 @@ const open = async (type: string, id?: number) => {
if (id) {
formLoading.value = true
try {
formData.value = await ScoreApi.getScore(id)
const data = await ScoreApi.getScore(id)
formData.value = { ...data, year: data.year?.toString() } as any
//
if (data.prisonerId) {
const prisoner = await PrisonerApi.getPrisoner(data.prisonerId)
prisonerList.value = [prisoner]
}
} finally {
formLoading.value = false
}
@ -155,16 +216,17 @@ const resetForm = () => {
prisonerNo: undefined,
year: undefined,
month: undefined,
baseScore: undefined,
rewardScore: undefined,
penaltyScore: undefined,
totalScore: undefined,
baseScore: 0,
rewardScore: 0,
penaltyScore: 0,
totalScore: 0,
level: undefined,
assessorId: undefined,
assessorName: undefined,
status: undefined,
status: 1,
remark: undefined
}
prisonerList.value = []
formRef.value?.resetFields()
}
</script>
</script>

View File

@ -17,6 +17,15 @@
class="!w-140px"
/>
</el-form-item>
<el-form-item label="罪犯姓名" prop="prisonerName">
<el-input
v-model="queryParams.prisonerName"
placeholder="请输入罪犯姓名"
clearable
@keyup.enter="handleQuery"
class="!w-120px"
/>
</el-form-item>
<el-form-item label="考核年份" prop="year">
<el-input
v-model="queryParams.year"
@ -98,7 +107,10 @@
>
<el-table-column type="selection" width="55" />
<el-table-column label="记录ID" align="center" prop="id" width="80" />
<el-table-column label="罪犯姓名" align="center" prop="prisonerName" width="100" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
<el-table-column label="监区" align="center" prop="prisonAreaName" width="100" />
<el-table-column label="监室" align="center" prop="prisonCellName" width="100" />
<el-table-column label="年份" align="center" prop="year" width="80" />
<el-table-column label="月份" align="center" prop="month" width="70" />
<el-table-column label="基础分" align="center" prop="baseScore" width="80" />
@ -118,10 +130,10 @@
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ dateFormatter(scope.row.createTime) }}
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<el-table-column label="操作" align="center" width="120" fixed="right">
<template #default="scope">
<el-button
type="primary"
@ -156,7 +168,7 @@
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { dateFormatter } from '@/utils/formatTime'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { ScoreApi, Score } from '@/api/prison/score'
import ScoreForm from './ScoreForm.vue'
@ -173,6 +185,7 @@ const queryParams = reactive({
pageNo: 1,
pageSize: 10,
prisonerNo: undefined,
prisonerName: undefined,
year: undefined,
level: undefined,
status: undefined

View File

@ -0,0 +1,340 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="700px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" :disabled="formType === 'view'" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="分类" prop="category">
<el-select v-model="formData.category" placeholder="请选择分类" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SITUATION_CATEGORY)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="等级" prop="level">
<el-select v-model="formData.level" placeholder="请选择等级" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SITUATION_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择来源" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SITUATION_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SITUATION_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="监区" prop="areaId">
<el-tree-select
v-model="formData.areaId"
:data="areaTreeList"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择监区"
clearable
filterable
:disabled="formType === 'view'"
:render-after-expand="false"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="监室" prop="cellId">
<el-select v-model="formData.cellId" placeholder="请选择监室" clearable :disabled="formType === 'view'">
<el-option
v-for="cell in cellOptions"
:key="cell.id"
:label="cell.name"
:value="cell.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报告人" prop="reporter">
<el-input v-model="formData.reporter" placeholder="请输入报告人" :disabled="formType === 'view'" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="处理人" prop="handler">
<el-input v-model="formData.handler" placeholder="请输入处理人" :disabled="formType === 'view'" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="发生时间" prop="occurTime">
<el-date-picker
v-model="formData.occurTime"
type="datetime"
placeholder="请选择发生时间"
value-format="x"
:disabled="formType === 'view'"
class="!w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="详情内容" prop="content">
<el-input
v-model="formData.content"
type="textarea"
placeholder="请输入详情内容"
:rows="4"
:disabled="formType === 'view'"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
:disabled="formType === 'view'"
/>
</el-form-item>
</el-form>
<!-- 查看详情时的信息展示 -->
<template v-if="formType === 'view'">
<el-divider content-position="left">详细信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="监区">
{{ formData.areaName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="监室">
{{ formData.cellName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formData.createTime ? formatDateTime(formData.createTime) : '-' }}
</el-descriptions-item>
</el-descriptions>
</template>
<template #footer>
<el-button v-if="formType !== 'view'" @click="submitForm" type="primary" :disabled="formLoading">
{{ formType === 'create' ? '新增' : '修改' }}
</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import { SituationApi, SituationSaveReqVO } from '@/api/prison/situation'
/** 狱情收集 表单 */
defineOptions({ name: 'PrisonSituationForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref<'create' | 'update' | 'view'>('create') //
const formData = ref({
id: undefined,
title: undefined,
content: undefined,
category: 1,
level: 1,
source: 1,
status: 1,
areaId: undefined,
cellId: undefined,
reporter: undefined,
handler: undefined,
occurTime: undefined,
remark: undefined,
areaName: undefined,
cellName: undefined,
createTime: undefined
})
const formRules = reactive({
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
category: [{ required: true, message: '请选择分类', trigger: 'change' }],
level: [{ required: true, message: '请选择等级', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
})
const formRef = ref() // Ref
const areaTreeList = ref<any[]>([]) //
const cellOptions = ref<{ id: number; name: string }[]>([]) //
/** 获取监区树形列表 */
const loadAreaTree = async () => {
areaTreeList.value = await SituationApi.AreaApi.getAreaTree()
}
/** 获取监室列表 */
const loadCellList = async (areaId?: number) => {
try {
const data = await SituationApi.getCellList({ areaId, status: 1 })
cellOptions.value = data
} catch {
cellOptions.value = []
}
}
/** 监听监区变化 */
const handleAreaChange = (areaId: number | undefined) => {
formData.value.cellId = undefined
if (areaId) {
loadCellList(areaId)
} else {
cellOptions.value = []
}
}
/** 打开弹窗 */
const open = async (type: 'create' | 'update' | 'view', id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'view' ? '查看详情' : t('action.' + type)
formType.value = type
//
await loadAreaTree()
resetForm()
// /
if (id) {
formLoading.value = true
try {
const data = await SituationApi.getSituation(id)
formData.value = {
id: data.id,
title: data.title,
content: data.content,
category: data.category,
level: data.level,
source: data.source,
status: data.status,
areaId: data.areaId,
cellId: data.cellId,
reporter: data.reporter,
handler: data.handler,
occurTime: data.occurTime ? new Date(data.occurTime).getTime() : undefined,
remark: data.remark,
areaName: data.areaName,
cellName: data.cellName,
createTime: data.createTime
}
//
if (data.areaId) {
await loadCellList(data.areaId)
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = {
...formData.value,
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString() : undefined
} as unknown as SituationSaveReqVO
if (formType.value === 'create') {
await SituationApi.createSituation(data)
message.success(t('common.createSuccess'))
} else {
await SituationApi.updateSituation(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
title: undefined,
content: undefined,
category: 1,
level: 1,
source: 1,
status: 1,
areaId: undefined,
cellId: undefined,
reporter: undefined,
handler: undefined,
occurTime: undefined,
remark: undefined,
areaName: undefined,
cellName: undefined,
createTime: undefined
}
cellOptions.value = []
formRef.value?.resetFields()
}
//
watch(() => formData.value.areaId, (val) => {
handleAreaChange(val)
})
</script>

View File

@ -0,0 +1,315 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
@keyup.enter="handleQuery"
class="!w-160px"
/>
</el-form-item>
<el-form-item label="分类" prop="category">
<el-select
v-model="queryParams.category"
placeholder="请选择"
clearable
class="!w-120px"
>
<el-option
v-for="dict in categoryOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="等级" prop="level">
<el-select
v-model="queryParams.level"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in levelOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="监区" prop="areaId">
<el-tree-select
v-model="queryParams.areaId"
:data="areaTreeList"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择监区"
clearable
filterable
class="!w-180px"
:render-after-expand="false"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['prison:situation:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增狱情
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:situation:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编号" align="center" prop="id" width="80" />
<el-table-column label="标题" align="center" prop="title" min-width="150" show-overflow-tooltip />
<el-table-column label="分类" align="center" prop="category" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_SITUATION_CATEGORY" :value="row.category" />
</template>
</el-table-column>
<el-table-column label="等级" align="center" prop="level" width="90">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_SITUATION_LEVEL" :value="row.level" />
</template>
</el-table-column>
<el-table-column label="来源" align="center" prop="source" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_SITUATION_SOURCE" :value="row.source" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_SITUATION_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="监区" align="center" prop="areaName" width="120" />
<el-table-column label="报告人" align="center" prop="reporter" width="100" />
<el-table-column label="发生时间" align="center" prop="occurTime" width="160">
<template #default="{ row }">
<span v-if="row.occurTime">{{ formatDateTime(row.occurTime) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template #default="{ row }">
<span v-if="row.createTime">{{ formatDateTime(row.createTime) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
@click="openForm('view', row.id)"
>
<Icon icon="ep:view" class="mr-5px" /> 查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', row.id)"
v-hasPermi="['prison:situation:update']"
>
<Icon icon="ep:edit" class="mr-5px" /> 修改
</el-button>
<el-button
link
type="danger"
@click="handleDelete(row.id)"
v-hasPermi="['prison:situation:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<SituationForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { SituationApi, SituationPageReqVO } from '@/api/prison/situation'
import SituationForm from './SituationForm.vue'
defineOptions({ name: 'PrisonSituation' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const exportLoading = ref(false)
const list = ref<any[]>([])
const total = ref(0)
const selectedIds = ref<number[]>([])
//
const queryParams = reactive<SituationPageReqVO>({
pageNo: 1,
pageSize: 10,
title: undefined,
category: undefined,
level: undefined,
source: undefined,
status: undefined,
areaId: undefined,
cellId: undefined,
reporter: undefined
})
const queryFormRef = ref()
//
const categoryOptions = getIntDictOptions(DICT_TYPE.PRISON_SITUATION_CATEGORY)
const levelOptions = getIntDictOptions(DICT_TYPE.PRISON_SITUATION_LEVEL)
const sourceOptions = getIntDictOptions(DICT_TYPE.PRISON_SITUATION_SOURCE)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_SITUATION_STATUS)
//
const areaOptions = ref<{ id: number; name: string }[]>([])
//
const areaTreeList = ref<any[]>([])
/** 获取监区树形列表 */
const loadAreas = async () => {
areaTreeList.value = await SituationApi.AreaApi.getAreaTree()
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await SituationApi.getSituationPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 多选操作 */
const handleSelectionChange = (selection: any[]) => {
selectedIds.value = selection.map((item) => item.id)
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await SituationApi.deleteSituation(id)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 批量删除 */
const handleBatchDelete = async () => {
if (selectedIds.value.length === 0) {
message.warning('请选择要删除的数据')
return
}
try {
await message.delConfirm()
await SituationApi.deleteSituationList(selectedIds.value)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await SituationApi.exportSituation(queryParams)
download.excel(data, '狱情收集.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
loadAreas()
})
</script>

View File

@ -0,0 +1,134 @@
<template>
<Dialog :title="actionTitle" v-model="dialogVisible" width="500px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item :label="actionLabel" :prop="actionProp">
<el-input
v-model="formData.result"
type="textarea"
:placeholder="`请输入${actionLabel}`"
:rows="4"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">
确认{{ actionBtnText }}
</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { WarningApi } from '@/api/prison/warning'
/** 预警处置操作表单 */
defineOptions({ name: 'PrisonWarningActionForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const warningId = ref<number>() // ID
const actionType = ref<'verify' | 'handle' | 'release'>('verify') //
const formData = ref({
result: ''
})
const formRules = reactive({
result: [{ required: true, message: '请输入内容', trigger: 'blur' }]
})
const formRef = ref() // Ref
//
const actionTitle = computed(() => {
const titles = {
verify: '预警核实',
handle: '预警处置',
release: '预警解除'
}
return titles[actionType.value]
})
//
const actionLabel = computed(() => {
const labels = {
verify: '核实结果',
handle: '处置结果',
release: '解除原因'
}
return labels[actionType.value]
})
//
const actionBtnText = computed(() => {
const texts = {
verify: '核实',
handle: '处置',
release: '解除'
}
return texts[actionType.value]
})
// APIprop
const actionProp = computed(() => {
const props = {
verify: 'verifyResult',
handle: 'handleResult',
release: 'releaseReason'
}
return props[actionType.value]
})
/** 打开弹窗 */
const open = (type: 'verify' | 'handle' | 'release', id: number) => {
dialogVisible.value = true
actionType.value = type
warningId.value = id
formData.value.result = ''
formRef.value?.resetFields()
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data: any = {
id: warningId.value
}
data[actionProp.value] = formData.value.result
if (actionType.value === 'verify') {
await WarningApi.verifyWarning(data)
message.success('核实成功')
} else if (actionType.value === 'handle') {
await WarningApi.handleWarning(data)
message.success('处置成功')
} else if (actionType.value === 'release') {
await WarningApi.releaseWarning(data)
message.success('解除成功')
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
</script>

View File

@ -0,0 +1,444 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="700px">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" :disabled="formType === 'view'" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_WARNING_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="等级" prop="level">
<el-select v-model="formData.level" placeholder="请选择等级" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_WARNING_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="来源" prop="source">
<el-select v-model="formData.source" placeholder="请选择来源" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_WARNING_SOURCE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="formData.status" placeholder="请选择状态" :disabled="formType === 'view'">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_WARNING_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="关联狱情" prop="situationId">
<el-select v-model="formData.situationId" placeholder="请选择关联狱情" clearable :disabled="formType === 'view'">
<el-option
v-for="sit in situationOptions"
:key="sit.id"
:label="sit.title"
:value="sit.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发生时间" prop="occurTime">
<el-date-picker
v-model="formData.occurTime"
type="datetime"
placeholder="请选择发生时间"
value-format="x"
:disabled="formType === 'view'"
class="!w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="监区" prop="areaId">
<el-tree-select
v-model="formData.areaId"
:data="areaTreeList"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择监区"
clearable
filterable
:disabled="formType === 'view'"
:render-after-expand="false"
class="!w-full"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="监室" prop="cellId">
<el-select v-model="formData.cellId" placeholder="请选择监室" clearable :disabled="formType === 'view'">
<el-option
v-for="cell in cellOptions"
:key="cell.id"
:label="cell.name"
:value="cell.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="预警时间" prop="alertTime">
<el-date-picker
v-model="formData.alertTime"
type="datetime"
placeholder="请选择预警时间"
value-format="x"
:disabled="formType === 'view'"
class="!w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="内容" prop="content">
<el-input
v-model="formData.content"
type="textarea"
placeholder="请输入内容"
:rows="4"
:disabled="formType === 'view'"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
:disabled="formType === 'view'"
/>
</el-form-item>
</el-form>
<!-- 查看详情时的完整信息展示 -->
<template v-if="formType === 'view'">
<el-divider content-position="left">详细信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="监区">
{{ formData.areaName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="监室">
{{ formData.cellName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="关联狱情">
{{ formData.situationTitle || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formData.createTime ? formatDateTime(formData.createTime) : '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 核实信息 -->
<el-divider content-position="left">核实信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="核实人">
{{ formData.verifier || '-' }}
</el-descriptions-item>
<el-descriptions-item label="核实时间">
{{ formData.verifyTime ? formatDateTime(formData.verifyTime) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="核实结果" :span="2">
{{ formData.verifyResult || '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 处置信息 -->
<el-divider content-position="left">处置信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="处置人">
{{ formData.handler || '-' }}
</el-descriptions-item>
<el-descriptions-item label="处置时间">
{{ formData.handleTime ? formatDateTime(formData.handleTime) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="处置结果" :span="2">
{{ formData.handleResult || '-' }}
</el-descriptions-item>
</el-descriptions>
<!-- 解除信息 -->
<el-divider content-position="left">解除信息</el-divider>
<el-descriptions :column="2" border>
<el-descriptions-item label="解除人">
{{ formData.releaser || '-' }}
</el-descriptions-item>
<el-descriptions-item label="解除时间">
{{ formData.releaseTime ? formatDateTime(formData.releaseTime) : '-' }}
</el-descriptions-item>
<el-descriptions-item label="解除原因" :span="2">
{{ formData.releaseReason || '-' }}
</el-descriptions-item>
</el-descriptions>
</template>
<template #footer>
<el-button v-if="formType !== 'view'" @click="submitForm" type="primary" :disabled="formLoading">
{{ formType === 'create' ? '新增' : '修改' }}
</el-button>
<el-button @click="dialogVisible = false">关闭</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import { WarningApi, WarningSaveReqVO } from '@/api/prison/warning'
/** 预警管理 表单 */
defineOptions({ name: 'PrisonWarningForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref<'create' | 'update' | 'view'>('create') //
const formData = ref({
id: undefined,
title: undefined,
content: undefined,
type: 1,
level: 1,
source: 1,
status: 1,
situationId: undefined,
areaId: undefined,
cellId: undefined,
alertTime: undefined,
occurTime: undefined,
remark: undefined,
//
areaName: undefined,
cellName: undefined,
situationTitle: undefined,
createTime: undefined,
//
verifyTime: undefined,
verifier: undefined,
verifyResult: undefined,
handleTime: undefined,
handler: undefined,
handleResult: undefined,
releaseTime: undefined,
releaser: undefined,
releaseReason: undefined
})
const formRules = reactive({
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
level: [{ required: true, message: '请选择等级', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
})
const formRef = ref() // Ref
const areaTreeList = ref<any[]>([]) //
const cellOptions = ref<{ id: number; name: string }[]>([]) //
const situationOptions = ref<{ id: number; title: string }[]>([]) //
/** 获取监区树形列表 */
const loadAreaTree = async () => {
areaTreeList.value = await WarningApi.AreaApi.getAreaTree()
}
/** 获取监室列表 */
const loadCellList = async (areaId?: number) => {
try {
const data = await WarningApi.getCellList({ areaId, status: 1 })
cellOptions.value = data
} catch {
cellOptions.value = []
}
}
/** 获取狱情列表 */
const loadSituationList = async () => {
try {
const data = await WarningApi.getSituationList({ status: 1 })
situationOptions.value = data
} catch {
situationOptions.value = []
}
}
/** 监听监区变化 */
const handleAreaChange = (areaId: number | undefined) => {
formData.value.cellId = undefined
if (areaId) {
loadCellList(areaId)
} else {
cellOptions.value = []
}
}
/** 打开弹窗 */
const open = async (type: 'create' | 'update' | 'view', id?: number) => {
dialogVisible.value = true
dialogTitle.value = type === 'view' ? '查看详情' : t('action.' + type)
formType.value = type
//
await loadAreaTree()
await loadSituationList()
resetForm()
// /
if (id) {
formLoading.value = true
try {
const data = await WarningApi.getWarning(id)
formData.value = {
id: data.id,
title: data.title,
content: data.content,
type: data.type,
level: data.level,
source: data.source,
status: data.status,
situationId: data.situationId,
areaId: data.areaId,
cellId: data.cellId,
alertTime: data.alertTime ? new Date(data.alertTime).getTime() : undefined,
occurTime: data.occurTime ? new Date(data.occurTime).getTime() : undefined,
remark: data.remark,
areaName: data.areaName,
cellName: data.cellName,
situationTitle: data.situationTitle,
createTime: data.createTime,
verifyTime: data.verifyTime,
verifier: data.verifier,
verifyResult: data.verifyResult,
handleTime: data.handleTime,
handler: data.handler,
handleResult: data.handleResult,
releaseTime: data.releaseTime,
releaser: data.releaser,
releaseReason: data.releaseReason
}
//
if (data.areaId) {
await loadCellList(data.areaId)
}
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success']) // success
const submitForm = async () => {
//
await formRef.value.validate()
//
formLoading.value = true
try {
const data = {
...formData.value,
alertTime: formData.value.alertTime ? new Date(formData.value.alertTime).toISOString() : undefined,
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString() : undefined
} as unknown as WarningSaveReqVO
if (formType.value === 'create') {
await WarningApi.createWarning(data)
message.success(t('common.createSuccess'))
} else {
await WarningApi.updateWarning(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
title: undefined,
content: undefined,
type: 1,
level: 1,
source: 1,
status: 1,
situationId: undefined,
areaId: undefined,
cellId: undefined,
alertTime: undefined,
occurTime: undefined,
remark: undefined,
areaName: undefined,
cellName: undefined,
situationTitle: undefined,
createTime: undefined,
verifyTime: undefined,
verifier: undefined,
verifyResult: undefined,
handleTime: undefined,
handler: undefined,
handleResult: undefined,
releaseTime: undefined,
releaser: undefined,
releaseReason: undefined
}
cellOptions.value = []
formRef.value?.resetFields()
}
//
watch(() => formData.value.areaId, (val) => {
handleAreaChange(val)
})
</script>

View File

@ -0,0 +1,332 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="标题" prop="title">
<el-input
v-model="queryParams.title"
placeholder="请输入标题"
clearable
@keyup.enter="handleQuery"
class="!w-160px"
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in typeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="等级" prop="level">
<el-select
v-model="queryParams.level"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in levelOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in statusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="来源" prop="source">
<el-select
v-model="queryParams.source"
placeholder="请选择"
clearable
class="!w-100px"
>
<el-option
v-for="dict in sourceOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="监区" prop="areaId">
<el-tree-select
v-model="queryParams.areaId"
:data="areaTreeList"
:props="{ label: 'name', value: 'id', children: 'children' }"
placeholder="请选择监区"
clearable
filterable
class="!w-180px"
:render-after-expand="false"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['prison:warning:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增预警
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:warning:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="编号" align="center" prop="id" width="80" />
<el-table-column label="标题" align="center" prop="title" min-width="150" show-overflow-tooltip />
<el-table-column label="类型" align="center" prop="type" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_WARNING_TYPE" :value="row.type" />
</template>
</el-table-column>
<el-table-column label="等级" align="center" prop="level" width="90">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_WARNING_LEVEL" :value="row.level" />
</template>
</el-table-column>
<el-table-column label="来源" align="center" prop="source" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_WARNING_SOURCE" :value="row.source" />
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="{ row }">
<dict-tag :type="DICT_TYPE.PRISON_WARNING_STATUS" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="监区" align="center" prop="areaName" width="120" />
<el-table-column label="预警时间" align="center" prop="alertTime" width="160">
<template #default="{ row }">
<span v-if="row.alertTime">{{ formatDateTime(row.alertTime) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template #default="{ row }">
<span v-if="row.createTime">{{ formatDateTime(row.createTime) }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="{ row }">
<el-button
link
type="primary"
@click="openForm('view', row.id)"
>
<Icon icon="ep:view" class="mr-5px" /> 查看
</el-button>
<el-button
link
type="primary"
@click="openForm('update', row.id)"
v-hasPermi="['prison:warning:update']"
>
<Icon icon="ep:edit" class="mr-5px" /> 修改
</el-button>
<el-button
link
type="danger"
@click="handleDelete(row.id)"
v-hasPermi="['prison:warning:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<WarningForm ref="formRef" @success="getList" />
<!-- 预警处置操作弹窗 -->
<WarningActionForm ref="actionFormRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { WarningApi, WarningPageReqVO } from '@/api/prison/warning'
import WarningForm from './WarningForm.vue'
import WarningActionForm from './WarningActionForm.vue'
defineOptions({ name: 'PrisonWarning' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(false)
const exportLoading = ref(false)
const list = ref<any[]>([])
const total = ref(0)
const selectedIds = ref<number[]>([])
//
const queryParams = reactive<WarningPageReqVO>({
pageNo: 1,
pageSize: 10,
title: undefined,
type: undefined,
level: undefined,
status: undefined,
source: undefined,
areaId: undefined,
cellId: undefined
})
const queryFormRef = ref()
//
const typeOptions = getIntDictOptions(DICT_TYPE.PRISON_WARNING_TYPE)
const levelOptions = getIntDictOptions(DICT_TYPE.PRISON_WARNING_LEVEL)
const sourceOptions = getIntDictOptions(DICT_TYPE.PRISON_WARNING_SOURCE)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_WARNING_STATUS)
//
const areaOptions = ref<{ id: number; name: string }[]>([])
//
const areaTreeList = ref<any[]>([])
/** 获取监区树形列表 */
const loadAreas = async () => {
areaTreeList.value = await WarningApi.AreaApi.getAreaTree()
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await WarningApi.getWarningPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 多选操作 */
const handleSelectionChange = (selection: any[]) => {
selectedIds.value = selection.map((item) => item.id)
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await WarningApi.deleteWarning(id)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 批量删除 */
const handleBatchDelete = async () => {
if (selectedIds.value.length === 0) {
message.warning('请选择要删除的数据')
return
}
try {
await message.delConfirm()
await WarningApi.deleteWarningList(selectedIds.value)
message.success(t('common.delSuccess'))
getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await WarningApi.exportWarning(queryParams)
download.excel(data, '预警管理.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
loadAreas()
})
</script>