fix(performance): 批量更新接口优化问卷系统性能

- 添加batchUpdate API调用
- 修复savePartitions循环调用API问题 (50题只需1次请求)
- 修复onPartitionDragEnd拖拽排序性能问题
- 修复onQuestionDragEnd问题拖拽排序性能问题
- 添加自动填充来源字典支持 PRISON_QUESTION_AUTO_FILL_SOURCE
- 问题表单优化: 折叠面板、分区选择、快速粘贴等
This commit is contained in:
tangweijie 2026-01-13 16:25:02 +08:00
parent 35af632010
commit d4cb996085
26 changed files with 5475 additions and 1 deletions

View File

@ -0,0 +1,53 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 监区信息信息 */
export interface Area {
id: number; // 监区ID
name?: string; // 监区名称
code?: string; // 监区编码
type: number; // 监区类型1-普通监区 2-严管监区 3-医院 4-禁闭室
capacity: number; // 容纳人数
currentCount: number; // 当前人数
sort: number; // 排序
status?: number; // 状态1-启用 2-禁用
remark: string; // 备注
}
// 监区信息 API
export const AreaApi = {
// 查询监区信息分页
getAreaPage: async (params: any) => {
return await request.get({ url: `/prison/area/page`, params })
},
// 查询监区信息详情
getArea: async (id: number) => {
return await request.get({ url: `/prison/area/get?id=` + id })
},
// 新增监区信息
createArea: async (data: Area) => {
return await request.post({ url: `/prison/area/create`, data })
},
// 修改监区信息
updateArea: async (data: Area) => {
return await request.put({ url: `/prison/area/update`, data })
},
// 删除监区信息
deleteArea: async (id: number) => {
return await request.delete({ url: `/prison/area/delete?id=` + id })
},
/** 批量删除监区信息 */
deleteAreaList: async (ids: number[]) => {
return await request.delete({ url: `/prison/area/delete-list?ids=${ids.join(',')}` })
},
// 导出监区信息 Excel
exportArea: async (params) => {
return await request.download({ url: `/prison/area/export-excel`, params })
}
}

View File

@ -0,0 +1,53 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 监室信息信息 */
export interface Cell {
id: number; // 监室ID
areaId?: number; // 所属监区ID
name?: string; // 监室名称
code?: string; // 监室编码
capacity: number; // 床位数量
currentCount: number; // 当前人数
sort: number; // 排序
status?: number; // 状态1-启用 2-禁用
remark: string; // 备注
}
// 监室信息 API
export const CellApi = {
// 查询监室信息分页
getCellPage: async (params: any) => {
return await request.get({ url: `/prison/cell/page`, params })
},
// 查询监室信息详情
getCell: async (id: number) => {
return await request.get({ url: `/prison/cell/get?id=` + id })
},
// 新增监室信息
createCell: async (data: Cell) => {
return await request.post({ url: `/prison/cell/create`, data })
},
// 修改监室信息
updateCell: async (data: Cell) => {
return await request.put({ url: `/prison/cell/update`, data })
},
// 删除监室信息
deleteCell: async (id: number) => {
return await request.delete({ url: `/prison/cell/delete?id=` + id })
},
/** 批量删除监室信息 */
deleteCellList: async (ids: number[]) => {
return await request.delete({ url: `/prison/cell/delete-list?ids=${ids.join(',')}` })
},
// 导出监室信息 Excel
exportCell: async (params) => {
return await request.download({ url: `/prison/cell/export-excel`, params })
}
}

View File

@ -0,0 +1,56 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 消费记录信息 */
export interface Consumption {
id: number; // 记录ID
prisonerId?: number; // 罪犯ID
prisonerNo?: string; // 罪犯编号
type?: number; // 类型1-存款 2-消费 3-转账
amount?: number; // 金额
balance: number; // 账户余额
goodsName: string; // 商品名称
goodsCount: number; // 商品数量
orderNo: string; // 订单号
tradeTime?: string | Dayjs; // 交易时间
status?: number; // 状态1-成功 2-失败
remark: string; // 备注
}
// 消费记录 API
export const ConsumptionApi = {
// 查询消费记录分页
getConsumptionPage: async (params: any) => {
return await request.get({ url: `/prison/consumption/page`, params })
},
// 查询消费记录详情
getConsumption: async (id: number) => {
return await request.get({ url: `/prison/consumption/get?id=` + id })
},
// 新增消费记录
createConsumption: async (data: Consumption) => {
return await request.post({ url: `/prison/consumption/create`, data })
},
// 修改消费记录
updateConsumption: async (data: Consumption) => {
return await request.put({ url: `/prison/consumption/update`, data })
},
// 删除消费记录
deleteConsumption: async (id: number) => {
return await request.delete({ url: `/prison/consumption/delete?id=` + id })
},
/** 批量删除消费记录 */
deleteConsumptionList: async (ids: number[]) => {
return await request.delete({ url: `/prison/consumption/delete-list?ids=${ids.join(',')}` })
},
// 导出消费记录 Excel
exportConsumption: async (params) => {
return await request.download({ url: `/prison/consumption/export-excel`, params })
}
}

View File

@ -0,0 +1,69 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 问卷问题信息 */
export interface Question {
id: number // 问题ID
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 // 帮助说明
placeholder?: string // 占位提示
defaultValue?: string // 默认值
autoFillType?: string // 自动填充类型NONE/AUTO/MANUAL
autoFillSource?: string // 自动填充来源
displayCondition?: string // 显示条件JSON
minValue?: number // 最小值
maxValue?: number // 最大值
createTime?: Date // 创建时间
}
// 问卷问题 API
export const QuestionApi = {
// 查询问卷问题分页
getQuestionPage: async (params: any) => {
return await request.get({ url: `/prison/question/page`, params })
},
// 查询问卷问题详情
getQuestion: async (id: number) => {
return await request.get({ url: `/prison/question/get?id=` + id })
},
// 新增问卷问题
createQuestion: async (data: Question) => {
return await request.post({ url: `/prison/question/create`, data })
},
// 修改问卷问题
updateQuestion: async (data: Question) => {
return await request.put({ url: `/prison/question/update`, data })
},
// 删除问卷问题
deleteQuestion: async (id: number) => {
return await request.delete({ url: `/prison/question/delete?id=` + id })
},
/** 批量删除问卷问题 */
deleteQuestionList: async (ids: number[]) => {
return await request.delete({ url: `/prison/question/delete-list?ids=${ids.join(',')}` })
},
// 导出问卷问题 Excel
exportQuestion: async (params) => {
return await request.download({ url: `/prison/question/export-excel`, params })
},
// 批量更新问卷问题(仅排序和分区字段)
batchUpdate: async (data: { questions: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> }) => {
return await request.post({ url: `/prison/question/batch-update`, data })
}
}

View File

@ -0,0 +1,51 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 问卷模板信息 */
export interface Questionnaire {
id: number; // 问卷ID
title?: string; // 问卷标题
type?: number; // 问卷类型1-心理测评 2-行为评估 3-满意度调查
description: string; // 问卷说明
totalScore: number; // 总分
passScore: number; // 及格分
status?: number; // 状态1-草稿 2-已发布 3-已禁用
}
// 问卷模板 API
export const QuestionnaireApi = {
// 查询问卷模板分页
getQuestionnairePage: async (params: any) => {
return await request.get({ url: `/prison/questionnaire/page`, params })
},
// 查询问卷模板详情
getQuestionnaire: async (id: number) => {
return await request.get({ url: `/prison/questionnaire/get?id=` + id })
},
// 新增问卷模板
createQuestionnaire: async (data: Questionnaire) => {
return await request.post({ url: `/prison/questionnaire/create`, data })
},
// 修改问卷模板
updateQuestionnaire: async (data: Questionnaire) => {
return await request.put({ url: `/prison/questionnaire/update`, data })
},
// 删除问卷模板
deleteQuestionnaire: async (id: number) => {
return await request.delete({ url: `/prison/questionnaire/delete?id=` + id })
},
/** 批量删除问卷模板 */
deleteQuestionnaireList: async (ids: number[]) => {
return await request.delete({ url: `/prison/questionnaire/delete-list?ids=${ids.join(',')}` })
},
// 导出问卷模板 Excel
exportQuestionnaire: async (params) => {
return await request.download({ url: `/prison/questionnaire/export-excel`, params })
}
}

View File

@ -0,0 +1,52 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 问卷答题记录信息 */
export interface QuestionnaireRecord {
id: number; // 记录ID
questionnaireId?: number; // 问卷ID
prisonerId?: number; // 罪犯ID
prisonerNo?: string; // 罪犯编号
totalScore: number; // 得分
passStatus: number; // 是否及格1-及格 2-不及格
answerTime?: string | Dayjs; // 答题时间
status?: number; // 状态1-已完成 2-已过期
}
// 问卷答题记录 API
export const QuestionnaireRecordApi = {
// 查询问卷答题记录分页
getQuestionnaireRecordPage: async (params: any) => {
return await request.get({ url: `/prison/questionnaire-record/page`, params })
},
// 查询问卷答题记录详情
getQuestionnaireRecord: async (id: number) => {
return await request.get({ url: `/prison/questionnaire-record/get?id=` + id })
},
// 新增问卷答题记录
createQuestionnaireRecord: async (data: QuestionnaireRecord) => {
return await request.post({ url: `/prison/questionnaire-record/create`, data })
},
// 修改问卷答题记录
updateQuestionnaireRecord: async (data: QuestionnaireRecord) => {
return await request.put({ url: `/prison/questionnaire-record/update`, data })
},
// 删除问卷答题记录
deleteQuestionnaireRecord: async (id: number) => {
return await request.delete({ url: `/prison/questionnaire-record/delete?id=` + id })
},
/** 批量删除问卷答题记录 */
deleteQuestionnaireRecordList: async (ids: number[]) => {
return await request.delete({ url: `/prison/questionnaire-record/delete-list?ids=${ids.join(',')}` })
},
// 导出问卷答题记录 Excel
exportQuestionnaireRecord: async (params) => {
return await request.download({ url: `/prison/questionnaire-record/export-excel`, params })
}
}

View File

@ -0,0 +1,61 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 危险评估信息 */
export interface RiskAssessment {
id: number; // 评估ID
prisonerId?: number; // 罪犯ID
prisonerNo?: string; // 罪犯编号
assessmentType?: number; // 评估类型1-入狱评估 2-定期评估 3-专项评估
assessmentDate?: string | Dayjs; // 评估日期
violenceScore: number; // 暴力倾向得分
escapeScore: number; // 脱逃倾向得分
suicideScore: number; // 自杀倾向得分
totalScore: number; // 综合得分
riskLevel?: number; // 风险等级1-低风险 2-中风险 3-高风险 4-极高风险
riskFactors: string; // 风险因素
suggestions: string; // 管控建议
assessorId: number; // 评估人ID
assessorName: string; // 评估人姓名
nextAssessmentDate: string | Dayjs; // 下次评估日期
status?: number; // 状态1-待审核 2-已通过
remark: string; // 备注
}
// 危险评估 API
export const RiskAssessmentApi = {
// 查询危险评估分页
getRiskAssessmentPage: async (params: any) => {
return await request.get({ url: `/prison/risk-assessment/page`, params })
},
// 查询危险评估详情
getRiskAssessment: async (id: number) => {
return await request.get({ url: `/prison/risk-assessment/get?id=` + id })
},
// 新增危险评估
createRiskAssessment: async (data: RiskAssessment) => {
return await request.post({ url: `/prison/risk-assessment/create`, data })
},
// 修改危险评估
updateRiskAssessment: async (data: RiskAssessment) => {
return await request.put({ url: `/prison/risk-assessment/update`, data })
},
// 删除危险评估
deleteRiskAssessment: async (id: number) => {
return await request.delete({ url: `/prison/risk-assessment/delete?id=` + id })
},
/** 批量删除危险评估 */
deleteRiskAssessmentList: async (ids: number[]) => {
return await request.delete({ url: `/prison/risk-assessment/delete-list?ids=${ids.join(',')}` })
},
// 导出危险评估 Excel
exportRiskAssessment: async (params) => {
return await request.download({ url: `/prison/risk-assessment/export-excel`, params })
}
}

View File

@ -0,0 +1,58 @@
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 计分考核信息 */
export interface Score {
id: number; // 记录ID
prisonerId?: number; // 罪犯ID
prisonerNo?: string; // 罪犯编号
year?: number; // 考核年份
month?: number; // 考核月份
baseScore: number; // 基础分
rewardScore: number; // 加分
penaltyScore: number; // 扣分
totalScore: number; // 总分
level: number; // 考核等级1-优秀 2-良好 3-合格 4-不合格
assessorId: number; // 考核人ID
assessorName: string; // 考核人姓名
status?: number; // 状态1-待审核 2-已通过 3-已驳回
remark: string; // 备注
}
// 计分考核 API
export const ScoreApi = {
// 查询计分考核分页
getScorePage: async (params: any) => {
return await request.get({ url: `/prison/score/page`, params })
},
// 查询计分考核详情
getScore: async (id: number) => {
return await request.get({ url: `/prison/score/get?id=` + id })
},
// 新增计分考核
createScore: async (data: Score) => {
return await request.post({ url: `/prison/score/create`, data })
},
// 修改计分考核
updateScore: async (data: Score) => {
return await request.put({ url: `/prison/score/update`, data })
},
// 删除计分考核
deleteScore: async (id: number) => {
return await request.delete({ url: `/prison/score/delete?id=` + id })
},
/** 批量删除计分考核 */
deleteScoreList: async (ids: number[]) => {
return await request.delete({ url: `/prison/score/delete-list?ids=${ids.join(',')}` })
},
// 导出计分考核 Excel
exportScore: async (params) => {
return await request.download({ url: `/prison/score/export-excel`, params })
}
}

View File

@ -253,5 +253,18 @@ export enum DICT_TYPE {
PRISON_SUPERVISION_LEVEL = 'prison_supervision_level', // 监管等级
PRISON_RISK_LEVEL = 'prison_risk_level', // 风险等级
PRISONER_STATUS = 'prisoner_status', // 罪犯状态
PRISON_EDUCATION = 'prison_education' // 文化程度
PRISON_EDUCATION = 'prison_education', // 文化程度
PRISON_QUESTION_TYPE = 'prison_question_type', // 问卷问题类型
PRISON_QUESTIONNAIRE_TYPE = 'prison_questionnaire_type', // 问卷类型
PRISON_QUESTIONNAIRE_STATUS = 'prison_questionnaire_status', // 问卷状态
PRISON_CONSUMPTION_TYPE = 'prison_consumption_type', // 消费类型
PRISON_CONSUMPTION_STATUS = 'prison_consumption_status', // 消费状态
PRISON_ASSESSMENT_TYPE = 'prison_assessment_type', // 评估类型
PRISON_SCORE_LEVEL = 'prison_score_level', // 考核等级
PRISON_SCORE_STATUS = 'prison_score_status', // 考核状态
PRISON_AREA_TYPE = 'prison_area_type', // 监区类型
PRISON_CELL_STATUS = 'prison_cell_status', // 监室状态
PRISON_RECORD_PASS_STATUS = 'prison_record_pass_status', // 问卷答题是否及格
PRISON_RECORD_STATUS = 'prison_record_status', // 问卷答题记录状态
PRISON_QUESTION_AUTO_FILL_SOURCE = 'prison_question_auto_fill_source' // 问卷问题自动填充来源
}

View File

@ -0,0 +1,143 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="监区名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入监区名称" />
</el-form-item>
<el-form-item label="监区编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入监区编码" />
</el-form-item>
<el-form-item label="监区类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择监区类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_AREA_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="容纳人数" prop="capacity">
<el-input v-model="formData.capacity" placeholder="请输入容纳人数" />
</el-form-item>
<el-form-item label="当前人数" prop="currentCount">
<el-input v-model="formData.currentCount" placeholder="请输入当前人数" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="formData.sort" placeholder="请输入排序" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_CELL_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" 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 { AreaApi, Area } from '@/api/prison/area'
/** 监区信息 表单 */
defineOptions({ name: 'AreaForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
name: undefined,
code: undefined,
type: undefined,
capacity: undefined,
currentCount: undefined,
sort: undefined,
status: undefined,
remark: undefined
})
const formRules = reactive({
name: [{ required: true, message: '监区名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '监区编码不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
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 {
formData.value = await AreaApi.getArea(id)
} 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 as unknown as Area
if (formType.value === 'create') {
await AreaApi.createArea(data)
message.success(t('common.createSuccess'))
} else {
await AreaApi.updateArea(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
code: undefined,
type: undefined,
capacity: undefined,
currentCount: undefined,
sort: undefined,
status: undefined,
remark: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,252 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="监区名称" prop="name">
<el-input
v-model="queryParams.name"
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-120px"
>
<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="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-90px"
>
<el-option
v-for="dict in statusOptions"
: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:area:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:area:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:area:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="监区ID" align="center" prop="id" width="80" />
<el-table-column label="监区名称" align="center" prop="name" width="120" />
<el-table-column label="监区编码" align="center" prop="code" width="120" />
<el-table-column label="监区类型" align="center" prop="type" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_AREA_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="容纳人数" align="center" prop="capacity" width="90" />
<el-table-column label="当前人数" align="center" prop="currentCount" width="90" />
<el-table-column label="排序" align="center" prop="sort" width="70" />
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_CELL_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:area:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:area:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<AreaForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { AreaApi, Area } from '@/api/prison/area'
import AreaForm from './AreaForm.vue'
defineOptions({ name: 'Area' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<Area[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
type: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
// 使
const typeOptions = getIntDictOptions(DICT_TYPE.PRISON_AREA_TYPE)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_CELL_STATUS)
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await AreaApi.getAreaPage(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 AreaApi.deleteArea(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: Area[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await AreaApi.deleteAreaList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
await message.exportConfirm()
exportLoading.value = true
const data = await AreaApi.exportArea(queryParams)
download.excel(data, '监区信息.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,137 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="所属监区ID" prop="areaId">
<el-input v-model="formData.areaId" placeholder="请输入所属监区ID" />
</el-form-item>
<el-form-item label="监室名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入监室名称" />
</el-form-item>
<el-form-item label="监室编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入监室编码" />
</el-form-item>
<el-form-item label="床位数量" prop="capacity">
<el-input v-model="formData.capacity" placeholder="请输入床位数量" />
</el-form-item>
<el-form-item label="当前人数" prop="currentCount">
<el-input v-model="formData.currentCount" placeholder="请输入当前人数" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input v-model="formData.sort" placeholder="请输入排序" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_CELL_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" 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 { CellApi, Cell } from '@/api/prison/cell'
/** 监室信息 表单 */
defineOptions({ name: 'CellForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
areaId: undefined,
name: undefined,
code: undefined,
capacity: undefined,
currentCount: undefined,
sort: undefined,
status: undefined,
remark: undefined
})
const formRules = reactive({
areaId: [{ required: true, message: '所属监区ID不能为空', trigger: 'blur' }],
name: [{ required: true, message: '监室名称不能为空', trigger: 'blur' }],
code: [{ required: true, message: '监室编码不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
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 {
formData.value = await CellApi.getCell(id)
} 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 as unknown as Cell
if (formType.value === 'create') {
await CellApi.createCell(data)
message.success(t('common.createSuccess'))
} else {
await CellApi.updateCell(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
areaId: undefined,
name: undefined,
code: undefined,
capacity: undefined,
currentCount: undefined,
sort: undefined,
status: undefined,
remark: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,241 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<el-form-item label="所属监区" prop="areaId">
<el-input
v-model="queryParams.areaId"
placeholder="请输入监区ID"
clearable
@keyup.enter="handleQuery"
class="!w-140px"
/>
</el-form-item>
<el-form-item label="监室名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入监室名称"
clearable
@keyup.enter="handleQuery"
class="!w-160px"
/>
</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>
<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:cell:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:cell:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:cell:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="监室ID" align="center" prop="id" width="80" />
<el-table-column label="所属监区" align="center" prop="areaId" width="100" />
<el-table-column label="监室名称" align="center" prop="name" width="120" />
<el-table-column label="监室编码" align="center" prop="code" width="120" />
<el-table-column label="床位数量" align="center" prop="capacity" width="90" />
<el-table-column label="当前人数" align="center" prop="currentCount" width="90" />
<el-table-column label="排序" align="center" prop="sort" width="70" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_CELL_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:cell:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:cell: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>
<!-- 表单弹窗添加/修改 -->
<CellForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { CellApi, Cell } from '@/api/prison/cell'
import CellForm from './CellForm.vue'
defineOptions({ name: 'Cell' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<Cell[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
areaId: undefined,
name: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
// 使
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_CELL_STATUS)
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await CellApi.getCellPage(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 CellApi.deleteCell(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: Cell[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await CellApi.deleteCellList(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 CellApi.exportCell(queryParams)
download.excel(data, '监室信息.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,166 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
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>
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_CONSUMPTION_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="金额" prop="amount">
<el-input v-model="formData.amount" placeholder="请输入金额" />
</el-form-item>
<el-form-item label="账户余额" prop="balance">
<el-input v-model="formData.balance" placeholder="请输入账户余额" />
</el-form-item>
<el-form-item label="商品名称" prop="goodsName">
<el-input v-model="formData.goodsName" placeholder="请输入商品名称" />
</el-form-item>
<el-form-item label="商品数量" prop="goodsCount">
<el-input v-model="formData.goodsCount" placeholder="请输入商品数量" />
</el-form-item>
<el-form-item label="订单号" prop="orderNo">
<el-input v-model="formData.orderNo" placeholder="请输入订单号" />
</el-form-item>
<el-form-item label="交易时间" prop="tradeTime">
<el-date-picker
v-model="formData.tradeTime"
type="date"
value-format="x"
placeholder="选择交易时间"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_CONSUMPTION_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" 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 { ConsumptionApi, Consumption } from '@/api/prison/consumption'
/** 消费记录 表单 */
defineOptions({ name: 'ConsumptionForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
prisonerId: undefined,
prisonerNo: undefined,
type: undefined,
amount: undefined,
balance: undefined,
goodsName: undefined,
goodsCount: undefined,
orderNo: undefined,
tradeTime: undefined,
status: undefined,
remark: undefined
})
const formRules = reactive({
prisonerId: [{ required: true, message: '罪犯ID不能为空', trigger: 'blur' }],
prisonerNo: [{ required: true, message: '罪犯编号不能为空', trigger: 'blur' }],
type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
amount: [{ required: true, message: '金额不能为空', trigger: 'blur' }],
tradeTime: [{ required: true, message: '交易时间不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
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 {
formData.value = await ConsumptionApi.getConsumption(id)
} 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 as unknown as Consumption
if (formType.value === 'create') {
await ConsumptionApi.createConsumption(data)
message.success(t('common.createSuccess'))
} else {
await ConsumptionApi.updateConsumption(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
prisonerId: undefined,
prisonerNo: undefined,
type: undefined,
amount: undefined,
balance: undefined,
goodsName: undefined,
goodsCount: undefined,
orderNo: undefined,
tradeTime: undefined,
status: undefined,
remark: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,258 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<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="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="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-90px"
>
<el-option
v-for="dict in statusOptions"
: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:consumption:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:consumption:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:consumption:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="记录ID" align="center" prop="id" width="80" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
<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" />
</template>
</el-table-column>
<el-table-column label="金额" align="center" prop="amount" width="100" />
<el-table-column label="账户余额" align="center" prop="balance" width="100" />
<el-table-column label="商品名称" align="center" prop="goodsName" width="150" />
<el-table-column label="商品数量" align="center" prop="goodsCount" width="90" />
<el-table-column label="订单号" align="center" prop="orderNo" width="180" />
<el-table-column label="交易时间" align="center" prop="tradeTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.tradeTime) }}
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_CONSUMPTION_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:consumption:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:consumption: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>
<!-- 表单弹窗添加/修改 -->
<ConsumptionForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { ConsumptionApi, Consumption } from '@/api/prison/consumption'
import ConsumptionForm from './ConsumptionForm.vue'
defineOptions({ name: 'Consumption' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<Consumption[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
prisonerNo: undefined,
type: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
// 使
const typeOptions = getIntDictOptions(DICT_TYPE.PRISON_CONSUMPTION_TYPE)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_CONSUMPTION_STATUS)
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ConsumptionApi.getConsumptionPage(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 ConsumptionApi.deleteConsumption(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: Consumption[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await ConsumptionApi.deleteConsumptionList(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 ConsumptionApi.exportConsumption(queryParams)
download.excel(data, '消费记录.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

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

@ -0,0 +1,247 @@
<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">
{{ formatDate(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)
// (1- 2- 3- 4-)
const questionTypeOptions = [
{ label: '单选', value: 1 },
{ label: '多选', value: 2 },
{ label: '填空', value: 3 },
{ label: '评分', value: 4 }
]
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 查询列表 */
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

@ -0,0 +1,134 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="问卷标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入问卷标题" />
</el-form-item>
<el-form-item label="问卷类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择问卷类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="问卷说明" prop="description">
<Editor v-model="formData.description" height="150px" />
</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="status">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</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 { QuestionnaireApi, Questionnaire } from '@/api/prison/questionnaire'
/** 问卷模板 表单 */
defineOptions({ name: 'QuestionnaireForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
title: undefined,
type: undefined,
description: undefined,
totalScore: undefined,
passScore: undefined,
status: undefined
})
const formRules = reactive({
title: [{ required: true, message: '问卷标题不能为空', trigger: 'blur' }],
type: [{ required: true, message: '问卷类型不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
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 {
formData.value = await QuestionnaireApi.getQuestionnaire(id)
} 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 as unknown as Questionnaire
if (formType.value === 'create') {
await QuestionnaireApi.createQuestionnaire(data)
message.success(t('common.createSuccess'))
} else {
await QuestionnaireApi.updateQuestionnaire(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
title: undefined,
type: undefined,
description: undefined,
totalScore: undefined,
passScore: undefined,
status: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,804 @@
<template>
<!-- 列表 -->
<ContentWrap>
<div class="question-header">
<div class="header-left">
<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="success"
plain
@click="openPartDialog"
v-hasPermi="['prison:question:create']"
>
<Icon icon="ep:folder" 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>
</div>
<div class="header-right">
<span class="total-count"> {{ list.length }} 道问题</span>
</div>
</div>
<!-- 分区显示可拖拽排序 -->
<div class="parts-container">
<draggable
v-model="partitions"
item-key="name"
handle=".part-drag-handle"
:animation="200"
@end="onPartitionDragEnd"
class="parts-list"
>
<template #item="{ element: partition }">
<div class="part-group" :class="{ 'is-default': !partition.name }">
<!-- 分区头部 -->
<div class="part-header">
<div class="part-drag-handle">
<Icon icon="ep:rank" class="drag-icon" />
</div>
<el-checkbox
v-model="partition.selected"
@change="(val: boolean) => togglePartQuestions(partition, val)"
:disabled="!partition.name"
>
<span class="part-title">{{ partition.name || '默认分区' }}</span>
</el-checkbox>
<span class="part-count">({{ partition.questions.length }} 道题)</span>
<div class="part-actions">
<el-button
v-if="partition.name"
type="primary"
link
size="small"
@click="editPartition(partition)"
>
<Icon icon="ep:edit" /> 编辑
</el-button>
<el-button
v-if="partition.name"
type="danger"
link
size="small"
@click="deletePartition(partition)"
>
<Icon icon="ep:delete" /> 删除
</el-button>
</div>
</div>
<!-- 分区内的问题列表可拖拽排序 -->
<draggable
v-model="partition.questions"
item-key="id"
handle=".question-drag-handle"
:animation="200"
@end="onQuestionDragEnd(partition)"
class="questions-table"
ghost-class="ghost-row"
>
<template #item="{ element: question }">
<div class="question-row" :class="{ 'is-hidden': !partition.selected }">
<div class="question-drag-handle">
<Icon icon="ep:rank" class="drag-icon" />
</div>
<el-checkbox
v-model="question._checked"
@change="(val: boolean) => toggleQuestionCheck(question, val, partition)"
class="question-checkbox"
/>
<span class="question-index">{{ question._index + 1 }}</span>
<div class="question-info">
<div class="question-title-row">
<span class="type-badge" :class="'type-' + question.type">
{{ getTypeLabel(question.type) }}
</span>
<span class="title-text">{{ question.title }}</span>
<el-tag v-if="question.isRequired" type="danger" size="small">必填</el-tag>
</div>
<div v-if="question.helpText" class="help-text">
<Icon icon="ep:info-filled" />
{{ question.helpText }}
</div>
</div>
<div class="question-meta">
<span class="meta-item">
<Icon icon="ep:coin" /> {{ question.score || 0 }}
</span>
<el-tooltip
v-if="question.autoFillType && question.autoFillType !== 'NONE'"
:content="getAutoFillContent(question)"
placement="top"
>
<el-tag :type="question.autoFillType === 'AUTO' ? 'primary' : 'warning'" size="small">
{{ getAutoFillLabel(question.autoFillType) }}
</el-tag>
</el-tooltip>
</div>
<div class="question-actions">
<el-button
type="primary"
link
size="small"
@click="openForm('update', question.id)"
v-hasPermi="['prison:question:update']"
>
<Icon icon="ep:edit" /> 修改
</el-button>
<el-button
type="danger"
link
size="small"
@click="handleDelete(question.id)"
v-hasPermi="['prison:question:delete']"
>
<Icon icon="ep:delete" /> 删除
</el-button>
</div>
</div>
</template>
</draggable>
</div>
</template>
</draggable>
</div>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<QuestionForm ref="formRef" @success="getList" />
<!-- 分区管理弹窗 -->
<Dialog title="分区管理" v-model="partDialogVisible" width="600px">
<el-form :model="partForm" label-width="100px">
<el-form-item label="分区列表">
<div class="part-manage-list">
<div v-for="(part, index) in allPartitions" :key="part.id || index" class="part-manage-item">
<el-icon class="drag-handle"><Rank /></el-icon>
<el-input
v-model="part.name"
:placeholder="part.isDefault ? '默认分区' : '请输入分区名称'"
:disabled="part.isDefault"
style="flex: 1; margin: 0 10px"
/>
<el-input-number
v-model="part.sort"
:min="0"
:max="999"
controls-position="right"
style="width: 100px"
:disabled="part.isDefault"
/>
<el-button
v-if="!part.isDefault"
type="danger"
:icon="Delete"
circle
@click="removePartition(index)"
/>
</div>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" plain :icon="Plus" @click="addPartition">添加分区</el-button>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="savePartitions" type="primary">保存设置</el-button>
<el-button @click="partDialogVisible = false">取消</el-button>
</template>
</Dialog>
</template>
<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 { Folder, InfoFilled, Rank, Plus, Delete } from '@element-plus/icons-vue'
import draggable from 'vuedraggable'
defineOptions({ name: 'PrisonQuestionList' })
const message = useMessage()
const { t } = useI18n()
const props = defineProps<{
questionnaireId?: number
}>()
const loading = ref(false)
const list = ref<Question[]>([])
const formRef = ref()
const partDialogVisible = ref(false)
//
const partitions = ref<Array<{
name: string
sort: number
selected: boolean
questions: Array<Question & { _index: number; _checked: boolean }>
}>>([])
//
const allPartList = ref<Array<{ id: string; name: string; sort: number; isDefault: boolean }>>([])
const allPartitions = computed({
get: () => allPartList.value,
set: (val) => { allPartList.value = val }
})
//
const partForm = ref({
name: '',
sort: 0
})
// ID
const checkedIds = ref<number[]>([])
/** 问题类型标签 */
const getTypeLabel = (type: number) => {
const labels: Record<number, string> = { 1: '单选', 2: '多选', 3: '填空', 4: '评分', 5: '日期', 6: '数字' }
return labels[type] || '未知'
}
/** 自动填充标签 */
const getAutoFillLabel = (type: string) => {
return { 'NONE': '-', 'AUTO': '自动', 'MANUAL': '手动' }[type] || type
}
/** 自动填充内容 */
const getAutoFillContent = (row: Question) => {
if (row.autoFillType === 'AUTO') {
return `自动填充来源:${row.autoFillSource || '-'}`
}
return '手动输入'
}
/** 从问题列表提取所有分区 */
const extractPartitions = (questions: Question[]) => {
const partMap = new Map<string, Question[]>()
const defaultPart: Question[] = []
questions.forEach(q => {
const partName = q.partName || ''
if (partName) {
if (!partMap.has(partName)) {
partMap.set(partName, [])
}
partMap.get(partName)!.push(q)
} else {
defaultPart.push(q)
}
})
//
const sortedParts = Array.from(partMap.entries())
.sort((a, b) => {
const partA = partitions.value.find(p => p.name === a[0])
const partB = partitions.value.find(p => p.name === b[0])
const sortA = partA?.sort ?? 0
const sortB = partB?.sort ?? 0
return sortA - sortB
})
//
const result: typeof partitions.value = []
//
result.push({
name: '',
sort: 0,
selected: true,
questions: defaultPart.map((q, i) => ({ ...q, _index: i, _checked: false }))
})
//
sortedParts.forEach(([name, qs]) => {
result.push({
name,
sort: partitions.value.find(p => p.name === name)?.sort ?? 0,
selected: true,
questions: qs.map((q, i) => ({ ...q, _index: i, _checked: false }))
})
})
return result
}
/** 加载问题列表 */
const getList = async () => {
if (!props.questionnaireId) return
loading.value = true
try {
const data = await QuestionApi.getQuestionPage({
pageNo: 1,
pageSize: 200,
questionnaireId: props.questionnaireId
})
list.value = data.list
partitions.value = extractPartitions(data.list)
//
if (allPartList.value.length === 0) {
//
allPartList.value.push({
id: 'default',
name: '',
sort: 0,
isDefault: true
})
//
const existingNames = new Set<string>()
partitions.value.forEach(p => {
if (p.name && !existingNames.has(p.name)) {
existingNames.add(p.name)
allPartList.value.push({
id: `part_${p.name}`,
name: p.name,
sort: p.sort,
isDefault: false
})
}
})
}
} finally {
loading.value = false
}
}
/** 监听问卷ID变化 */
watch(
() => props.questionnaireId,
(val) => {
if (!val) {
list.value = []
partitions.value = []
return
}
getList()
},
{ immediate: true }
)
/** 打开表单 */
const openForm = (type: string, id?: number) => {
if (!props.questionnaireId) {
message.error('请选择一个问卷')
return
}
//
formRef.value.open(type, id, props.questionnaireId, getPartitionNames())
}
/** 获取分区名称列表 */
const getPartitionNames = () => {
return allPartList.value
.filter(p => !p.isDefault && p.name)
.map((p, index) => ({
label: p.name,
value: p.name,
sort: index
}))
.sort((a, b) => a.sort - b.sort)
}
/** 打开分区管理弹窗 */
const openPartDialog = () => {
//
if (allPartList.value.length === 0 || !allPartList.value[0]?.isDefault) {
//
allPartList.value.unshift({
id: 'default',
name: '',
sort: 0,
isDefault: true
})
}
//
for (let i = 1; i < allPartList.value.length; i++) {
allPartList.value[i].isDefault = false
}
partDialogVisible.value = true
}
/** 添加分区 */
const addPartition = () => {
const newSort = allPartList.value.length > 0
? Math.max(...allPartList.value.map(p => p.sort || 0)) + 1
: 0
allPartList.value.push({
id: `part_${Date.now()}`,
name: '',
sort: newSort,
isDefault: false
})
}
/** 删除分区 */
const removePartition = (index: number) => {
allPartList.value.splice(index, 1)
}
/** 编辑分区名称 */
const editPartition = (partition: typeof partitions.value[0]) => {
const part = allPartList.value.find(p => p.name === partition.name)
if (part) {
//
message.info('请在下方分区管理列表中编辑分区名称')
}
}
/** 删除分区 */
const deletePartition = async (partition: typeof partitions.value[0]) => {
try {
await message.delConfirm(`确定删除分区"${partition.name}"吗?该分区下的问题将移到默认分区`)
//
for (const q of partition.questions) {
await QuestionApi.updateQuestion({ ...q, id: q.id, partName: '' })
}
await getList()
message.success('删除成功')
} catch {}
}
/** 保存分区设置 */
const savePartitions = async () => {
try {
//
const names = allPartList.value.filter(p => !p.isDefault).map(p => p.name).filter(n => n)
if (new Set(names).size !== names.length) {
message.error('分区名称不能重复')
return
}
//
const emptyParts = allPartList.value.filter(p => !p.isDefault && !p.name)
if (emptyParts.length > 0) {
message.error('请为所有分区输入名称')
return
}
//
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
for (let i = 0; i < allPartList.value.length; i++) {
const part = allPartList.value[i]
if (!part.isDefault && part.name) {
for (const p of partitions.value) {
if (p.name === part.name) {
p.questions.forEach((q, sortIndex) => {
updates.push({
id: q.id!,
partName: part.name,
partSort: i,
sort: sortIndex
})
})
}
}
}
}
//
await QuestionApi.batchUpdate({ questions: updates })
await getList()
partDialogVisible.value = false
message.success('保存成功')
} catch (e) {
message.error('保存失败')
}
}
/** 分区拖拽排序完成 */
const onPartitionDragEnd = async () => {
//
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
for (let i = 0; i < partitions.value.length; i++) {
const part = partitions.value[i]
if (part.name) {
part.questions.forEach((q, sortIndex) => {
updates.push({
id: q.id!,
partName: part.name,
partSort: i,
sort: sortIndex
})
})
}
}
//
await QuestionApi.batchUpdate({ questions: updates })
message.success('分区排序已更新')
}
/** 问题拖拽排序完成 */
const onQuestionDragEnd = async (partition: typeof partitions.value[0]) => {
//
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
partition.questions.forEach((q, i) => {
updates.push({
id: q.id!,
partName: partition.name || undefined,
partSort: partition.sort,
sort: i
})
})
//
await QuestionApi.batchUpdate({ questions: updates })
message.success('问题排序已更新')
}
/** 切换分区显示/隐藏 */
const togglePartQuestions = (partition: typeof partitions.value[0], selected: boolean) => {
partition.questions.forEach(q => {
q._checked = selected
})
}
/** 切换问题选中状态 */
const toggleQuestionCheck = (question: Question & { _index: number; _checked: boolean }, val: boolean, partition: typeof partitions.value[0]) => {
if (val) {
if (!checkedIds.value.includes(question.id!)) {
checkedIds.value.push(question.id!)
}
} else {
checkedIds.value = checkedIds.value.filter(id => id !== question.id)
}
}
/** 删除问题 */
const handleDelete = async (id: number) => {
try {
await message.delConfirm()
await QuestionApi.deleteQuestion(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除 */
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await QuestionApi.deleteQuestionList(checkedIds.value)
checkedIds.value = []
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
</script>
<style scoped>
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.header-left {
display: flex;
gap: 10px;
}
.header-right {
color: #909399;
font-size: 14px;
}
.total-count {
padding: 4px 12px;
background: #f4f4f5;
border-radius: 4px;
}
.parts-container {
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
}
.parts-list {
display: flex;
flex-direction: column;
}
.part-group {
border-bottom: 1px solid #e4e7ed;
}
.part-group:last-child {
border-bottom: none;
}
.part-group.is-default {
background: #fafafa;
}
.part-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4fd 100%);
border-bottom: 1px solid #e4e7ed;
}
.part-drag-handle {
cursor: grab;
color: #c0c4cc;
display: flex;
align-items: center;
}
.part-drag-handle:active {
cursor: grabbing;
}
.drag-icon {
font-size: 16px;
}
.part-title {
font-weight: 600;
color: #1a5cb8;
font-size: 14px;
}
.part-count {
color: #909399;
font-size: 12px;
}
.part-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
.questions-table {
min-height: 50px;
}
.question-row {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
transition: all 0.3s;
}
.question-row:hover {
background: #f5f7fa;
}
.question-row.is-hidden {
opacity: 0.5;
}
.question-row:last-child {
border-bottom: none;
}
.ghost-row {
background: #f0f9eb;
opacity: 0.8;
}
.question-drag-handle {
cursor: grab;
color: #c0c4cc;
display: flex;
align-items: center;
}
.question-drag-handle:active {
cursor: grabbing;
}
.question-checkbox {
margin-right: 8px;
}
.question-index {
width: 30px;
text-align: center;
color: #909399;
font-weight: 500;
}
.question-info {
flex: 1;
min-width: 0;
}
.question-title-row {
display: flex;
align-items: center;
gap: 8px;
}
.title-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.type-badge {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
flex-shrink: 0;
}
.type-1 { background: #e6f7ff; color: #1890ff; }
.type-2 { background: #f6ffed; color: #52c41a; }
.type-3 { background: #fff7e6; color: #fa8c16; }
.type-4 { background: #f9f0ff; color: #722ed1; }
.type-5 { background: #fff1f0; color: #f5222d; }
.type-6 { background: #e6fffb; color: #13c2c2; }
.help-text {
font-size: 12px;
color: #909399;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.question-meta {
display: flex;
align-items: center;
gap: 12px;
min-width: 150px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
color: #606266;
font-size: 13px;
}
.question-actions {
display: flex;
gap: 8px;
}
.part-manage-list {
max-height: 400px;
overflow-y: auto;
}
.part-manage-item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
margin-bottom: 8px;
background: #f5f7fa;
border-radius: 4px;
}
.drag-handle {
cursor: move;
color: #c0c4cc;
}
</style>

View File

@ -0,0 +1,273 @@
<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-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 questionnaireTypeOptions"
: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-120px"
>
<el-option
v-for="dict in questionnaireStatusOptions"
: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:questionnaire:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:questionnaire:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:questionnaire:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
highlight-current-row
@current-change="handleCurrentChange"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="问卷ID" align="center" prop="id" width="80" />
<el-table-column label="问卷标题" align="center" prop="title" width="200" />
<el-table-column label="问卷类型" align="center" prop="type" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="问卷说明" align="center" prop="description" width="200" />
<el-table-column label="总分" align="center" prop="totalScore" width="80" />
<el-table-column label="及格分" align="center" prop="passScore" width="80" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_QUESTIONNAIRE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:questionnaire:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:questionnaire: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>
<!-- 子表的列表 -->
<ContentWrap>
<el-tabs model-value="question">
<el-tab-pane label="问卷问题" name="question">
<QuestionList :questionnaire-id="currentRow.id" />
</el-tab-pane>
</el-tabs>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<QuestionnaireForm 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 { QuestionnaireApi, Questionnaire } from '@/api/prison/questionnaire'
import QuestionnaireForm from './QuestionnaireForm.vue'
import QuestionList from './components/QuestionList.vue'
defineOptions({ name: 'Questionnaire' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<Questionnaire[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
title: undefined,
type: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
// 使
const questionnaireTypeOptions = getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE)
const questionnaireStatusOptions = getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_STATUS)
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await QuestionnaireApi.getQuestionnairePage(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 QuestionnaireApi.deleteQuestionnaire(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: Questionnaire[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await QuestionnaireApi.deleteQuestionnaireList(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 QuestionnaireApi.exportQuestionnaire(queryParams)
download.excel(data, '问卷模板.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 选中行操作 */
const currentRow = ref<Questionnaire>({} as Questionnaire)
const handleCurrentChange = (row: Questionnaire | undefined) => {
currentRow.value = row || {} as Questionnaire
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,144 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="问卷ID" prop="questionnaireId">
<el-input v-model="formData.questionnaireId" placeholder="请输入问卷ID" />
</el-form-item>
<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>
<el-form-item label="得分" prop="totalScore">
<el-input v-model="formData.totalScore" placeholder="请输入得分" />
</el-form-item>
<el-form-item label="是否及格" prop="passStatus">
<el-radio-group v-model="formData.passStatus">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RECORD_PASS_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="答题时间" prop="answerTime">
<el-date-picker
v-model="formData.answerTime"
type="date"
value-format="x"
placeholder="选择答题时间"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_RECORD_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</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 { QuestionnaireRecordApi, QuestionnaireRecord } from '@/api/prison/questionnairerecord'
/** 问卷答题记录 表单 */
defineOptions({ name: 'QuestionnaireRecordForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
const formData = ref({
id: undefined,
questionnaireId: undefined,
prisonerId: undefined,
prisonerNo: undefined,
totalScore: undefined,
passStatus: undefined,
answerTime: undefined,
status: undefined
})
const formRules = reactive({
questionnaireId: [{ required: true, message: '问卷ID不能为空', trigger: 'blur' }],
prisonerId: [{ required: true, message: '罪犯ID不能为空', trigger: 'blur' }],
prisonerNo: [{ required: true, message: '罪犯编号不能为空', trigger: 'blur' }],
answerTime: [{ required: true, message: '答题时间不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
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 {
formData.value = await QuestionnaireRecordApi.getQuestionnaireRecord(id)
} 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 as unknown as QuestionnaireRecord
if (formType.value === 'create') {
await QuestionnaireRecordApi.createQuestionnaireRecord(data)
message.success(t('common.createSuccess'))
} else {
await QuestionnaireRecordApi.updateQuestionnaireRecord(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
questionnaireId: undefined,
prisonerId: undefined,
prisonerNo: undefined,
totalScore: undefined,
passStatus: undefined,
answerTime: undefined,
status: undefined
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,321 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="问卷ID" prop="questionnaireId">
<el-input
v-model="queryParams.questionnaireId"
placeholder="请输入问卷ID"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="罪犯ID" prop="prisonerId">
<el-input
v-model="queryParams.prisonerId"
placeholder="请输入罪犯ID"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="罪犯编号" prop="prisonerNo">
<el-input
v-model="queryParams.prisonerNo"
placeholder="请输入罪犯编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="得分" prop="totalScore">
<el-input
v-model="queryParams.totalScore"
placeholder="请输入得分"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="是否及格" prop="passStatus">
<el-select
v-model="queryParams.passStatus"
placeholder="请选择是否及格"
clearable
class="!w-240px"
>
<el-option
v-for="dict in passStatusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="答题时间" prop="answerTime">
<el-date-picker
v-model="queryParams.answerTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-220px"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="请选择状态"
clearable
class="!w-240px"
>
<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="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
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="openForm('create')"
v-hasPermi="['prison:questionnaire-record:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:questionnaire-record:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="isEmpty(checkedIds)"
@click="handleDeleteBatch"
v-hasPermi="['prison:questionnaire-record:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
row-key="id"
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="记录ID" align="center" prop="id" />
<el-table-column label="问卷ID" align="center" prop="questionnaireId" />
<el-table-column label="罪犯ID" align="center" prop="prisonerId" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" />
<el-table-column label="得分" align="center" prop="totalScore" />
<el-table-column label="是否及格" align="center" prop="passStatus">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_RECORD_PASS_STATUS" :value="scope.row.passStatus" />
</template>
</el-table-column>
<el-table-column
label="答题时间"
align="center"
prop="answerTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_RECORD_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center" min-width="120px">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:questionnaire-record:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:questionnaire-record: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>
<!-- 表单弹窗添加/修改 -->
<QuestionnaireRecordForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { isEmpty } from '@/utils/is'
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { QuestionnaireRecordApi, QuestionnaireRecord } from '@/api/prison/questionnairerecord'
import QuestionnaireRecordForm from './QuestionnaireRecordForm.vue'
/** 问卷答题记录 列表 */
defineOptions({ name: 'QuestionnaireRecord' })
const message = useMessage() //
const { t } = useI18n() //
const loading = ref(true) //
const list = ref<QuestionnaireRecord[]>([]) //
const total = ref(0) //
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
questionnaireId: undefined,
prisonerId: undefined,
prisonerNo: undefined,
totalScore: undefined,
passStatus: undefined,
answerTime: [],
status: undefined,
createTime: []
})
const queryFormRef = ref() //
const exportLoading = ref(false) //
// 使
const passStatusOptions = getIntDictOptions(DICT_TYPE.PRISON_RECORD_PASS_STATUS)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_RECORD_STATUS)
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await QuestionnaireRecordApi.getQuestionnaireRecordPage(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 QuestionnaireRecordApi.deleteQuestionnaireRecord(id)
message.success(t('common.delSuccess'))
//
await getList()
} catch {}
}
/** 批量删除问卷答题记录 */
const handleDeleteBatch = async () => {
try {
//
await message.delConfirm()
await QuestionnaireRecordApi.deleteQuestionnaireRecordList(checkedIds.value);
checkedIds.value = [];
message.success(t('common.delSuccess'))
await getList();
} catch {}
}
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (records: QuestionnaireRecord[]) => {
checkedIds.value = records.map((item) => item.id!);
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
//
await message.exportConfirm()
//
exportLoading.value = true
const data = await QuestionnaireRecordApi.exportQuestionnaireRecord(queryParams)
download.excel(data, '问卷答题记录.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,203 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
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>
<el-form-item label="评估类型" prop="assessmentType">
<el-select v-model="formData.assessmentType" placeholder="请选择评估类型">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_ASSESSMENT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="评估日期" prop="assessmentDate">
<el-date-picker
v-model="formData.assessmentDate"
type="date"
value-format="x"
placeholder="选择评估日期"
/>
</el-form-item>
<el-form-item label="暴力倾向得分" prop="violenceScore">
<el-input v-model="formData.violenceScore" placeholder="请输入暴力倾向得分" />
</el-form-item>
<el-form-item label="脱逃倾向得分" prop="escapeScore">
<el-input v-model="formData.escapeScore" placeholder="请输入脱逃倾向得分" />
</el-form-item>
<el-form-item label="自杀倾向得分" prop="suicideScore">
<el-input v-model="formData.suicideScore" placeholder="请输入自杀倾向得分" />
</el-form-item>
<el-form-item label="综合得分" prop="totalScore">
<el-input v-model="formData.totalScore" placeholder="请输入综合得分" />
</el-form-item>
<el-form-item label="风险等级" prop="riskLevel">
<el-select v-model="formData.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="风险因素" prop="riskFactors">
<el-input v-model="formData.riskFactors" placeholder="请输入风险因素" />
</el-form-item>
<el-form-item label="管控建议" prop="suggestions">
<el-input v-model="formData.suggestions" placeholder="请输入管控建议" />
</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="nextAssessmentDate">
<el-date-picker
v-model="formData.nextAssessmentDate"
type="date"
value-format="x"
placeholder="选择下次评估日期"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SCORE_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" 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 { RiskAssessmentApi, RiskAssessment } from '@/api/prison/riskassessment'
/** 危险评估 表单 */
defineOptions({ name: 'RiskAssessmentForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
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
})
const formRules = reactive({
prisonerId: [{ required: true, message: '罪犯ID不能为空', trigger: 'blur' }],
prisonerNo: [{ required: true, message: '罪犯编号不能为空', trigger: 'blur' }],
assessmentType: [{ required: true, message: '评估类型不能为空', trigger: 'change' }],
assessmentDate: [{ required: true, message: '评估日期不能为空', trigger: 'blur' }],
riskLevel: [{ required: true, message: '风险等级不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
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 {
formData.value = await RiskAssessmentApi.getRiskAssessment(id)
} 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 as unknown as RiskAssessment
if (formType.value === 'create') {
await RiskAssessmentApi.createRiskAssessment(data)
message.success(t('common.createSuccess'))
} else {
await RiskAssessmentApi.updateRiskAssessment(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
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
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,277 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<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="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="status">
<el-select
v-model="queryParams.status"
placeholder="请选择"
clearable
class="!w-90px"
>
<el-option
v-for="dict in statusOptions"
: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:risk-assessment:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:risk-assessment:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:risk-assessment:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="评估ID" align="center" prop="id" width="80" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
<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="violenceScore" width="90" />
<el-table-column label="脱逃得分" align="center" prop="escapeScore" width="90" />
<el-table-column label="自杀得分" align="center" prop="suicideScore" width="90" />
<el-table-column label="综合得分" align="center" prop="totalScore" width="90" />
<el-table-column label="风险等级" align="center" prop="riskLevel" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_RISK_LEVEL" :value="scope.row.riskLevel" />
</template>
</el-table-column>
<el-table-column label="风险因素" align="center" prop="riskFactors" width="150" />
<el-table-column label="评估人" align="center" prop="assessorName" width="100" />
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_SCORE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:risk-assessment:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:risk-assessment: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>
<!-- 表单弹窗添加/修改 -->
<RiskAssessmentForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { RiskAssessmentApi, RiskAssessment } from '@/api/prison/riskassessment'
import RiskAssessmentForm from './RiskAssessmentForm.vue'
defineOptions({ name: 'RiskAssessment' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<RiskAssessment[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
prisonerNo: undefined,
assessmentType: undefined,
riskLevel: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
// 使
const assessmentTypeOptions = getIntDictOptions(DICT_TYPE.PRISON_ASSESSMENT_TYPE)
const riskLevelOptions = getIntDictOptions(DICT_TYPE.PRISON_RISK_LEVEL)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_SCORE_STATUS)
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await RiskAssessmentApi.getRiskAssessmentPage(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 RiskAssessmentApi.deleteRiskAssessment(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: RiskAssessment[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await RiskAssessmentApi.deleteRiskAssessmentList(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 RiskAssessmentApi.exportRiskAssessment(queryParams)
download.excel(data, '危险评估.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,170 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
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>
<el-form-item label="考核年份" prop="year">
<el-input v-model="formData.year" placeholder="请输入考核年份" />
</el-form-item>
<el-form-item label="考核月份" prop="month">
<el-input v-model="formData.month" placeholder="请输入考核月份" />
</el-form-item>
<el-form-item label="基础分" prop="baseScore">
<el-input v-model="formData.baseScore" placeholder="请输入基础分" />
</el-form-item>
<el-form-item label="加分" prop="rewardScore">
<el-input v-model="formData.rewardScore" placeholder="请输入加分" />
</el-form-item>
<el-form-item label="扣分" prop="penaltyScore">
<el-input v-model="formData.penaltyScore" placeholder="请输入扣分" />
</el-form-item>
<el-form-item label="总分" prop="totalScore">
<el-input v-model="formData.totalScore" placeholder="请输入总分" />
</el-form-item>
<el-form-item label="考核等级" prop="level">
<el-select v-model="formData.level" placeholder="请选择考核等级">
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SCORE_LEVEL)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</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
v-for="dict in getIntDictOptions(DICT_TYPE.PRISON_SCORE_STATUS)"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" 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 { ScoreApi, Score } from '@/api/prison/score'
/** 计分考核 表单 */
defineOptions({ name: 'ScoreForm' })
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false) //
const dialogTitle = ref('') //
const formLoading = ref(false) // 12
const formType = ref('') // create - update -
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
})
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' }]
})
const formRef = ref() // Ref
/** 打开弹窗 */
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 {
formData.value = await ScoreApi.getScore(id)
} 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 as unknown as Score
if (formType.value === 'create') {
await ScoreApi.createScore(data)
message.success(t('common.createSuccess'))
} else {
await ScoreApi.updateScore(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
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
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,265 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="80px"
>
<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="year">
<el-input
v-model="queryParams.year"
placeholder="请输入"
clearable
@keyup.enter="handleQuery"
class="!w-80px"
/>
</el-form-item>
<el-form-item label="考核等级" prop="level">
<el-select
v-model="queryParams.level"
placeholder="请选择"
clearable
class="!w-90px"
>
<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-90px"
>
<el-option
v-for="dict in statusOptions"
: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:score:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['prison:score:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
<el-button
type="danger"
plain
:disabled="checkedIds.length === 0"
@click="handleDeleteBatch"
v-hasPermi="['prison:score:delete']"
>
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleRowCheckboxChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="记录ID" align="center" prop="id" width="80" />
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
<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" />
<el-table-column label="加分" align="center" prop="rewardScore" width="70" />
<el-table-column label="扣分" align="center" prop="penaltyScore" width="70" />
<el-table-column label="总分" align="center" prop="totalScore" width="80" />
<el-table-column label="考核等级" align="center" prop="level" width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_SCORE_LEVEL" :value="scope.row.level" />
</template>
</el-table-column>
<el-table-column label="考核人" align="center" prop="assessorName" width="100" />
<el-table-column label="状态" align="center" prop="status" width="90">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PRISON_SCORE_STATUS" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
{{ formatDate(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="120">
<template #default="scope">
<el-button
type="primary"
link
@click="openForm('update', scope.row.id)"
v-hasPermi="['prison:score:update']"
>
修改
</el-button>
<el-button
type="danger"
link
@click="handleDelete(scope.row.id)"
v-hasPermi="['prison:score: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>
<!-- 表单弹窗添加/修改 -->
<ScoreForm ref="formRef" @success="getList" />
</template>
<script lang="ts" setup>
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import download from '@/utils/download'
import { ScoreApi, Score } from '@/api/prison/score'
import ScoreForm from './ScoreForm.vue'
defineOptions({ name: 'Score' })
const message = useMessage()
const { t } = useI18n()
const loading = ref(true)
const list = ref<Score[]>([])
const total = ref(0)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
prisonerNo: undefined,
year: undefined,
level: undefined,
status: undefined
})
const queryFormRef = ref()
const exportLoading = ref(false)
// 使
const levelOptions = getIntDictOptions(DICT_TYPE.PRISON_SCORE_LEVEL)
const statusOptions = getIntDictOptions(DICT_TYPE.PRISON_SCORE_STATUS)
/** 日期格式化 */
const formatDate = (date: string | Date | undefined) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ScoreApi.getScorePage(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 ScoreApi.deleteScore(id)
message.success(t('common.delSuccess'))
await getList()
} catch {}
}
/** 批量删除按钮操作 */
const checkedIds = ref<number[]>([])
const handleRowCheckboxChange = (rows: Score[]) => {
checkedIds.value = rows.map((row) => row.id!)
}
const handleDeleteBatch = async () => {
try {
await message.delConfirm()
await ScoreApi.deleteScoreList(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 ScoreApi.exportScore(queryParams)
download.excel(data, '计分考核.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getList()
})
</script>