Compare commits
24 Commits
lm/feat/ou
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 273ff796a5 | |||
| 190093c47c | |||
| 3198ad6131 | |||
| e8aefff16b | |||
| e7dc7d2b04 | |||
| 535b7be802 | |||
| 6c57427679 | |||
| fc595c2c26 | |||
| 59f2fbbfae | |||
| 2cee84c00d | |||
| 11e3ef148b | |||
| ec0ff8dc90 | |||
| 08917db45b | |||
| 5b41d2b23d | |||
| 44fa196bd7 | |||
| 13d86f4707 | |||
| 0ba25e3492 | |||
| ae0493428e | |||
| 4e65bb4300 | |||
| 3793d64d3c | |||
| a65d4e9280 | |||
| 9a3044147b | |||
| 4bff154317 | |||
| 77b78ac64d |
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ pnpm-debug
|
|||||||
auto-*.d.ts
|
auto-*.d.ts
|
||||||
.idea
|
.idea
|
||||||
.history
|
.history
|
||||||
|
.omc/
|
||||||
|
|||||||
@ -34,6 +34,8 @@ const include = [
|
|||||||
'markmap-toolbar',
|
'markmap-toolbar',
|
||||||
'highlight.js',
|
'highlight.js',
|
||||||
'element-plus',
|
'element-plus',
|
||||||
|
'html-docx-js-typescript',
|
||||||
|
'file-saver',
|
||||||
'element-plus/es',
|
'element-plus/es',
|
||||||
'element-plus/es/locale/lang/zh-cn',
|
'element-plus/es/locale/lang/zh-cn',
|
||||||
'element-plus/es/locale/lang/en',
|
'element-plus/es/locale/lang/en',
|
||||||
|
|||||||
@ -50,10 +50,8 @@
|
|||||||
"echarts-wordcloud": "^2.1.0",
|
"echarts-wordcloud": "^2.1.0",
|
||||||
"element-plus": "2.11.1",
|
"element-plus": "2.11.1",
|
||||||
"fast-xml-parser": "^4.3.2",
|
"fast-xml-parser": "^4.3.2",
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"html-docx-js": "^0.3.1",
|
"html-docx-js": "^0.3.1",
|
||||||
"html-docx-js-typescript": "^0.1.5",
|
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"jsoneditor": "^10.1.3",
|
"jsoneditor": "^10.1.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
@ -115,6 +113,8 @@
|
|||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-vue": "^9.22.0",
|
"eslint-plugin-vue": "^9.22.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"html-docx-js-typescript": "^0.1.5",
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"postcss-html": "^1.6.0",
|
"postcss-html": "^1.6.0",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -83,18 +83,12 @@ importers:
|
|||||||
fast-xml-parser:
|
fast-xml-parser:
|
||||||
specifier: ^4.3.2
|
specifier: ^4.3.2
|
||||||
version: 4.5.0
|
version: 4.5.0
|
||||||
file-saver:
|
|
||||||
specifier: ^2.0.5
|
|
||||||
version: 2.0.5
|
|
||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.9.0
|
specifier: ^11.9.0
|
||||||
version: 11.10.0
|
version: 11.10.0
|
||||||
html-docx-js:
|
html-docx-js:
|
||||||
specifier: ^0.3.1
|
specifier: ^0.3.1
|
||||||
version: 0.3.1
|
version: 0.3.1
|
||||||
html-docx-js-typescript:
|
|
||||||
specifier: ^0.1.5
|
|
||||||
version: 0.1.5
|
|
||||||
jsencrypt:
|
jsencrypt:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@ -273,6 +267,12 @@ importers:
|
|||||||
eslint-plugin-vue:
|
eslint-plugin-vue:
|
||||||
specifier: ^9.22.0
|
specifier: ^9.22.0
|
||||||
version: 9.31.0(eslint@8.57.1)
|
version: 9.31.0(eslint@8.57.1)
|
||||||
|
file-saver:
|
||||||
|
specifier: ^2.0.5
|
||||||
|
version: 2.0.5
|
||||||
|
html-docx-js-typescript:
|
||||||
|
specifier: ^0.1.5
|
||||||
|
version: 0.1.5
|
||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: ^15.2.2
|
specifier: ^15.2.2
|
||||||
version: 15.2.10
|
version: 15.2.10
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export const AreaApi = {
|
|||||||
|
|
||||||
/** 批量删除监区信息 */
|
/** 批量删除监区信息 */
|
||||||
deleteAreaList: async (ids: number[]) => {
|
deleteAreaList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/area/delete-list?ids=${ids.join(',')}` })
|
return await request.post({ url: `/prison/area/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出监区信息 Excel
|
// 导出监区信息 Excel
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export const CellApi = {
|
|||||||
|
|
||||||
// 批量删除监室信息
|
// 批量删除监室信息
|
||||||
deleteCellList: async (ids: number[]) => {
|
deleteCellList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/cell/delete-list', params: { ids: ids.join(',') } })
|
return await request.post({ url: '/prison/cell/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出监室信息 Excel
|
// 导出监室信息 Excel
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export const ConsumptionApi = {
|
|||||||
|
|
||||||
/** 批量删除消费订单 */
|
/** 批量删除消费订单 */
|
||||||
deleteConsumptionList: async (ids: number[]) => {
|
deleteConsumptionList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/consumption/delete-list?ids=${ids.join(',')}` })
|
return await request.post({ url: `/prison/consumption/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 查询消费明细列表
|
// 查询消费明细列表
|
||||||
|
|||||||
@ -231,6 +231,13 @@ export interface PrisonerDashboardStatsRespVO {
|
|||||||
riskScore: number // 风险评估分
|
riskScore: number // 风险评估分
|
||||||
riskLevel: number // 风险等级
|
riskLevel: number // 风险等级
|
||||||
|
|
||||||
|
// 累计数据
|
||||||
|
violationCount: number // 累计违规次数
|
||||||
|
praiseDays: string // 累计表扬天数
|
||||||
|
praiseCount: number // 累计表扬次数
|
||||||
|
penaltyCount: number // 累计扣分次数
|
||||||
|
rewardCount: number // 累计加分次数
|
||||||
|
|
||||||
// 中心数据
|
// 中心数据
|
||||||
centerLeftData: CenterLeftData
|
centerLeftData: CenterLeftData
|
||||||
centerRightData: CenterRightData
|
centerRightData: CenterRightData
|
||||||
|
|||||||
@ -182,7 +182,7 @@ export const TemplateApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteTemplateList: async (ids: number[]) => {
|
deleteTemplateList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/template/delete-list', params: { ids: ids.join(',') } })
|
return await request.post({ url: '/prison/evaluation-report/template/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
getEnabledTemplateList: async () => {
|
getEnabledTemplateList: async () => {
|
||||||
@ -220,7 +220,7 @@ export const DimensionApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteDimensionList: async (ids: number[]) => {
|
deleteDimensionList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/dimension/delete-list', params: { ids: ids.join(',') } })
|
return await request.post({ url: '/prison/evaluation-report/dimension/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
getDimensionsByTemplateId: async (templateId: number) => {
|
getDimensionsByTemplateId: async (templateId: number) => {
|
||||||
@ -262,7 +262,7 @@ export const ReportApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteReportList: async (ids: number[]) => {
|
deleteReportList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/report/delete-list', params: { ids: ids.join(',') } })
|
return await request.post({ url: '/prison/evaluation-report/report/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
submitReport: async (id: number) => {
|
submitReport: async (id: number) => {
|
||||||
@ -383,7 +383,7 @@ export const CommentApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteCommentList: async (ids: number[]) => {
|
deleteCommentList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/comment/delete-list', params: { ids: ids.join(',') } })
|
return await request.post({ url: '/prison/evaluation-report/comment/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
getCommentsByTypeAndLevel: async (commentType: number, level?: number) => {
|
getCommentsByTypeAndLevel: async (commentType: number, level?: number) => {
|
||||||
|
|||||||
@ -162,7 +162,7 @@ export const EvaluationTemplateApi = {
|
|||||||
|
|
||||||
// 批量删除模板
|
// 批量删除模板
|
||||||
deleteTemplateList: async (ids: number[]) => {
|
deleteTemplateList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/template/delete-list?ids=' + ids.join(',') })
|
return await request.post({ url: '/prison/evaluation-report/template/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取启用的模板列表
|
// 获取启用的模板列表
|
||||||
@ -216,7 +216,7 @@ export const EvaluationDimensionApi = {
|
|||||||
|
|
||||||
// 批量删除维度
|
// 批量删除维度
|
||||||
deleteDimensionList: async (ids: number[]) => {
|
deleteDimensionList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/dimension/delete-list?ids=' + ids.join(',') })
|
return await request.post({ url: '/prison/evaluation-report/dimension/delete-list', data: ids })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ export const EvaluationReportApi = {
|
|||||||
|
|
||||||
// 批量删除报告
|
// 批量删除报告
|
||||||
deleteReportList: async (ids: number[]) => {
|
deleteReportList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/report/delete-list?ids=' + ids.join(',') })
|
return await request.post({ url: '/prison/evaluation-report/report/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 提交审核
|
// 提交审核
|
||||||
@ -324,7 +324,7 @@ export const ReportCommentApi = {
|
|||||||
|
|
||||||
// 批量删除评语
|
// 批量删除评语
|
||||||
deleteCommentList: async (ids: number[]) => {
|
deleteCommentList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/evaluation-report/comment/delete-list?ids=' + ids.join(',') })
|
return await request.post({ url: '/prison/evaluation-report/comment/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取评语详情
|
// 获取评语详情
|
||||||
|
|||||||
@ -89,10 +89,18 @@ export const PrisonerApi = {
|
|||||||
getPage: (params: PageParam) => {
|
getPage: (params: PageParam) => {
|
||||||
return request.get({ url: '/prison/prisoner/page', params })
|
return request.get({ url: '/prison/prisoner/page', params })
|
||||||
},
|
},
|
||||||
|
// 分页查询(别名,用于选择犯人弹窗)
|
||||||
|
getPrisonerPage: (params: PageParam) => {
|
||||||
|
return request.get({ url: '/prison/prisoner/page', params })
|
||||||
|
},
|
||||||
// 获取详情
|
// 获取详情
|
||||||
get: (id: number) => {
|
get: (id: number) => {
|
||||||
return request.get({ url: '/prison/prisoner/get', params: { id } })
|
return request.get({ url: '/prison/prisoner/get', params: { id } })
|
||||||
},
|
},
|
||||||
|
// 获取详情(别名,与其他模块保持一致)
|
||||||
|
getPrisoner: (id: number) => {
|
||||||
|
return request.get({ url: '/prison/prisoner/get', params: { id } })
|
||||||
|
},
|
||||||
// 创建
|
// 创建
|
||||||
create: (data: PrisonerCreateVO) => {
|
create: (data: PrisonerCreateVO) => {
|
||||||
return request.post({ url: '/prison/prisoner/create', data })
|
return request.post({ url: '/prison/prisoner/create', data })
|
||||||
@ -107,7 +115,7 @@ export const PrisonerApi = {
|
|||||||
},
|
},
|
||||||
// 批量删除
|
// 批量删除
|
||||||
deleteList: (ids: number[]) => {
|
deleteList: (ids: number[]) => {
|
||||||
return request.delete({ url: '/prison/prisoner/delete-list', params: { ids: ids.join(',') } })
|
return request.post({ url: '/prison/prisoner/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
// 导出
|
// 导出
|
||||||
export: (params: PageParam) => {
|
export: (params: PageParam) => {
|
||||||
|
|||||||
@ -73,7 +73,7 @@ export const QuestionApi = {
|
|||||||
|
|
||||||
/** 批量删除问卷问题 */
|
/** 批量删除问卷问题 */
|
||||||
deleteQuestionList: async (ids: number[]) => {
|
deleteQuestionList: async (ids: number[]) => {
|
||||||
return await request.delete<boolean>({ url: `/prison/question/delete-list`, params: { ids: ids.join(',') } })
|
return await request.post<boolean>({ url: `/prison/question/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 批量更新问卷问题(使用POST方法,与后端一致)
|
// 批量更新问卷问题(使用POST方法,与后端一致)
|
||||||
|
|||||||
@ -108,6 +108,8 @@ export interface TaskAreaStatistics {
|
|||||||
|
|
||||||
/** 人员填写进度 */
|
/** 人员填写进度 */
|
||||||
export interface PrisonerProgress {
|
export interface PrisonerProgress {
|
||||||
|
questionnaireId: number
|
||||||
|
questionnaireName?: string
|
||||||
id: number
|
id: number
|
||||||
prisonerId: number
|
prisonerId: number
|
||||||
prisonerNo: string
|
prisonerNo: string
|
||||||
@ -162,7 +164,7 @@ export const QuestionnaireTaskApi = {
|
|||||||
|
|
||||||
// 批量删除问卷任务
|
// 批量删除问卷任务
|
||||||
deleteQuestionnaireTaskList: async (ids: number[]) => {
|
deleteQuestionnaireTaskList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/questionnaire-task/delete-list`, params: { ids: ids.join(',') } })
|
return await request.post({ url: `/prison/questionnaire-task/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出问卷任务 Excel
|
// 导出问卷任务 Excel
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export const QuestionnaireApi = {
|
|||||||
|
|
||||||
/** 批量删除问卷模板 */
|
/** 批量删除问卷模板 */
|
||||||
deleteQuestionnaireList: async (ids: number[]) => {
|
deleteQuestionnaireList: async (ids: number[]) => {
|
||||||
return await request.delete<boolean>({ url: `/prison/questionnaire/delete-list`, params: { ids: ids.join(',') } })
|
return await request.post<boolean>({ url: `/prison/questionnaire/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出问卷模板 Excel
|
// 导出问卷模板 Excel
|
||||||
|
|||||||
@ -92,7 +92,7 @@ export const QuickCommentApi = {
|
|||||||
|
|
||||||
// 批量删除评语
|
// 批量删除评语
|
||||||
deleteList: async (ids: number[]) => {
|
deleteList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: '/prison/quick-comment/delete-list?ids=' + ids.join(',') })
|
return await request.post({ url: '/prison/quick-comment/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导入评语
|
// 导入评语
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export const ReleaseApi = {
|
|||||||
return request.delete({ url: `/prison/release/delete?id=${id}` })
|
return request.delete({ url: `/prison/release/delete?id=${id}` })
|
||||||
},
|
},
|
||||||
deleteReleaseList: (ids: number[]) => {
|
deleteReleaseList: (ids: number[]) => {
|
||||||
return request.delete({ url: `/prison/release/delete-list?ids=${ids.join(',')}` })
|
return request.post({ url: '/prison/release/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
doRelease: (id: number) => {
|
doRelease: (id: number) => {
|
||||||
return request.post({ url: `/prison/release/do-release?id=${id}` })
|
return request.post({ url: `/prison/release/do-release?id=${id}` })
|
||||||
|
|||||||
@ -113,7 +113,7 @@ export const RiskApi = {
|
|||||||
|
|
||||||
/** 批量删除风险评估 */
|
/** 批量删除风险评估 */
|
||||||
deleteRiskList: async (ids: number[]) => {
|
deleteRiskList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/risk/delete-list?ids=${ids.join(',')}` })
|
return await request.post({ url: `/prison/risk/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出风险评估 Excel
|
// 导出风险评估 Excel
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export interface RiskAssessment {
|
|||||||
id: number // 评估ID
|
id: number // 评估ID
|
||||||
prisonerId?: number // 罪犯ID
|
prisonerId?: number // 罪犯ID
|
||||||
prisonerNo?: string // 罪犯编号
|
prisonerNo?: string // 罪犯编号
|
||||||
prisonerName?: string // 罪犯姓名
|
prisonerName?: string // 罪犯姓名(用于回显)
|
||||||
assessmentType?: number // 评估类型:1-入狱评估 2-定期评估 3-专项评估
|
assessmentType?: number // 评估类型:1-入狱评估 2-定期评估 3-专项评估
|
||||||
assessmentDate?: string // 评估日期
|
assessmentDate?: string // 评估日期
|
||||||
violenceScore: number // 暴力倾向得分
|
violenceScore: number // 暴力倾向得分
|
||||||
@ -31,7 +31,7 @@ export interface RiskAssessment {
|
|||||||
riskFactors: string // 风险因素
|
riskFactors: string // 风险因素
|
||||||
suggestions: string // 管控建议
|
suggestions: string // 管控建议
|
||||||
// assessorId 和 assessorName 由后端自动从登录上下文获取,不需要前端传递
|
// assessorId 和 assessorName 由后端自动从登录上下文获取,不需要前端传递
|
||||||
nextAssessmentDate: string // 下次评估日期
|
nextAssessmentDate?: string // 下次评估日期
|
||||||
status?: number // 状态:1-待审核 2-已通过
|
status?: number // 状态:1-待审核 2-已通过
|
||||||
remark: string // 备注
|
remark: string // 备注
|
||||||
createTime?: string // 创建时间
|
createTime?: string // 创建时间
|
||||||
@ -66,7 +66,7 @@ export const RiskAssessmentApi = {
|
|||||||
|
|
||||||
/** 批量删除危险评估 */
|
/** 批量删除危险评估 */
|
||||||
deleteRiskAssessmentList: async (ids: number[]) => {
|
deleteRiskAssessmentList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/risk-assessment/delete-list?ids=${ids.join(',')}` })
|
return await request.post({ url: `/prison/risk-assessment/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出危险评估 Excel
|
// 导出危险评估 Excel
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export const ScoreDetailApi = {
|
|||||||
return request.delete({ url: `/prison/score-detail/delete?id=${id}` })
|
return request.delete({ url: `/prison/score-detail/delete?id=${id}` })
|
||||||
},
|
},
|
||||||
deleteList: (ids: number[]) => {
|
deleteList: (ids: number[]) => {
|
||||||
return request.delete({ url: `/prison/score-detail/delete-list?ids=${ids.join(',')}` })
|
return request.post({ url: '/prison/score-detail/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
export: (params: ScoreDetailPageReqVO) => {
|
export: (params: ScoreDetailPageReqVO) => {
|
||||||
return request.download({ url: '/prison/score-detail/export-excel', params })
|
return request.download({ url: '/prison/score-detail/export-excel', params })
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export const ScoreRuleApi = {
|
|||||||
return request.delete({ url: `/prison/score-rule/delete?id=${id}` })
|
return request.delete({ url: `/prison/score-rule/delete?id=${id}` })
|
||||||
},
|
},
|
||||||
deleteRuleList: (ids: number[]) => {
|
deleteRuleList: (ids: number[]) => {
|
||||||
return request.delete({ url: `/prison/score-rule/delete-list?ids=${ids.join(',')}` })
|
return request.post({ url: '/prison/score-rule/delete-list', data: ids })
|
||||||
},
|
},
|
||||||
getRuleByCategory: (category: number) => {
|
getRuleByCategory: (category: number) => {
|
||||||
return request.get({ url: `/prison/score-rule/list-by-category?category=${category}` })
|
return request.get({ url: `/prison/score-rule/list-by-category?category=${category}` })
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export const ScoreApi = {
|
|||||||
|
|
||||||
/** 批量删除计分考核 */
|
/** 批量删除计分考核 */
|
||||||
deleteScoreList: async (ids: number[]) => {
|
deleteScoreList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/score/delete-list?ids=${ids.join(',')}` })
|
return await request.post({ url: `/prison/score/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出计分考核 Excel
|
// 导出计分考核 Excel
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export const SituationApi = {
|
|||||||
|
|
||||||
/** 批量删除狱情收集 */
|
/** 批量删除狱情收集 */
|
||||||
deleteSituationList: async (ids: number[]) => {
|
deleteSituationList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/situation/delete-list?ids=${ids.join(',')}` })
|
return await request.post({ url: `/prison/situation/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出狱情收集 Excel
|
// 导出狱情收集 Excel
|
||||||
|
|||||||
@ -161,7 +161,7 @@ export const WarningApi = {
|
|||||||
|
|
||||||
/** 批量删除预警 */
|
/** 批量删除预警 */
|
||||||
deleteWarningList: async (ids: number[]) => {
|
deleteWarningList: async (ids: number[]) => {
|
||||||
return await request.delete({ url: `/prison/warning/delete-list?ids=${ids.join(',')}` })
|
return await request.post({ url: `/prison/warning/delete-list`, data: ids })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 导出预警 Excel
|
// 导出预警 Excel
|
||||||
|
|||||||
@ -70,10 +70,9 @@ service.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 监狱系统:即使 tenantEnable 为 false,也尝试获取并设置租户 ID
|
// 监狱系统:即使 tenantEnable 为 false,也尝试获取并设置租户 ID
|
||||||
const tenantId = getTenantId()
|
// 如果缓存中没有租户 ID,使用默认值 1
|
||||||
if (tenantId) {
|
const tenantId = getTenantId() || 1
|
||||||
config.headers['tenant-id'] = tenantId
|
config.headers['tenant-id'] = tenantId
|
||||||
}
|
|
||||||
const method = config.method?.toUpperCase()
|
const method = config.method?.toUpperCase()
|
||||||
// 防止 GET 请求缓存
|
// 防止 GET 请求缓存
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
|
|||||||
@ -55,7 +55,15 @@ const whiteList = [
|
|||||||
'/register',
|
'/register',
|
||||||
'/oauthLogin/gitee',
|
'/oauthLogin/gitee',
|
||||||
'/prisoner/prisoner/dashboard', // Dashboard 页面
|
'/prisoner/prisoner/dashboard', // Dashboard 页面
|
||||||
'/ai-dash-entry' // DashEntry 页面
|
'/ai-dash-entry', // DashEntry 页面
|
||||||
|
// 监狱模块路由(解决SPA路由重定向问题)
|
||||||
|
'/prison/template', // 问卷模版管理
|
||||||
|
'/prison/questionnaire', // 问卷任务管理
|
||||||
|
'/prison/guard', // 狱警管理
|
||||||
|
// AI指导建议路由(evaluation-mgmt)
|
||||||
|
'/prison/evaluation-mgmt/template', // 评估模板管理
|
||||||
|
'/prison/evaluation-mgmt/dimension', // 评估维度管理
|
||||||
|
'/prison/evaluation-mgmt/report', // 评估报告管理
|
||||||
]
|
]
|
||||||
|
|
||||||
// 路由加载前
|
// 路由加载前
|
||||||
|
|||||||
@ -10,20 +10,19 @@
|
|||||||
<div class="dashboard-content-top-center">
|
<div class="dashboard-content-top-center">
|
||||||
<div class="gauge-container">
|
<div class="gauge-container">
|
||||||
<div class="dashboard-content-top-center-data">
|
<div class="dashboard-content-top-center-data">
|
||||||
<!-- 左侧第一个卡片 -->
|
<!-- 左侧区域:上1下2排列 -->
|
||||||
<div class="info-card-item">
|
<div class="info-field-item top-field">
|
||||||
<div class="card-number">{{ centerLeftData.top.value }}</div>
|
<div class="field-label">累计服刑天数</div>
|
||||||
<div class="card-label">{{ centerLeftData.top.label }}</div>
|
<div class="field-value">{{ servedDays }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 左侧第二个卡片 -->
|
|
||||||
<div class="card-row">
|
<div class="card-row">
|
||||||
<div class="info-card-item center-right-card-item">
|
<div class="info-field-item">
|
||||||
<div class="card-number">{{ centerLeftData.middle.left.value }}</div>
|
<div class="field-label">累计违规次数</div>
|
||||||
<div class="card-label">{{ centerLeftData.middle.left.label }}</div>
|
<div class="field-value">{{ violationCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card-item center-right-card-item">
|
<div class="info-field-item">
|
||||||
<div class="card-number">{{ centerLeftData.middle.right.value }}</div>
|
<div class="field-label">累计表扬次数</div>
|
||||||
<div class="card-label">{{ centerLeftData.middle.right.label }}</div>
|
<div class="field-value">{{ praiseCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -31,20 +30,19 @@
|
|||||||
<GaugeChart :height="'240px'" :value="gaugeValue" :name="gaugeName" />
|
<GaugeChart :height="'240px'" :value="gaugeValue" :name="gaugeName" />
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-content-top-center-data">
|
<div class="dashboard-content-top-center-data">
|
||||||
<!-- 右侧第一个卡片 -->
|
<!-- 右侧区域:上1下2排列 -->
|
||||||
<div class="info-card-item">
|
<div class="info-field-item top-field">
|
||||||
<div class="card-number">{{ centerRightData.top.value }}</div>
|
<div class="field-label">剩余刑期天数</div>
|
||||||
<div class="card-label">{{ centerRightData.top.label }}</div>
|
<div class="field-value">{{ remainingDays }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧第二个卡片 -->
|
|
||||||
<div class="card-row">
|
<div class="card-row">
|
||||||
<div class="info-card-item center-right-card-item">
|
<div class="info-field-item">
|
||||||
<div class="card-number">{{ centerRightData.middle.left.value }}</div>
|
<div class="field-label">累计扣分次数</div>
|
||||||
<div class="card-label">{{ centerRightData.middle.left.label }}</div>
|
<div class="field-value">{{ penaltyCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card-item center-right-card-item">
|
<div class="info-field-item">
|
||||||
<div class="card-number">{{ centerRightData.middle.right.value }}</div>
|
<div class="field-label">累计加分次数</div>
|
||||||
<div class="card-label">{{ centerRightData.middle.right.label }}</div>
|
<div class="field-value">{{ rewardCount }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="dashboard-content-bottom-right">
|
<div class="dashboard-content-bottom-right">
|
||||||
<div class="dashboard-content-bottom-right-title">大帐统计</div>
|
<div class="dashboard-content-bottom-right-title">大帐统计</div>
|
||||||
<BarChart :height="'200px'" :data="barChartData" :card-data="barCardData" />
|
<BarChart :data="barChartData" :balance="balance" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -171,12 +169,8 @@ const centerRightData = ref({
|
|||||||
// 柱状图数据
|
// 柱状图数据
|
||||||
const barChartData = ref<{ category: string; monthlyStandard: number; perCapita: number }[]>([])
|
const barChartData = ref<{ category: string; monthlyStandard: number; perCapita: number }[]>([])
|
||||||
|
|
||||||
// 卡片数据
|
// 账户余额
|
||||||
const barCardData = ref({
|
const balance = ref(0)
|
||||||
inProgress: 0,
|
|
||||||
toWarehouse: 0,
|
|
||||||
outWarehouse: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 基本信息数据
|
// 基本信息数据
|
||||||
const basicInfo = ref({
|
const basicInfo = ref({
|
||||||
@ -246,6 +240,16 @@ const rewardsPunishments = ref<{
|
|||||||
const currentTime = ref('')
|
const currentTime = ref('')
|
||||||
const prisonerName = ref('加载中...')
|
const prisonerName = ref('加载中...')
|
||||||
|
|
||||||
|
// 左侧区域三个字段数据
|
||||||
|
const servedDays = ref(0)
|
||||||
|
const violationCount = ref(0)
|
||||||
|
const praiseCount = ref(0)
|
||||||
|
|
||||||
|
// 右侧区域三个字段数据
|
||||||
|
const remainingDays = ref(0)
|
||||||
|
const penaltyCount = ref(0)
|
||||||
|
const rewardCount = ref(0)
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = () => {
|
const formatTime = () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@ -279,13 +283,14 @@ const loadData = async (prisonerId: number) => {
|
|||||||
gaugeName.value = ''
|
gaugeName.value = ''
|
||||||
|
|
||||||
// 计算累计服刑天数和剩余刑期天数
|
// 计算累计服刑天数和剩余刑期天数
|
||||||
const servedDays = res.servedDays || 0;
|
const servedDaysValue = res.servedDays || 0;
|
||||||
let remainingDays = 0;
|
servedDays.value = servedDaysValue
|
||||||
|
remainingDays.value = 0;
|
||||||
if (res.imprisonmentDate && res.releaseDate) {
|
if (res.imprisonmentDate && res.releaseDate) {
|
||||||
const startDate = new Date(res.imprisonmentDate);
|
const startDate = new Date(res.imprisonmentDate);
|
||||||
const endDate = new Date(res.releaseDate);
|
const endDate = new Date(res.releaseDate);
|
||||||
const totalDays = Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
const totalDays = Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
remainingDays = Math.max(0, totalDays - servedDays);
|
remainingDays.value = Math.max(0, totalDays - servedDaysValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取计分考核数据 - 累计扣分次数和累计加分次数
|
// 获取计分考核数据 - 累计扣分次数和累计加分次数
|
||||||
@ -305,6 +310,10 @@ const loadData = async (prisonerId: number) => {
|
|||||||
console.error('获取计分考核数据失败:', error)
|
console.error('获取计分考核数据失败:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新右侧区域三个字段数据
|
||||||
|
penaltyCount.value = totalPenaltyCount
|
||||||
|
rewardCount.value = totalRewardCount
|
||||||
|
|
||||||
// 获取狱情收集数据 - 累计违规次数
|
// 获取狱情收集数据 - 累计违规次数
|
||||||
let totalViolationCount = 0
|
let totalViolationCount = 0
|
||||||
try {
|
try {
|
||||||
@ -319,6 +328,10 @@ const loadData = async (prisonerId: number) => {
|
|||||||
console.error('获取狱情收集数据失败:', error)
|
console.error('获取狱情收集数据失败:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新左侧区域三个字段数据
|
||||||
|
violationCount.value = totalViolationCount
|
||||||
|
praiseCount.value = res.praiseCount || 0
|
||||||
|
|
||||||
// 更新中心左侧数据
|
// 更新中心左侧数据
|
||||||
if (res.centerLeftData) {
|
if (res.centerLeftData) {
|
||||||
centerLeftData.value = {
|
centerLeftData.value = {
|
||||||
@ -374,14 +387,8 @@ const loadData = async (prisonerId: number) => {
|
|||||||
// 更新柱状图数据
|
// 更新柱状图数据
|
||||||
barChartData.value = res.consumptionMonthlyData || []
|
barChartData.value = res.consumptionMonthlyData || []
|
||||||
|
|
||||||
// 更新消费汇总
|
// 更新账户余额
|
||||||
if (res.consumptionSummary) {
|
balance.value = res.balance || 0
|
||||||
barCardData.value = {
|
|
||||||
inProgress: res.consumptionSummary.inProgress || 0,
|
|
||||||
toWarehouse: res.consumptionSummary.toWarehouse || 0,
|
|
||||||
outWarehouse: res.consumptionSummary.outWarehouse || 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新基本信息
|
// 更新基本信息
|
||||||
basicInfo.value = {
|
basicInfo.value = {
|
||||||
@ -389,7 +396,7 @@ const loadData = async (prisonerId: number) => {
|
|||||||
prisonNumber: res.prisonerNo || '',
|
prisonNumber: res.prisonerNo || '',
|
||||||
sentenceStart: res.imprisonmentDate || '',
|
sentenceStart: res.imprisonmentDate || '',
|
||||||
sentenceEnd: res.releaseDate || '',
|
sentenceEnd: res.releaseDate || '',
|
||||||
sentenceDays: res.servedDays || 0,
|
sentenceDays: res.sentenceDays || 0,
|
||||||
age: res.age || 0,
|
age: res.age || 0,
|
||||||
hometown: res.nativePlace || '',
|
hometown: res.nativePlace || '',
|
||||||
education: res.education || '',
|
education: res.education || '',
|
||||||
@ -550,7 +557,7 @@ onUnmounted(() => {
|
|||||||
.list-card-item {
|
.list-card-item {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
height: 90%;
|
height: 90%;
|
||||||
background: #2d3d5f;
|
background: rgba(45, 65, 131, 0.6);
|
||||||
border: 1px solid rgba(56, 102, 141, 0.5);
|
border: 1px solid rgba(56, 102, 141, 0.5);
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
@ -593,6 +600,55 @@ onUnmounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 纵向字段项样式
|
||||||
|
.info-field-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: rgba(45, 65, 131, 0.6);
|
||||||
|
border: 1px solid rgba(56, 102, 141, 0.5);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: inset 0 0 10px 0 rgba(43, 65, 131, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(56, 102, 141, 0.8);
|
||||||
|
box-shadow: inset 0 0 15px 0 rgba(43, 65, 131, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 顶部字段样式(用于上1下2布局中的顶部字段)
|
||||||
|
.top-field {
|
||||||
|
height: 50%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字段标签样式
|
||||||
|
.field-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字段数值样式
|
||||||
|
.field-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-content-top-center-center {
|
.dashboard-content-top-center-center {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
height: 220px;
|
height: 220px;
|
||||||
|
|||||||
@ -1,22 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="supply-chart-container">
|
<div class="supply-chart-container" ref="containerRef">
|
||||||
<!-- 卡片统计 -->
|
|
||||||
<div class="chart-cards">
|
|
||||||
<div class="chart-card-item">
|
|
||||||
<div class="card-value">{{ cardData.inProgress }}</div>
|
|
||||||
<div class="card-label">进行中</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-card-item">
|
|
||||||
<div class="card-value">{{ cardData.toWarehouse }}</div>
|
|
||||||
<div class="card-label">待入库</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-card-item">
|
|
||||||
<div class="card-value">{{ cardData.outWarehouse }}</div>
|
|
||||||
<div class="card-label">已出库</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 柱状图 -->
|
<!-- 柱状图 -->
|
||||||
<EChart :options="barOption" :height="height" />
|
<EChart ref="chartRef" :options="barOption" :height="height" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -24,7 +9,7 @@
|
|||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import EChart from '@/components/Echart/src/Echart.vue'
|
import EChart from '@/components/Echart/src/Echart.vue'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, ref, onMounted, watch } from 'vue'
|
||||||
|
|
||||||
defineOptions({ name: 'BarChart' })
|
defineOptions({ name: 'BarChart' })
|
||||||
|
|
||||||
@ -34,49 +19,45 @@ interface ChartDataItem {
|
|||||||
perCapita: number
|
perCapita: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CardData {
|
|
||||||
inProgress: number
|
|
||||||
toWarehouse: number
|
|
||||||
outWarehouse: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
width?: number
|
width?: number | string
|
||||||
height?: string
|
height?: string
|
||||||
data?: ChartDataItem[]
|
data?: ChartDataItem[]
|
||||||
cardData?: CardData
|
balance?: number
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
width: 400,
|
width: '100%',
|
||||||
height: '300px',
|
|
||||||
data: () => [],
|
data: () => [],
|
||||||
cardData: () => ({
|
balance: () => 0
|
||||||
inProgress: 5,
|
|
||||||
toWarehouse: 5,
|
|
||||||
outWarehouse: 5
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLElement>()
|
||||||
|
const chartRef = ref()
|
||||||
|
|
||||||
// 创建图表配置
|
// 创建图表配置
|
||||||
const createChartOption = (): EChartsOption => {
|
const createChartOption = (): EChartsOption => {
|
||||||
const categories = props.data.map((item) => item.category)
|
const categories = props.data.map((item) => item.category)
|
||||||
const monthlyStandardData = props.data.map((item) => item.monthlyStandard ?? 0)
|
const monthlyStandardData = props.data.map((item) => item.monthlyStandard ?? 0)
|
||||||
const perCapitaData = props.data.map((item) => item.perCapita ?? 0)
|
const perCapitaData = props.data.map((item) => item.perCapita ?? 0)
|
||||||
|
|
||||||
// 创建底色数据(最大值50)
|
// 动态计算最大值,确保能够显示所有数据
|
||||||
const maxValue = 50
|
const maxDataValue = Math.max(...monthlyStandardData, ...perCapitaData, 100)
|
||||||
const monthlyStandardBgData = categories.map((_, index) => maxValue - monthlyStandardData[index])
|
// 向上取整到百位,并留出 20% 空间
|
||||||
const perCapitaBgData = categories.map((_, index) => maxValue - perCapitaData[index])
|
const maxValue = Math.ceil(maxDataValue * 1.2 / 100) * 100
|
||||||
|
|
||||||
|
// 创建底色数据(填充到 maxValue)
|
||||||
|
const monthlyStandardBgData = categories.map((_, index) => Math.max(0, maxValue - monthlyStandardData[index]))
|
||||||
|
const perCapitaBgData = categories.map((_, index) => Math.max(0, maxValue - perCapitaData[index]))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
grid: {
|
grid: {
|
||||||
left: '10%',
|
left: '8%',
|
||||||
right: '15%',
|
right: '8%',
|
||||||
top: '20%',
|
top: '25%',
|
||||||
bottom: '15%',
|
bottom: '18%',
|
||||||
containLabel: false
|
containLabel: false
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@ -100,8 +81,8 @@ const createChartOption = (): EChartsOption => {
|
|||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 50,
|
max: maxValue,
|
||||||
interval: 10,
|
interval: Math.ceil(maxValue / 5 / 100) * 100,
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
@ -110,7 +91,8 @@ const createChartOption = (): EChartsOption => {
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#D8F0FF',
|
color: '#D8F0FF',
|
||||||
fontSize: 10
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => value.toString()
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
@ -119,17 +101,33 @@ const createChartOption = (): EChartsOption => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 图例和余额放在同一行
|
||||||
legend: {
|
legend: {
|
||||||
data: ['支出', '收入'],
|
data: [
|
||||||
top: '5%',
|
{ name: '支出', icon: 'rect' },
|
||||||
right: '10%',
|
{ name: '收入', icon: 'rect' }
|
||||||
|
],
|
||||||
|
top: '3%',
|
||||||
|
left: '8%',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#6D869A',
|
color: '#6D869A',
|
||||||
fontSize: 9
|
fontSize: 10
|
||||||
},
|
},
|
||||||
itemWidth: 9,
|
itemWidth: 12,
|
||||||
itemHeight: 9,
|
itemHeight: 8,
|
||||||
itemGap: 25
|
itemGap: 15
|
||||||
|
},
|
||||||
|
// 使用 title 显示余额,放在图例右侧
|
||||||
|
title: {
|
||||||
|
text: `账户余额: ${props.balance}元`,
|
||||||
|
left: 'auto',
|
||||||
|
right: '8%',
|
||||||
|
top: '3%',
|
||||||
|
textStyle: {
|
||||||
|
color: '#00d4ff',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@ -144,7 +142,6 @@ const createChartOption = (): EChartsOption => {
|
|||||||
},
|
},
|
||||||
formatter: function (params: any) {
|
formatter: function (params: any) {
|
||||||
let result = params[0].name + '<br/>'
|
let result = params[0].name + '<br/>'
|
||||||
// 只显示数据系列,不显示底色系列
|
|
||||||
params.forEach((param: any) => {
|
params.forEach((param: any) => {
|
||||||
if (param.seriesName === '支出' || param.seriesName === '收入') {
|
if (param.seriesName === '支出' || param.seriesName === '收入') {
|
||||||
result += param.marker + param.seriesName + ': ' + param.value + '<br/>'
|
result += param.marker + param.seriesName + ': ' + param.value + '<br/>'
|
||||||
@ -154,7 +151,7 @@ const createChartOption = (): EChartsOption => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
// 支出数据(渐变)- 先绘制,作为底层
|
// 支出数据
|
||||||
{
|
{
|
||||||
name: '支出',
|
name: '支出',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
@ -165,40 +162,36 @@ const createChartOption = (): EChartsOption => {
|
|||||||
type: 'linear',
|
type: 'linear',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
x2: 0,
|
x2: 1,
|
||||||
y2: 1,
|
y2: 0,
|
||||||
colorStops: [
|
colorStops: [
|
||||||
{
|
{ offset: 0, color: '#10A0F2' },
|
||||||
offset: 0,
|
{ offset: 0.5, color: '#0D8BD9' },
|
||||||
color: '#10A0F2'
|
{ offset: 1, color: '#0A6EB0' }
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: 'rgba(0, 82, 184, 0)'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
borderRadius: [2, 2, 0, 0]
|
||||||
},
|
},
|
||||||
barWidth: '20%',
|
barWidth: '25%',
|
||||||
barGap: '20%'
|
barGap: '30%'
|
||||||
},
|
},
|
||||||
// 支出底色 - 后绘制,堆叠在数据上方
|
// 支出底色
|
||||||
{
|
{
|
||||||
name: '支出底色',
|
name: '支出底色',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
stack: 'monthly',
|
stack: 'monthly',
|
||||||
data: monthlyStandardBgData,
|
data: monthlyStandardBgData,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#38668D70'
|
color: 'rgba(56, 102, 141, 0.3)'
|
||||||
},
|
},
|
||||||
barWidth: '20%',
|
barWidth: '25%',
|
||||||
barGap: '20%',
|
barGap: '30%',
|
||||||
silent: true,
|
silent: true,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: false
|
show: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 收入数据(渐变)
|
// 收入数据
|
||||||
{
|
{
|
||||||
name: '收入',
|
name: '收入',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
@ -209,33 +202,30 @@ const createChartOption = (): EChartsOption => {
|
|||||||
type: 'linear',
|
type: 'linear',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
x2: 0,
|
x2: 1,
|
||||||
y2: 1,
|
y2: 0,
|
||||||
colorStops: [
|
colorStops: [
|
||||||
{
|
{ offset: 0, color: '#FFA58D' },
|
||||||
offset: 0,
|
{ offset: 0.5, color: '#E88F5A' },
|
||||||
color: '#FFA58D'
|
{ offset: 1, color: '#D07530' }
|
||||||
},
|
|
||||||
{
|
|
||||||
offset: 1,
|
|
||||||
color: 'rgba(87, 140, 205, 0)'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
borderRadius: [2, 2, 0, 0]
|
||||||
},
|
},
|
||||||
barWidth: '20%',
|
barWidth: '25%',
|
||||||
barGap: '80%'
|
barGap: '30%'
|
||||||
},
|
},
|
||||||
|
// 收入底色
|
||||||
{
|
{
|
||||||
name: '收入底色',
|
name: '收入底色',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
stack: 'perCapita',
|
stack: 'perCapita',
|
||||||
data: perCapitaBgData,
|
data: perCapitaBgData,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#38668D70'
|
color: 'rgba(56, 102, 141, 0.3)'
|
||||||
},
|
},
|
||||||
barWidth: '20%',
|
barWidth: '25%',
|
||||||
barGap: '80%',
|
barGap: '30%',
|
||||||
silent: true,
|
silent: true,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: false
|
show: false
|
||||||
@ -247,15 +237,6 @@ const createChartOption = (): EChartsOption => {
|
|||||||
|
|
||||||
// 柱状图配置
|
// 柱状图配置
|
||||||
const barOption = computed(() => createChartOption())
|
const barOption = computed(() => createChartOption())
|
||||||
|
|
||||||
// 监听数据变化,更新图表
|
|
||||||
watch(
|
|
||||||
() => [props.data, props.cardData],
|
|
||||||
() => {
|
|
||||||
// 数据变化时,computed 会自动更新
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -265,30 +246,4 @@ watch(
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-cards {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-card-item {
|
|
||||||
text-align: center;
|
|
||||||
padding: 4px;
|
|
||||||
background: rgba(56, 102, 141, 0.3);
|
|
||||||
border-radius: 8px;
|
|
||||||
min-width: 100px;
|
|
||||||
|
|
||||||
.card-value {
|
|
||||||
font-size: 1.6vh;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #00d4ff;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-label {
|
|
||||||
font-size: 1.5vh;
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<div class="info-tag">{{ basicInfo.district }}</div>
|
<div class="info-tag">{{ basicInfo.district }}</div>
|
||||||
<div class="info-tag">狱政编号: {{ basicInfo.prisonNumber }}</div>
|
<div class="info-tag">狱政编号: {{ basicInfo.prisonNumber }}</div>
|
||||||
<div class="info-tag">
|
<div class="info-tag">
|
||||||
刑期起/止日:{{ basicInfo.sentenceStart }}---{{ basicInfo.sentenceEnd }} ({{
|
刑期:{{ basicInfo.sentenceStart }} --- {{ basicInfo.sentenceEnd }} (总天数:{{
|
||||||
basicInfo.sentenceDays
|
basicInfo.sentenceDays
|
||||||
}}天)
|
}}天)
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,18 +2,7 @@
|
|||||||
<div class="rewards-punishments-container">
|
<div class="rewards-punishments-container">
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div class="rewards-header">
|
<div class="rewards-header">
|
||||||
<span class="header-title">近期奖惩</span>
|
<span class="header-title">风险评估</span>
|
||||||
<div class="filter-tabs">
|
|
||||||
<div
|
|
||||||
v-for="tab in filterTabs"
|
|
||||||
:key="tab.value"
|
|
||||||
class="filter-tab"
|
|
||||||
:class="{ active: activeFilter === tab.value }"
|
|
||||||
@click="activeFilter = tab.value"
|
|
||||||
>
|
|
||||||
{{ tab.label }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 时间线列表 -->
|
<!-- 时间线列表 -->
|
||||||
@ -21,7 +10,7 @@
|
|||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<div class="timeline-line"></div>
|
<div class="timeline-line"></div>
|
||||||
<div class="timeline-items">
|
<div class="timeline-items">
|
||||||
<div v-for="(item, index) in filteredList" :key="index" class="timeline-item">
|
<div v-for="(item, index) in listData" :key="index" class="timeline-item">
|
||||||
<div class="timeline-dot" :class="item.type"></div>
|
<div class="timeline-dot" :class="item.type"></div>
|
||||||
<div class="timeline-card">
|
<div class="timeline-card">
|
||||||
<div class="card-type" :class="item.type">{{ item.typeText }}</div>
|
<div class="card-type" :class="item.type">{{ item.typeText }}</div>
|
||||||
@ -35,7 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
defineOptions({ name: 'RecentRewardsPunishments' })
|
defineOptions({ name: 'RecentRewardsPunishments' })
|
||||||
|
|
||||||
@ -46,29 +35,9 @@ interface RewardPunishmentItem {
|
|||||||
content: string // 内容
|
content: string // 内容
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤标签
|
|
||||||
const filterTabs = [
|
|
||||||
{ label: '全部', value: 'all' },
|
|
||||||
{ label: '奖励记录', value: 'reward' },
|
|
||||||
{ label: '惩罚记录', value: 'punishment' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const activeFilter = ref<string>('all')
|
|
||||||
|
|
||||||
// 数据列表 - 使用 ref 存储
|
// 数据列表 - 使用 ref 存储
|
||||||
const listData = ref<RewardPunishmentItem[]>([])
|
const listData = ref<RewardPunishmentItem[]>([])
|
||||||
|
|
||||||
// 过滤后的列表
|
|
||||||
const filteredList = computed(() => {
|
|
||||||
if (activeFilter.value === 'all') {
|
|
||||||
return listData.value
|
|
||||||
} else if (activeFilter.value === 'reward') {
|
|
||||||
return listData.value.filter((item) => item.type === 'reward')
|
|
||||||
} else {
|
|
||||||
return listData.value.filter((item) => item.type === 'danger')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 可以通过 props 接收外部数据
|
// 可以通过 props 接收外部数据
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -118,24 +87,6 @@ watch(
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tab {
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
background: rgba(56, 102, 141, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: #37599d;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 时间线容器
|
// 时间线容器
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
|||||||
@ -351,6 +351,10 @@ watch(
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getLoginFormCache()
|
getLoginFormCache()
|
||||||
getTenantByWebsite()
|
getTenantByWebsite()
|
||||||
|
// 初始化租户 ID - 确保登录前缓存中有租户 ID
|
||||||
|
if (!authUtil.getTenantId()) {
|
||||||
|
authUtil.setTenantId(Number(loginData.loginForm.tenantName) || 1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog title="消费明细" v-model="dialogVisible" width="600px">
|
<Dialog title="消费明细" v-model="dialogVisible" width="600px">
|
||||||
<el-table :data="detailList" v-loading="loading">
|
<el-table :data="detailList" v-loading="loading">
|
||||||
<el-table-column label="商品名称" prop="goodsName" align="center" />
|
<el-table-column label="商品名称" prop="goodsName" align="center">
|
||||||
<el-table-column label="商品编码" prop="goodsCode" align="center" width="120" />
|
<template #default="{ row }">
|
||||||
|
{{ row.goodsName || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品编码" prop="goodsCode" align="center" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.goodsCode || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="单价" prop="goodsPrice" align="center" width="100">
|
<el-table-column label="单价" prop="goodsPrice" align="center" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
¥{{ row.goodsPrice?.toFixed(2) }}
|
¥{{ row.goodsPrice?.toFixed(2) }}
|
||||||
|
|||||||
@ -9,13 +9,13 @@
|
|||||||
>
|
>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="罪犯ID" prop="prisonerId">
|
<el-form-item label="服刑人员">
|
||||||
<el-input v-model="formData.prisonerId" placeholder="请输入罪犯ID" />
|
<el-input v-model="formData.prisonerName" placeholder="服刑人员姓名" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="罪犯编号" prop="prisonerNo">
|
<el-form-item label="罪犯编号" prop="prisonerNo">
|
||||||
<el-input v-model="formData.prisonerNo" placeholder="请输入罪犯编号" />
|
<el-input v-model="formData.prisonerNo" placeholder="罪犯编号" disabled />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
@ -135,6 +135,7 @@ const formType = ref('')
|
|||||||
const formData = ref({
|
const formData = ref({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
prisonerId: undefined,
|
prisonerId: undefined,
|
||||||
|
prisonerName: undefined,
|
||||||
prisonerNo: undefined,
|
prisonerNo: undefined,
|
||||||
orderNo: undefined,
|
orderNo: undefined,
|
||||||
type: undefined,
|
type: undefined,
|
||||||
@ -228,6 +229,7 @@ const resetForm = () => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
prisonerId: undefined,
|
prisonerId: undefined,
|
||||||
|
prisonerName: undefined,
|
||||||
prisonerNo: undefined,
|
prisonerNo: undefined,
|
||||||
orderNo: undefined,
|
orderNo: undefined,
|
||||||
type: undefined,
|
type: undefined,
|
||||||
|
|||||||
@ -77,11 +77,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||||
import { EvaluationTemplateApi } from '@/api/prison/evaluation'
|
import { EvaluationTemplateApi } from '@/api/prison/evaluation'
|
||||||
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
|
||||||
defineOptions({ name: 'EvaluationTemplateForm' })
|
defineOptions({ name: 'EvaluationTemplateForm' })
|
||||||
|
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
// 保存当前编辑的模板ID,用于刷新数据
|
// 保存当前编辑的模板ID,用于刷新数据
|
||||||
const currentTemplateId = ref<number | undefined>(undefined)
|
const currentTemplateId = ref<number | undefined>(undefined)
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog style="display: none;" :title="'评估报告'" v-model="dialogVisible" width="900px">
|
<Dialog style="display: none" :title="'评估报告'" v-model="dialogVisible" width="900px">
|
||||||
<div v-loading="loading" class="report-edit-container" ref="previewRef">
|
<div v-loading="loading" class="report-edit-container" ref="previewRef">
|
||||||
<template v-if="selectedReport">
|
<template v-if="selectedReport">
|
||||||
<div class="basic-info-title">{{ selectedReport.templateName }}</div>
|
<div class="basic-info-title">{{ selectedReport.templateName }}</div>
|
||||||
<!-- 基本信息区 -->
|
<!-- 基本信息区 -->
|
||||||
<div class="basic-info-section">
|
<div class="basic-info-section">
|
||||||
<div class="basic-info-item">服刑人员:{{ selectedReport.prisonerName }} ({{ selectedReport.prisonerNo }})</div>
|
<div class="basic-info-item"
|
||||||
|
>服刑人员:{{ selectedReport.prisonerName }} ({{ selectedReport.prisonerNo }})</div
|
||||||
|
>
|
||||||
<div class="basic-info-item">监区:{{ selectedReport.areaName || '-' }}</div>
|
<div class="basic-info-item">监区:{{ selectedReport.areaName || '-' }}</div>
|
||||||
<div class="basic-info-item">评估日期:{{ formatDateTime(selectedReport.evaluationDate, 'YYYY-MM-DD') }}</div>
|
<div class="basic-info-item"
|
||||||
<div class="basic-info-item">风险等级:{{ getDictLabel(DICT_TYPE.PRISON_RISK_LEVEL, selectedReport.riskLevel) }}</div>
|
>评估日期:{{ formatDateTime(selectedReport.evaluationDate, 'YYYY-MM-DD') }}</div
|
||||||
<div class="basic-info-item">状态:{{ getDictLabel(DICT_TYPE.PRISON_REPORT_STATUS, selectedReport.status) }}</div>
|
>
|
||||||
|
<div class="basic-info-item"
|
||||||
|
>风险等级:{{
|
||||||
|
getDictLabel(DICT_TYPE.PRISON_RISK_LEVEL, selectedReport.riskLevel)
|
||||||
|
}}</div
|
||||||
|
>
|
||||||
|
<div class="basic-info-item"
|
||||||
|
>状态:{{ getDictLabel(DICT_TYPE.PRISON_REPORT_STATUS, selectedReport.status) }}</div
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="item in dimensionAnalysisPanelRef" :key="item.id" class="dimension-item">
|
<div
|
||||||
<div class="dimension-item-title">{{ item.name }}</div>
|
v-for="(item, index) in dimensionAnalysisPanelRef"
|
||||||
<div style="white-space: pre-line; line-height: 1.5;">{{ item.aiAnalysis?.replace(/## 综合分析建议\n\n/g, '') }}</div>
|
:key="item.id"
|
||||||
|
class="dimension-item"
|
||||||
|
>
|
||||||
|
<div class="dimension-item-title"
|
||||||
|
>{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}、{{
|
||||||
|
item.name
|
||||||
|
}}</div
|
||||||
|
>
|
||||||
|
<div style="white-space: pre-line; line-height: 1.5">{{
|
||||||
|
// 移除所有以 '##' 开头、并以两个连续换行结尾的标题块,兼容 CRLF/LF
|
||||||
|
item.aiAnalysis?.replace(/(^|\r?\n)##.*?\r?\n\r?\n/gm, '$1')
|
||||||
|
}}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -32,7 +53,14 @@
|
|||||||
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||||
import { formatDateTime } from '@/utils/formatTime'
|
import { formatDateTime } from '@/utils/formatTime'
|
||||||
import download from '@/utils/download'
|
import download from '@/utils/download'
|
||||||
import { ReportApi, ReportVO, DimensionDataApi, DimensionDataVO, DimensionApi, DimensionVO } from '@/api/prison/evaluation-report'
|
import {
|
||||||
|
ReportApi,
|
||||||
|
ReportVO,
|
||||||
|
DimensionDataApi,
|
||||||
|
DimensionDataVO,
|
||||||
|
DimensionApi,
|
||||||
|
DimensionVO
|
||||||
|
} from '@/api/prison/evaluation-report'
|
||||||
import { PrisonerApi } from '@/api/prison/prisoner'
|
import { PrisonerApi } from '@/api/prison/prisoner'
|
||||||
import { asBlob } from 'html-docx-js-typescript'
|
import { asBlob } from 'html-docx-js-typescript'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
@ -64,8 +92,10 @@ const open = async (id: number, prisonerId?: number) => {
|
|||||||
reportId.value = id
|
reportId.value = id
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
await loadReportDetail(id)
|
await loadReportDetail(id)
|
||||||
|
// 等待视图渲染完成,避免导出时 DOM 仍为空
|
||||||
|
await nextTick()
|
||||||
try {
|
try {
|
||||||
exportToWord()
|
await exportToWord()
|
||||||
} catch {
|
} catch {
|
||||||
} finally {
|
} finally {
|
||||||
handleClose()
|
handleClose()
|
||||||
@ -78,7 +108,8 @@ const loadReportDetail = async (id: number) => {
|
|||||||
try {
|
try {
|
||||||
selectedReport.value = await ReportApi.getReport(id)
|
selectedReport.value = await ReportApi.getReport(id)
|
||||||
dimensionDataList.value = await DimensionDataApi.getDimensionDataListByReportId(id)
|
dimensionDataList.value = await DimensionDataApi.getDimensionDataListByReportId(id)
|
||||||
drawerTitle.value = selectedReport.value?.title || `${selectedReport.value?.prisonerName} - 评估报告`
|
drawerTitle.value =
|
||||||
|
selectedReport.value?.title || `${selectedReport.value?.prisonerName} - 评估报告`
|
||||||
|
|
||||||
// 监区信息兜底(报告未返回监区时,从罪犯档案补齐)
|
// 监区信息兜底(报告未返回监区时,从罪犯档案补齐)
|
||||||
if (selectedReport.value?.prisonerId && !selectedReport.value.areaName) {
|
if (selectedReport.value?.prisonerId && !selectedReport.value.areaName) {
|
||||||
@ -92,15 +123,16 @@ const loadReportDetail = async (id: number) => {
|
|||||||
// 加载维度配置
|
// 加载维度配置
|
||||||
if (selectedReport.value?.templateId) {
|
if (selectedReport.value?.templateId) {
|
||||||
try {
|
try {
|
||||||
const dimensionList = await DimensionApi.getDimensionsByTemplateId(selectedReport.value.templateId)
|
const dimensionList = await DimensionApi.getDimensionsByTemplateId(
|
||||||
|
selectedReport.value.templateId
|
||||||
|
)
|
||||||
if (dimensionList && dimensionList.length > 0) {
|
if (dimensionList && dimensionList.length > 0) {
|
||||||
console.log(dimensionList);
|
console.log(dimensionList)
|
||||||
dimensions.value = dimensionList
|
dimensions.value = dimensionList
|
||||||
} else {
|
} else {
|
||||||
// 使用默认维度配置
|
// 使用默认维度配置
|
||||||
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
|
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
|
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
|
||||||
}
|
}
|
||||||
@ -108,11 +140,11 @@ const loadReportDetail = async (id: number) => {
|
|||||||
|
|
||||||
if (selectedReport.value?.id && dimensions.value.length > 0) {
|
if (selectedReport.value?.id && dimensions.value.length > 0) {
|
||||||
const list = await DimensionDataApi.getDimensionDataListByReportId(selectedReport.value.id)
|
const list = await DimensionDataApi.getDimensionDataListByReportId(selectedReport.value.id)
|
||||||
console.log(list, dimensions.value);
|
console.log(list, dimensions.value)
|
||||||
dimensionAnalysisPanelRef.value = dimensions.value.map(item => {
|
dimensionAnalysisPanelRef.value = dimensions.value.map((item) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
aiAnalysis: list.find(analys => analys.dimensionId === item.id)?.aiAnalysis
|
aiAnalysis: list.find((analys) => analys.dimensionId === item.id)?.aiAnalysis
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -127,11 +159,51 @@ const loadReportDetail = async (id: number) => {
|
|||||||
/** 获取默认维度配置 */
|
/** 获取默认维度配置 */
|
||||||
const getDefaultDimensions = (templateId: number): DimensionVO[] => {
|
const getDefaultDimensions = (templateId: number): DimensionVO[] => {
|
||||||
return [
|
return [
|
||||||
{ id: 1, templateId, name: '基本信息', dimensionType: 1, aiEnabled: 0, status: 0, dataSources: ['prisoner'] },
|
{
|
||||||
{ id: 2, templateId, name: '犯罪情况分析', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['prisoner', 'risk'] },
|
id: 1,
|
||||||
{ id: 3, templateId, name: '服刑表现评估', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['score', 'violation', 'reward'] },
|
templateId,
|
||||||
{ id: 4, templateId, name: '消费行为分析', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['consumption'] },
|
name: '基本信息',
|
||||||
{ id: 5, templateId, name: '综合评估结论', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['prisoner', 'psychology'] }
|
dimensionType: 1,
|
||||||
|
aiEnabled: 0,
|
||||||
|
status: 0,
|
||||||
|
dataSources: ['prisoner']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
templateId,
|
||||||
|
name: '犯罪情况分析',
|
||||||
|
dimensionType: 1,
|
||||||
|
aiEnabled: 1,
|
||||||
|
status: 0,
|
||||||
|
dataSources: ['prisoner', 'risk']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
templateId,
|
||||||
|
name: '服刑表现评估',
|
||||||
|
dimensionType: 1,
|
||||||
|
aiEnabled: 1,
|
||||||
|
status: 0,
|
||||||
|
dataSources: ['score', 'violation', 'reward']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
templateId,
|
||||||
|
name: '消费行为分析',
|
||||||
|
dimensionType: 1,
|
||||||
|
aiEnabled: 1,
|
||||||
|
status: 0,
|
||||||
|
dataSources: ['consumption']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
templateId,
|
||||||
|
name: '综合评估结论',
|
||||||
|
dimensionType: 1,
|
||||||
|
aiEnabled: 1,
|
||||||
|
status: 0,
|
||||||
|
dataSources: ['prisoner', 'psychology']
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,40 +261,36 @@ const exportToWord = async () => {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<style>
|
<style>
|
||||||
|
/* 使用 pt 单位以避免 Word/WPS 在 px->pt 换算时产生差异 */
|
||||||
body {
|
body {
|
||||||
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 11pt;
|
||||||
line-height: 1.6;
|
line-height: 1.5;
|
||||||
margin: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
.basic-info-title{
|
.basic-info-title{
|
||||||
font-family: '黑体', 'Microsoft YaHei', SimSun, Arial, sans-serif;
|
font-size: 18pt;
|
||||||
font-size: 21px;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: black;
|
color: black;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 15pt;
|
||||||
}
|
}
|
||||||
.basic-info-section {
|
.basic-info-section {
|
||||||
padding: 15px 20px;
|
|
||||||
color: black;
|
color: black;
|
||||||
font-size: 12px;
|
font-size: 11pt;
|
||||||
}
|
}
|
||||||
.basic-info-item{
|
.basic-info-item{
|
||||||
margin-right: 30px;
|
margin-right: 25pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dimension-item {
|
.dimension-item {
|
||||||
padding: 0 40px;
|
font-size: 11pt;
|
||||||
font-size: 12px;
|
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
.dimension-item-title {
|
.dimension-item-title {
|
||||||
font-size: 21px;
|
font-size: 15pt;
|
||||||
padding: 15px 0;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: black;
|
color: black;
|
||||||
|
margin-top: 15pt;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -269,7 +337,7 @@ defineExpose({ open })
|
|||||||
color: black;
|
color: black;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.basic-info-item{
|
.basic-info-item {
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -163,12 +163,13 @@ const open = async (type: string, templateId: number, id?: number) => {
|
|||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
dialogTitle.value = t('action.' + type)
|
dialogTitle.value = t('action.' + type)
|
||||||
formType.value = type
|
formType.value = type
|
||||||
formData.value.templateId = templateId
|
|
||||||
resetForm()
|
resetForm()
|
||||||
|
formData.value.templateId = templateId
|
||||||
if (id) {
|
if (id) {
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
formData.value = await DimensionApi.getDimension(id)
|
const existingDimension = await DimensionApi.getDimension(id)
|
||||||
|
formData.value = { ...existingDimension, templateId }
|
||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false
|
formLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@
|
|||||||
<!-- 维度配置 Tab -->
|
<!-- 维度配置 Tab -->
|
||||||
<el-tab-pane label="维度配置" name="dimension">
|
<el-tab-pane label="维度配置" name="dimension">
|
||||||
<div class="dimension-header mb-15px">
|
<div class="dimension-header mb-15px">
|
||||||
<el-button type="primary" @click="openDimensionForm(selectedTemplate.id!)" v-hasPermi="['prison:evaluation-report:dimension:create']">
|
<el-button type="primary" @click="openDimensionForm(selectedTemplate.id!)" v-hasPermi="['prison:evaluation-report:template:update']">
|
||||||
<Icon icon="ep:plus" class="mr-5px" /> 新增维度
|
<Icon icon="ep:plus" class="mr-5px" /> 新增维度
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -111,7 +111,7 @@
|
|||||||
<el-button type="primary" link size="small" @click="openDimensionForm(selectedTemplate.id!, dimension.id)">
|
<el-button type="primary" link size="small" @click="openDimensionForm(selectedTemplate.id!, dimension.id)">
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" link size="small" @click="handleDeleteDimension(dimension.id!)" v-hasPermi="['prison:evaluation-report:dimension:delete']">
|
<el-button type="danger" link size="small" @click="handleDeleteDimension(dimension.id!)" v-hasPermi="['prison:evaluation-report:template:update']">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,13 +32,21 @@
|
|||||||
<div class="options-container">
|
<div class="options-container">
|
||||||
<el-radio-group v-model="answers[question.id]" :disabled="disabled">
|
<el-radio-group v-model="answers[question.id]" :disabled="disabled">
|
||||||
<el-radio
|
<el-radio
|
||||||
v-for="(opt, index) in parseOptions(question.options)"
|
v-for="(opt, optIdx) in parseOptions(question.options)"
|
||||||
:key="index"
|
:key="optIdx"
|
||||||
:value="index"
|
:value="optIdx"
|
||||||
>
|
>
|
||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
<!-- 其他选项输入框 -->
|
||||||
|
<div v-if="getOtherOptionIndex(question) !== undefined && answers[question.id] === getOtherOptionIndex(question)" class="other-input">
|
||||||
|
<el-input
|
||||||
|
v-model="otherAnswers[question.id]"
|
||||||
|
placeholder="请输入其他选项内容"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -47,14 +55,25 @@
|
|||||||
<div class="options-container">
|
<div class="options-container">
|
||||||
<el-checkbox-group v-model="multiAnswers[question.id]">
|
<el-checkbox-group v-model="multiAnswers[question.id]">
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
v-for="(opt, index) in parseOptions(question.options)"
|
v-for="(opt, optIdx) in parseOptions(question.options)"
|
||||||
:key="index"
|
:key="optIdx"
|
||||||
:value="index"
|
:value="optIdx"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
|
<!-- 其他选项输入框 -->
|
||||||
|
<div
|
||||||
|
v-if="isOtherOptionSelected(question, multiAnswers[question.id])"
|
||||||
|
class="other-input"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="otherAnswers[question.id]"
|
||||||
|
placeholder="请输入其他选项内容"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -107,7 +126,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { QuestionnaireApi } from '@/api/prison/questionnaire'
|
|
||||||
import { QuestionApi } from '@/api/prison/question'
|
import { QuestionApi } from '@/api/prison/question'
|
||||||
import { QuestionnaireRecordApi } from '@/api/prison/questionnairerecord'
|
import { QuestionnaireRecordApi } from '@/api/prison/questionnairerecord'
|
||||||
|
|
||||||
@ -128,6 +146,7 @@ const questions = ref<any[]>([])
|
|||||||
// 答案
|
// 答案
|
||||||
const answers = reactive<Record<number, any>>({})
|
const answers = reactive<Record<number, any>>({})
|
||||||
const multiAnswers = reactive<Record<number, number[]>>({})
|
const multiAnswers = reactive<Record<number, number[]>>({})
|
||||||
|
const otherAnswers = reactive<Record<number, string>>({}) // 其他选项输入框的答案
|
||||||
|
|
||||||
// 是否禁用
|
// 是否禁用
|
||||||
const disabled = computed(() => submitLoading.value)
|
const disabled = computed(() => submitLoading.value)
|
||||||
@ -142,24 +161,51 @@ const parseOptions = (optionsStr: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取其他选项的索引 */
|
||||||
|
const getOtherOptionIndex = (question: any): number | undefined => {
|
||||||
|
const options = parseOptions(question.options)
|
||||||
|
return options.findIndex((opt: any) => opt.isOther === true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检查多选题的其他选项是否被选中 */
|
||||||
|
const isOtherOptionSelected = (question: any, selectedIndices: number[] | undefined): boolean => {
|
||||||
|
const otherIdx = getOtherOptionIndex(question)
|
||||||
|
if (otherIdx === undefined || otherIdx === -1) return false
|
||||||
|
return selectedIndices?.includes(otherIdx) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
const open = async (record: any) => {
|
const open = async (record: any) => {
|
||||||
|
console.log('=== 代填弹窗打开 ===')
|
||||||
|
console.log('传入的record:', record)
|
||||||
|
console.log('record.questionnaireId:', record.questionnaireId)
|
||||||
|
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
recordId.value = record.id
|
recordId.value = record.id
|
||||||
prisonerInfo.value = record
|
prisonerInfo.value = record
|
||||||
questionnaireId.value = record.questionnaireId
|
questionnaireId.value = record.questionnaireId
|
||||||
|
|
||||||
|
console.log('设置的questionnaireId.value:', questionnaireId.value)
|
||||||
|
|
||||||
// 重置答案
|
// 重置答案
|
||||||
Object.keys(answers).forEach(key => delete answers[key])
|
Object.keys(answers).forEach(key => delete answers[key])
|
||||||
Object.keys(multiAnswers).forEach(key => delete multiAnswers[key])
|
Object.keys(multiAnswers).forEach(key => delete multiAnswers[key])
|
||||||
|
Object.keys(otherAnswers).forEach(key => delete otherAnswers[key])
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
|
// 从后端重新获取最新的记录信息,确保状态是最新的
|
||||||
|
const latestRecord = await QuestionnaireRecordApi.getQuestionnaireRecord(record.id)
|
||||||
|
console.log('获取的最新记录状态:', latestRecord.status)
|
||||||
|
prisonerInfo.value = { ...record, ...latestRecord }
|
||||||
|
|
||||||
// 先获取问卷题目总数
|
// 先获取问卷题目总数
|
||||||
const countData = await QuestionApi.getQuestionnaireQuestionList(
|
const countData = await QuestionApi.getQuestionnaireQuestionList(
|
||||||
{ questionnaireId: questionnaireId.value, pageNo: 1, pageSize: 1 }
|
{ questionnaireId: questionnaireId.value, pageNo: 1, pageSize: 1 }
|
||||||
)
|
)
|
||||||
|
console.log('获取的countData:', countData)
|
||||||
const totalCount = countData.total || 0
|
const totalCount = countData.total || 0
|
||||||
|
console.log('题目总数:', totalCount)
|
||||||
|
|
||||||
// 根据总数获取所有题目(后端限制每页最大200条)
|
// 根据总数获取所有题目(后端限制每页最大200条)
|
||||||
const maxPageSize = 200
|
const maxPageSize = 200
|
||||||
@ -168,6 +214,8 @@ const open = async (record: any) => {
|
|||||||
const questionData = await QuestionApi.getQuestionnaireQuestionList(
|
const questionData = await QuestionApi.getQuestionnaireQuestionList(
|
||||||
{ questionnaireId: questionnaireId.value, pageNo: 1, pageSize: pageSize }
|
{ questionnaireId: questionnaireId.value, pageNo: 1, pageSize: pageSize }
|
||||||
)
|
)
|
||||||
|
console.log('获取的questionData:', questionData)
|
||||||
|
console.log('实际获取到的题目数量:', questionData.list?.length || 0)
|
||||||
questions.value = questionData.list || []
|
questions.value = questionData.list || []
|
||||||
|
|
||||||
// 初始化答案结构
|
// 初始化答案结构
|
||||||
@ -208,11 +256,68 @@ const handleSubmit = async () => {
|
|||||||
questionId: q.id
|
questionId: q.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const options = parseOptions(q.options)
|
||||||
|
const otherIndex = getOtherOptionIndex(q)
|
||||||
|
const hasOtherSelected = otherIndex !== undefined && (
|
||||||
|
(q.type === 1 && answers[q.id] === otherIndex) ||
|
||||||
|
(q.type === 2 && multiAnswers[q.id]?.includes(otherIndex))
|
||||||
|
)
|
||||||
|
|
||||||
if (q.type === 2) {
|
if (q.type === 2) {
|
||||||
// 多选题
|
// 多选题:直接使用选项索引作为 optionIds(纯数字)
|
||||||
answerItem.optionIds = multiAnswers[q.id] || []
|
answerItem.optionIds = multiAnswers[q.id] || []
|
||||||
} else {
|
// 拼接所有选中的选项文字到 answer 字段
|
||||||
|
const selectedIndices = multiAnswers[q.id] || []
|
||||||
|
const selectedLabels = selectedIndices.map((idx: number) => {
|
||||||
|
const opt = options[idx]
|
||||||
|
return opt ? (opt.value !== undefined ? opt.value : opt.label) : String(idx)
|
||||||
|
})
|
||||||
|
if (selectedLabels.length > 0) {
|
||||||
|
answerItem.answer = selectedLabels.join('、')
|
||||||
|
}
|
||||||
|
} else if (q.type === 1) {
|
||||||
|
// 单选题:检查是否选择了"其他"选项
|
||||||
|
const selectedIndex = answers[q.id]
|
||||||
|
const otherIndex = getOtherOptionIndex(q)
|
||||||
|
const isOtherSelected = otherIndex !== undefined && selectedIndex === otherIndex
|
||||||
|
|
||||||
|
if (isOtherSelected) {
|
||||||
|
// 选择了"其他"选项,需要发送 optionIds
|
||||||
|
answerItem.optionIds = [selectedIndex]
|
||||||
|
}
|
||||||
|
// 普通单选题不发送 optionIds
|
||||||
|
}
|
||||||
|
// 填空题、评分题、日期题、数字题不发送 optionIds
|
||||||
|
|
||||||
|
// 如果选中了其他选项,将其他输入框的内容通过 answerText 传递
|
||||||
|
if (hasOtherSelected && otherAnswers[q.id]) {
|
||||||
|
const otherOptValue = options[otherIndex] ? (options[otherIndex].value !== undefined ? options[otherIndex].value : options[otherIndex].label) : '其他'
|
||||||
|
const otherText = `${otherOptValue}:${otherAnswers[q.id]}`
|
||||||
|
if (q.type === 2) {
|
||||||
|
// 多选题:追加其他选项的文字(前面已经有选中选项的文字了)
|
||||||
|
answerItem.answer = answerItem.answer
|
||||||
|
? `${answerItem.answer}、${otherText}`
|
||||||
|
: otherText
|
||||||
|
} else {
|
||||||
|
// 单选题:直接设置其他选项的文字
|
||||||
|
answerItem.answer = otherText
|
||||||
|
}
|
||||||
|
} else if (q.type === 2 && answerItem.answer) {
|
||||||
|
// 多选题选中了"其他"选项但没有输入文字,也要显示"其他"选项的文字
|
||||||
|
if (hasOtherSelected) {
|
||||||
|
const otherOptValue = options[otherIndex] ? (options[otherIndex].value !== undefined ? options[otherIndex].value : options[otherIndex].label) : '其他'
|
||||||
|
answerItem.answer = `${answerItem.answer}、${otherOptValue}`
|
||||||
|
}
|
||||||
|
} else if (q.type === 3 || q.type === 4 || q.type === 5 || q.type === 6) {
|
||||||
|
// 填空题、评分题、日期题、数字题直接使用 answerText
|
||||||
answerItem.answer = answers[q.id] !== undefined ? String(answers[q.id]) : ''
|
answerItem.answer = answers[q.id] !== undefined ? String(answers[q.id]) : ''
|
||||||
|
} else if (q.type === 1 && !hasOtherSelected) {
|
||||||
|
// 单选题没有选择其他选项时,将选项值设置为 answerText
|
||||||
|
const selectedIndex = answers[q.id]
|
||||||
|
if (selectedIndex !== undefined && selectedIndex !== null) {
|
||||||
|
const opt = options[selectedIndex]
|
||||||
|
answerItem.answer = opt ? (opt.value !== undefined ? opt.value : opt.label) : String(selectedIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return answerItem
|
return answerItem
|
||||||
@ -327,5 +432,11 @@ defineExpose({ open })
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.other-input {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-left: 24px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -271,7 +271,7 @@
|
|||||||
{{ scope.row.finishTime ? formatDateTime(scope.row.finishTime) : '-' }}
|
{{ scope.row.finishTime ? formatDateTime(scope.row.finishTime) : '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center" width="320" fixed="right">
|
<el-table-column label="操作" align="center" width="400" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="scope.row.status === 3"
|
v-if="scope.row.status === 3"
|
||||||
@ -319,6 +319,15 @@
|
|||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleViewPrisoner(scope.row)"
|
||||||
|
v-hasPermi="['prison:prisoner:query']"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -338,6 +347,8 @@
|
|||||||
<!-- 代填弹窗 -->
|
<!-- 代填弹窗 -->
|
||||||
<AgentFillDialog ref="agentFillDialogRef" @success="loadPrisonerProgress" />
|
<AgentFillDialog ref="agentFillDialogRef" @success="loadPrisonerProgress" />
|
||||||
|
|
||||||
|
<!-- 服刑人员详情弹窗 -->
|
||||||
|
<PrisonerDetail ref="prisonerDetailRef" />
|
||||||
<!-- 问卷导出弹窗 -->
|
<!-- 问卷导出弹窗 -->
|
||||||
<QuestionnaireOutput ref="outputDialogRef" />
|
<QuestionnaireOutput ref="outputDialogRef" />
|
||||||
</template>
|
</template>
|
||||||
@ -348,6 +359,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
|||||||
import { formatDateTime } from '@/utils/formatTime'
|
import { formatDateTime } from '@/utils/formatTime'
|
||||||
import AnswerDetailDialog from '@/views/prison/questionnairerecord/AnswerDetailDialog.vue'
|
import AnswerDetailDialog from '@/views/prison/questionnairerecord/AnswerDetailDialog.vue'
|
||||||
import AgentFillDialog from './AgentFillDialog.vue'
|
import AgentFillDialog from './AgentFillDialog.vue'
|
||||||
|
import PrisonerDetail from '@/views/prison/prisoner/PrisonerDetail.vue'
|
||||||
import { QuestionnaireTaskApi } from '@/api/prison/questionnaire-task'
|
import { QuestionnaireTaskApi } from '@/api/prison/questionnaire-task'
|
||||||
import QuestionnaireOutput from '@/views/prison/questionnairerecord/QuestionnaireOutputfile.vue'
|
import QuestionnaireOutput from '@/views/prison/questionnairerecord/QuestionnaireOutputfile.vue'
|
||||||
|
|
||||||
@ -366,6 +378,9 @@ const answerDetailDialogRef = ref()
|
|||||||
// 代填弹窗
|
// 代填弹窗
|
||||||
const agentFillDialogRef = ref()
|
const agentFillDialogRef = ref()
|
||||||
|
|
||||||
|
// 服刑人员详情弹窗
|
||||||
|
const prisonerDetailRef = ref()
|
||||||
|
|
||||||
// 任务详情
|
// 任务详情
|
||||||
const taskDetail = ref<any>(null)
|
const taskDetail = ref<any>(null)
|
||||||
|
|
||||||
@ -477,6 +492,9 @@ const open = async (id: number) => {
|
|||||||
taskId.value = id
|
taskId.value = id
|
||||||
activeTab.value = 'overview'
|
activeTab.value = 'overview'
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
|
// 清空之前的任务数据
|
||||||
|
prisonerProgressList.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 并行加载数据
|
// 并行加载数据
|
||||||
@ -599,6 +617,11 @@ const handleAgentFill = (row: any) => {
|
|||||||
agentFillDialogRef.value?.open(row)
|
agentFillDialogRef.value?.open(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查看服刑人员详情 */
|
||||||
|
const handleViewPrisoner = (row: any) => {
|
||||||
|
prisonerDetailRef.value?.open(row.prisonerId)
|
||||||
|
}
|
||||||
|
|
||||||
/** 提醒未完成人员 */
|
/** 提醒未完成人员 */
|
||||||
const handleRemind = async () => {
|
const handleRemind = async () => {
|
||||||
if (!taskId.value) return
|
if (!taskId.value) return
|
||||||
|
|||||||
@ -117,29 +117,35 @@ const open = async (type: string, id?: number) => {
|
|||||||
}
|
}
|
||||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||||
|
|
||||||
/** 提交表单 */
|
/** 提交表单 */
|
||||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
// 校验表单
|
// 校验表单
|
||||||
await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
// 提交请求
|
// 提交请求:将 coverImage 数组转换为字符串(取第一个元素或null)
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = formData.value as unknown as Questionnaire
|
const data = { ...formData.value } as unknown as Questionnaire
|
||||||
if (formType.value === 'create') {
|
// 处理 coverImage 数组转换为字符串
|
||||||
await QuestionnaireApi.createQuestionnaire(data)
|
if (Array.isArray(data.coverImage) && data.coverImage.length > 0) {
|
||||||
message.success(t('common.createSuccess'))
|
data.coverImage = data.coverImage[0] as unknown as string
|
||||||
} else {
|
} else {
|
||||||
await QuestionnaireApi.updateQuestionnaire(data)
|
data.coverImage = undefined as unknown as string
|
||||||
message.success(t('common.updateSuccess'))
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
dialogVisible.value = false
|
|
||||||
// 发送操作成功的事件
|
|
||||||
emit('success')
|
|
||||||
} finally {
|
|
||||||
formLoading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** 重置表单 */
|
/** 重置表单 */
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
plain
|
plain
|
||||||
@click="openForm('create')"
|
@click="openForm('create')"
|
||||||
v-hasPermi="['prison:question:create']"
|
v-hasPermi="['prison:question:create', 'prison:questionnaire:update']"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:plus" class="mr-5px" /> 新建问题
|
<Icon icon="ep:plus" class="mr-5px" /> 新建问题
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
type="success"
|
type="success"
|
||||||
plain
|
plain
|
||||||
@click="openPartDialog"
|
@click="openPartDialog"
|
||||||
v-hasPermi="['prison:question:create']"
|
v-hasPermi="['prison:question:create', 'prison:questionnaire:update']"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:folder" class="mr-5px" /> 分区管理
|
<Icon icon="ep:folder" class="mr-5px" /> 分区管理
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
plain
|
plain
|
||||||
:disabled="checkedIds.length === 0"
|
:disabled="checkedIds.length === 0"
|
||||||
@click="handleDeleteBatch"
|
@click="handleDeleteBatch"
|
||||||
v-hasPermi="['prison:question:delete']"
|
v-hasPermi="['prison:question:delete', 'prison:questionnaire:update']"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
|
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -135,7 +135,7 @@
|
|||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
@click="openForm('update', question.id)"
|
@click="openForm('update', question.id)"
|
||||||
v-hasPermi="['prison:question:update']"
|
v-hasPermi="['prison:question:update', 'prison:questionnaire:update']"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:edit" /> 修改
|
<Icon icon="ep:edit" /> 修改
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -144,7 +144,7 @@
|
|||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleDelete(question.id)"
|
@click="handleDelete(question.id)"
|
||||||
v-hasPermi="['prison:question:delete']"
|
v-hasPermi="['prison:question:delete', 'prison:questionnaire:update']"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:delete" /> 删除
|
<Icon icon="ep:delete" /> 删除
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -192,13 +192,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" plain :icon="Plus" @click="addPartition" v-hasPermi="['prison:question:create']">添加分区</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="savePartitions" type="primary" v-hasPermi="['prison:question:update']">保存设置</el-button>
|
<el-button @click="savePartitions" type="primary" v-hasPermi="['prison:question:update', 'prison:questionnaire:update']">保存设置</el-button>
|
||||||
<el-button @click="partDialogVisible = false">取消</el-button>
|
<el-button @click="partDialogVisible = false">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
@ -486,26 +483,39 @@ const savePartitions = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收集所有需要更新的问题
|
// 收集所有需要更新的问题(包括默认分区)
|
||||||
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
||||||
for (let i = 0; i < allPartList.value.length; i++) {
|
for (let i = 0; i < allPartList.value.length; i++) {
|
||||||
const part = allPartList.value[i]
|
const part = allPartList.value[i]
|
||||||
if (!part.isDefault && part.name) {
|
for (const p of partitions.value) {
|
||||||
for (const p of partitions.value) {
|
// 匹配分区:两个 name 相等,或者两个都是空(默认分区)
|
||||||
if (p.name === part.name) {
|
if ((p.name === part.name) || (p.name === '' && part.name === '')) {
|
||||||
p.questions.forEach((q, sortIndex) => {
|
p.questions.forEach((q, sortIndex) => {
|
||||||
updates.push({
|
updates.push({
|
||||||
id: q.id!,
|
id: q.id!,
|
||||||
partName: part.name,
|
partName: part.name || undefined,
|
||||||
partSort: i,
|
partSort: i,
|
||||||
sort: sortIndex
|
sort: sortIndex
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 没有需要更新的问题(可能是添加了空分区)
|
||||||
|
if (updates.length === 0) {
|
||||||
|
// 检查是否有新建的空分区
|
||||||
|
const hasNewPartition = allPartList.value.some(p => !p.isDefault && p.id?.toString().startsWith('part_'))
|
||||||
|
if (hasNewPartition) {
|
||||||
|
message.error('请先在该分区下添加问题后再保存')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 如果确实没有变化,直接关闭弹窗
|
||||||
|
partDialogVisible.value = false
|
||||||
|
message.success('保存成功')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 批量更新
|
// 批量更新
|
||||||
await QuestionApi.batchUpdate({ questions: updates })
|
await QuestionApi.batchUpdate({ questions: updates })
|
||||||
await getList()
|
await getList()
|
||||||
@ -518,20 +528,22 @@ const savePartitions = async () => {
|
|||||||
|
|
||||||
/** 分区拖拽排序完成 */
|
/** 分区拖拽排序完成 */
|
||||||
const onPartitionDragEnd = async () => {
|
const onPartitionDragEnd = async () => {
|
||||||
// 收集所有需要更新的问题
|
// 收集所有需要更新的问题(包括默认分区)
|
||||||
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
||||||
for (let i = 0; i < partitions.value.length; i++) {
|
for (let i = 0; i < partitions.value.length; i++) {
|
||||||
const part = partitions.value[i]
|
const part = partitions.value[i]
|
||||||
if (part.name) {
|
part.questions.forEach((q, sortIndex) => {
|
||||||
part.questions.forEach((q, sortIndex) => {
|
updates.push({
|
||||||
updates.push({
|
id: q.id!,
|
||||||
id: q.id!,
|
partName: part.name || undefined,
|
||||||
partName: part.name,
|
partSort: i,
|
||||||
partSort: i,
|
sort: sortIndex
|
||||||
sort: sortIndex
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
// 空数组无需调用后端
|
||||||
|
if (updates.length === 0) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// 批量更新
|
// 批量更新
|
||||||
await QuestionApi.batchUpdate({ questions: updates })
|
await QuestionApi.batchUpdate({ questions: updates })
|
||||||
|
|||||||
@ -101,7 +101,11 @@
|
|||||||
<dict-tag :type="DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE" :value="scope.row.type" />
|
<dict-tag :type="DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE" :value="scope.row.type" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="问卷说明" align="center" prop="description" width="200" />
|
<el-table-column label="问卷说明" align="center" prop="description" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<div v-html="scope.row.description"></div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="总分" align="center" prop="totalScore" width="80" />
|
<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="passScore" width="80" />
|
||||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||||
|
|||||||
@ -1,210 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog v-model="dialogVisible" :title="title" width="1000px" :fullscreen="false">
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="`答题详情 - ${recordInfo.prisonerName || ''}`"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
<div v-loading="loading" class="answer-detail-dialog">
|
<div v-loading="loading" class="answer-detail-dialog">
|
||||||
<!-- 记录基本信息 -->
|
<!-- 记录基本信息 -->
|
||||||
<div class="record-header">
|
<el-descriptions :column="3" border class="mb-20px">
|
||||||
<div class="header-info">
|
<el-descriptions-item label="问卷名称">{{ recordInfo.questionnaireName }}</el-descriptions-item>
|
||||||
<h3 class="title">{{ recordInfo.questionnaireName }}</h3>
|
<el-descriptions-item label="罪犯编号">{{ recordInfo.prisonerNo }}</el-descriptions-item>
|
||||||
<div class="meta-info">
|
<el-descriptions-item label="完成时间">{{ formatDateTime(recordInfo.createTime) }}</el-descriptions-item>
|
||||||
<span class="info-item">
|
<el-descriptions-item label="客观分">{{ recordInfo.objectiveScore || 0 }}</el-descriptions-item>
|
||||||
<Icon icon="ep:user" />
|
<el-descriptions-item label="主观分">{{ recordInfo.subjectiveScore || 0 }}</el-descriptions-item>
|
||||||
{{ recordInfo.prisonerName || recordInfo.prisonerNo }}
|
<el-descriptions-item label="总分">{{ recordInfo.totalScore || 0 }}</el-descriptions-item>
|
||||||
</span>
|
<el-descriptions-item label="答题用时">{{ formatDuration(recordInfo.duration) }}</el-descriptions-item>
|
||||||
<span class="info-item">
|
<el-descriptions-item label="及格状态">
|
||||||
<Icon icon="ep:clock" />
|
<el-tag :type="getPassStatusTag(recordInfo.passStatus)" size="small">
|
||||||
{{ formatDateTime(recordInfo.createTime) }}
|
{{ getPassStatusText(recordInfo.passStatus) }}
|
||||||
</span>
|
</el-tag>
|
||||||
<span class="info-item" v-if="recordInfo.duration">
|
</el-descriptions-item>
|
||||||
<Icon icon="ep:timer" />
|
</el-descriptions>
|
||||||
用时:{{ formatDuration(recordInfo.duration) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="score-info">
|
|
||||||
<div class="score-item">
|
|
||||||
<span class="label">客观分</span>
|
|
||||||
<span class="value objective">{{ recordInfo.objectiveScore || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="score-item">
|
|
||||||
<span class="label">主观分</span>
|
|
||||||
<span class="value subjective">{{ recordInfo.subjectiveScore || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="score-item total">
|
|
||||||
<span class="label">总分</span>
|
|
||||||
<span class="value">{{ recordInfo.totalScore || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="score-item" :class="getPassStatusClass(recordInfo.passStatus)">
|
|
||||||
<span class="label">状态</span>
|
|
||||||
<span class="value">{{ getPassStatusText(recordInfo.passStatus) }}</span>
|
|
||||||
</div>
|
|
||||||
<el-button type="primary" link @click="handlePreviewQuestionnaire">
|
|
||||||
<Icon icon="ep:view" class="mr-3px" />查看问卷
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 问题列表(按分区显示) -->
|
<!-- 问卷题目 -->
|
||||||
<div class="questions-container">
|
<div v-if="questions.length > 0" class="questionnaire-content">
|
||||||
<template v-for="partition in partitions" :key="partition.name || 'default'">
|
<div
|
||||||
<!-- 分区标题 -->
|
v-for="(item, index) in questions"
|
||||||
<div v-if="partition.name" class="partition-title">
|
:key="item.id"
|
||||||
<Icon icon="ep:folder" />
|
class="question-item"
|
||||||
{{ partition.name }}
|
>
|
||||||
<span class="question-count">({{ partition.questions.length }} 道题)</span>
|
<div class="question-header">
|
||||||
|
<span class="question-index">{{ index + 1 }}</span>
|
||||||
|
<span class="question-title">{{ item.title }}</span>
|
||||||
|
<el-tag v-if="item.isRequired" type="danger" size="small" class="required-tag">必填</el-tag>
|
||||||
|
<el-tag v-if="item.score" type="info" size="small">{{ item.score }}分</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 问题列表 -->
|
<!-- 答案展示 -->
|
||||||
<div class="questions-list">
|
<div class="answer-area">
|
||||||
<div
|
<!-- 单选题 -->
|
||||||
v-for="item in partition.questions"
|
<div v-if="item.type === 1" class="options-container">
|
||||||
:key="item.question.id"
|
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||||
class="question-item"
|
{{ getAnswerText(item.id) }}
|
||||||
>
|
</span>
|
||||||
<!-- 问题标题 -->
|
<span v-else class="empty-answer">未作答</span>
|
||||||
<div class="question-header">
|
</div>
|
||||||
<span class="question-index">{{ item.index }}.</span>
|
|
||||||
<span class="question-title">
|
|
||||||
{{ item.question.title }}
|
|
||||||
<el-tag v-if="item.question.isRequired" type="danger" size="small" class="required-tag">
|
|
||||||
必填
|
|
||||||
</el-tag>
|
|
||||||
<el-tag v-if="item.question.score" type="info" size="small">
|
|
||||||
{{ item.question.score }}分
|
|
||||||
</el-tag>
|
|
||||||
<el-tag
|
|
||||||
v-if="item.answer"
|
|
||||||
:type="item.answer.isCorrect ? 'success' : 'danger'"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{{ item.answer.isCorrect ? '正确' : '错误' }}
|
|
||||||
</el-tag>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户答案 -->
|
<!-- 多选题 -->
|
||||||
<div class="answer-section">
|
<div v-else-if="item.type === 2" class="options-container">
|
||||||
<div class="answer-label">
|
<span v-if="getMultiAnswerText(item.id)" class="answer-text">
|
||||||
<Icon icon="ep:edit" />
|
{{ getMultiAnswerText(item.id) }}
|
||||||
用户答案:
|
</span>
|
||||||
</div>
|
<span v-else class="empty-answer">未作答</span>
|
||||||
<div class="answer-content">
|
</div>
|
||||||
<!-- 单选/多选题 -->
|
|
||||||
<template v-if="item.question.type === 1 || item.question.type === 2">
|
|
||||||
<div v-if="item.answer?.answerText" class="selected-options">
|
|
||||||
<el-tag
|
|
||||||
v-for="(opt, idx) in parseAnswerOptions(item.answer.answerText)"
|
|
||||||
:key="idx"
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
class="option-tag"
|
|
||||||
>
|
|
||||||
{{ opt }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<span v-else class="empty-answer">未作答</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 填空题 -->
|
<!-- 填空题 -->
|
||||||
<template v-else-if="item.question.type === 3">
|
<div v-else-if="item.type === 3" class="options-container">
|
||||||
<span v-if="item.answer?.answerText" class="text-answer">{{ item.answer.answerText }}</span>
|
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||||
<span v-else class="empty-answer">未作答</span>
|
{{ getAnswerText(item.id) }}
|
||||||
</template>
|
</span>
|
||||||
|
<span v-else class="empty-answer">未作答</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 评分题 -->
|
<!-- 评分题 -->
|
||||||
<template v-else-if="item.question.type === 4">
|
<div v-else-if="item.type === 4" class="options-container">
|
||||||
<span v-if="item.answer?.answerText" class="score-answer">
|
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||||
<el-tag type="warning">{{ item.answer.answerText }} 分</el-tag>
|
{{ getAnswerText(item.id) }} 分
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="empty-answer">未作答</span>
|
<span v-else class="empty-answer">未作答</span>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<!-- 日期题 -->
|
<!-- 日期题 -->
|
||||||
<template v-else-if="item.question.type === 5">
|
<div v-else-if="item.type === 5" class="options-container">
|
||||||
<span v-if="item.answer?.answerText" class="date-answer">{{ item.answer.answerText }}</span>
|
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||||
<span v-else class="empty-answer">未作答</span>
|
{{ getAnswerText(item.id) }}
|
||||||
</template>
|
</span>
|
||||||
|
<span v-else class="empty-answer">未作答</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 数字题 -->
|
<!-- 数字题 -->
|
||||||
<template v-else-if="item.question.type === 6">
|
<div v-else-if="item.type === 6" class="options-container">
|
||||||
<span v-if="item.answer?.answerText" class="number-answer">{{ item.answer.answerText }}</span>
|
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||||
<span v-else class="empty-answer">未作答</span>
|
{{ getAnswerText(item.id) }}
|
||||||
</template>
|
</span>
|
||||||
</div>
|
<span v-else class="empty-answer">未作答</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 得分信息 -->
|
|
||||||
<div v-if="item.answer?.score !== undefined" class="score-section">
|
|
||||||
<span class="score-label">
|
|
||||||
<Icon icon="ep:coin" />
|
|
||||||
得分:
|
|
||||||
</span>
|
|
||||||
<span class="score-value" :class="{ 'full-score': item.answer.score === item.question.score }">
|
|
||||||
{{ item.answer.score }} / {{ item.question.score || 0 }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 正确答案(客观题) -->
|
|
||||||
<div v-if="showCorrectAnswer(item.question, item.answer)" class="correct-answer-section">
|
|
||||||
<span class="correct-label">
|
|
||||||
<Icon icon="ep:check" />
|
|
||||||
正确答案:
|
|
||||||
</span>
|
|
||||||
<span class="correct-content">{{ getCorrectAnswer(item.question) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 答题用时 -->
|
|
||||||
<div v-if="item.answer?.duration" class="duration-section">
|
|
||||||
<Icon icon="ep:timer" />
|
|
||||||
答题用时:{{ formatDuration(item.answer.duration) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<el-empty v-else description="暂无答题记录" />
|
||||||
<el-empty v-if="partitions.length === 0 && !loading" description="暂无答题记录" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 问卷预览弹窗 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="previewVisible"
|
|
||||||
title="问卷预览"
|
|
||||||
width="800px"
|
|
||||||
destroy-on-close
|
|
||||||
>
|
|
||||||
<QuestionnairePreview :id="previewQuestionnaireId" />
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="previewVisible = false">关闭</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="dialogVisible = false">关 闭</el-button>
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { DICT_TYPE } from '@/utils/dict'
|
import { DICT_TYPE } from '@/utils/dict'
|
||||||
import { formatDateTime } from '@/utils/formatTime'
|
import { formatDateTime } from '@/utils/formatTime'
|
||||||
import { QuestionnaireRecordApi, type QuestionnaireRecord } from '@/api/prison/questionnairerecord'
|
import { QuestionnaireRecordApi, type QuestionnaireRecord } from '@/api/prison/questionnairerecord'
|
||||||
import { QuestionApi, type Question } from '@/api/prison/question'
|
import { QuestionApi, type Question } from '@/api/prison/question'
|
||||||
import { AnswerApi, type Answer } from '@/api/prison/answer'
|
import { AnswerApi, type Answer } from '@/api/prison/answer'
|
||||||
import QuestionnairePreview from '@/views/prison/questionnaire/components/QuestionnairePreview.vue'
|
|
||||||
import { getIntDictOptions } from '@/utils/dict'
|
import { getIntDictOptions } from '@/utils/dict'
|
||||||
|
|
||||||
defineOptions({ name: 'AnswerDetailDialog' })
|
defineOptions({ name: 'AnswerDetailDialog' })
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const title = ref('答题详情')
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
// 预览相关
|
|
||||||
const previewVisible = ref(false)
|
|
||||||
const previewQuestionnaireId = ref<number>()
|
|
||||||
|
|
||||||
// 记录信息
|
// 记录信息
|
||||||
const recordInfo = ref<QuestionnaireRecord>({
|
const recordInfo = ref<QuestionnaireRecord>({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
@ -225,69 +133,19 @@ const questions = ref<Question[]>([])
|
|||||||
// 答案列表
|
// 答案列表
|
||||||
const answers = ref<Answer[]>([])
|
const answers = ref<Answer[]>([])
|
||||||
|
|
||||||
// 分区列表(带答案)
|
|
||||||
interface QuestionWithAnswer {
|
|
||||||
question: Question
|
|
||||||
answer?: Answer
|
|
||||||
index: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const partitions = computed(() => {
|
|
||||||
const partMap = new Map<string, QuestionWithAnswer[]>()
|
|
||||||
|
|
||||||
questions.value.forEach((q, index) => {
|
|
||||||
const partName = q.partName || ''
|
|
||||||
const answer = answers.value.find(a => a.questionId === q.id)
|
|
||||||
|
|
||||||
if (!partMap.has(partName)) {
|
|
||||||
partMap.set(partName, [])
|
|
||||||
}
|
|
||||||
partMap.get(partName)!.push({
|
|
||||||
question: q,
|
|
||||||
answer,
|
|
||||||
index: index + 1
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 按分区排序
|
|
||||||
const sortedParts = Array.from(partMap.entries())
|
|
||||||
.sort((a, b) => {
|
|
||||||
const sortA = a[1][0]?.question.partSort ?? 0
|
|
||||||
const sortB = b[1][0]?.question.partSort ?? 0
|
|
||||||
return sortA - sortB
|
|
||||||
})
|
|
||||||
|
|
||||||
// 构建分区列表
|
|
||||||
const result: Array<{ name: string; questions: QuestionWithAnswer[] }> = []
|
|
||||||
|
|
||||||
// 添加默认分区
|
|
||||||
const defaultQuestions = sortedParts.find(([name]) => !name)
|
|
||||||
if (defaultQuestions) {
|
|
||||||
result.push({
|
|
||||||
name: '',
|
|
||||||
questions: defaultQuestions[1]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加其他分区
|
|
||||||
sortedParts
|
|
||||||
.filter(([name]) => name)
|
|
||||||
.forEach(([name, qs]) => {
|
|
||||||
result.push({
|
|
||||||
name,
|
|
||||||
questions: qs
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 格式化时长 */
|
/** 格式化时长 */
|
||||||
const formatDuration = (seconds: number): string => {
|
const formatDuration = (seconds: number | undefined): string => {
|
||||||
if (!seconds) return '-'
|
if (!seconds) return '-'
|
||||||
const minutes = Math.floor(seconds / 60)
|
const hours = Math.floor(seconds / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
const secs = seconds % 60
|
const secs = seconds % 60
|
||||||
return minutes > 0 ? `${minutes}分${secs}秒` : `${secs}秒`
|
if (hours > 0) {
|
||||||
|
return `${hours}时${minutes}分${secs}秒`
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}分${secs}秒`
|
||||||
|
} else {
|
||||||
|
return `${secs}秒`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取及格状态文本 */
|
/** 获取及格状态文本 */
|
||||||
@ -296,52 +154,55 @@ const getPassStatusText = (status: number | undefined) => {
|
|||||||
return options.find(o => o.value === status)?.label || '-'
|
return options.find(o => o.value === status)?.label || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取及格状态样式 */
|
/** 获取及格状态标签类型 */
|
||||||
const getPassStatusClass = (status: number | undefined) => {
|
const getPassStatusTag = (status: number | undefined): 'success' | 'danger' | 'warning' | 'info' => {
|
||||||
const classMap: Record<number, string> = {
|
const tagMap: Record<number, 'success' | 'danger' | 'warning' | 'info'> = {
|
||||||
1: 'pass',
|
1: 'success',
|
||||||
2: 'fail',
|
2: 'danger',
|
||||||
3: 'pending'
|
3: 'warning'
|
||||||
}
|
}
|
||||||
return classMap[status || 0] || ''
|
return tagMap[status || 0] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析答案选项(单选/多选) */
|
/** 获取单选/填空/评分/日期/数字题的答案文本 */
|
||||||
const parseAnswerOptions = (answerText: string): string[] => {
|
const getAnswerText = (questionId: number | undefined): string | null => {
|
||||||
if (!answerText) return []
|
if (questionId === undefined) return null
|
||||||
// 假设答案用逗号分隔(多选)或单个值(单选)
|
const answer = answers.value.find(a => a.questionId === questionId)
|
||||||
return answerText.split(',').filter(s => s.trim())
|
return answer?.answerText || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断是否显示正确答案 */
|
/** 获取多选题的答案文本 */
|
||||||
const showCorrectAnswer = (question: Question, answer?: Answer): boolean => {
|
const getMultiAnswerText = (questionId: number | undefined): string | null => {
|
||||||
// 仅客观题显示正确答案
|
if (questionId === undefined) return null
|
||||||
if (question.type !== 1 && question.type !== 2) return false
|
const answer = answers.value.find(a => a.questionId === questionId)
|
||||||
// 必须有答案记录
|
if (!answer?.optionIds || !Array.isArray(answer.optionIds) || answer.optionIds.length === 0) {
|
||||||
if (!answer) return false
|
return null
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
|
// 找到对应的问题
|
||||||
|
const question = questions.value.find(q => q.id === questionId)
|
||||||
|
if (!question?.options) return null
|
||||||
|
|
||||||
/** 获取正确答案 */
|
|
||||||
const getCorrectAnswer = (question: Question): string => {
|
|
||||||
if (!question.options) return '-'
|
|
||||||
try {
|
try {
|
||||||
const options = JSON.parse(question.options)
|
// 解析选项
|
||||||
// 找到分值最高的选项作为正确答案
|
const options = JSON.parse(question.options) as Array<{ label: string; value?: string; isOther?: boolean }>
|
||||||
const correctOptions = options
|
|
||||||
.filter((o: any) => o.score > 0)
|
|
||||||
.map((o: any) => o.label)
|
|
||||||
|
|
||||||
return correctOptions.length > 0 ? correctOptions.join('、') : '-'
|
// 根据 optionIds(索引数组)获取对应的选项文字
|
||||||
} catch {
|
const selectedLabels = answer.optionIds
|
||||||
return '-'
|
.map((idx: number) => options[idx])
|
||||||
|
.filter((opt): opt is { label: string } => !!opt)
|
||||||
|
.map(opt => opt.label)
|
||||||
|
|
||||||
|
return selectedLabels.length > 0 ? selectedLabels.join('、') : null
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析选项失败:', e)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
const open = async (recordId: number) => {
|
const open = async (recordId: number) => {
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
title.value = '答题详情'
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -370,296 +231,89 @@ const open = async (recordId: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 预览问卷 */
|
|
||||||
const handlePreviewQuestionnaire = () => {
|
|
||||||
if (!recordInfo.value.questionnaireId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
previewQuestionnaireId.value = recordInfo.value.questionnaireId
|
|
||||||
previewVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open })
|
defineExpose({ open })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
.answer-detail-dialog {
|
.answer-detail-dialog {
|
||||||
max-height: 70vh;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 20px;
|
max-height: 55vh;
|
||||||
background: #f5f7fa;
|
padding-right: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.record-header {
|
&::-webkit-scrollbar {
|
||||||
background: #fff;
|
width: 8px;
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
.header-info {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-info {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
color: #606266;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-info {
|
&::-webkit-scrollbar-thumb {
|
||||||
display: flex;
|
background-color: #e4e7ed;
|
||||||
gap: 16px;
|
border-radius: 4px;
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
|
|
||||||
.score-item {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
display: block;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
|
|
||||||
&.objective {
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.subjective {
|
|
||||||
color: #e6a23c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.total {
|
|
||||||
.value {
|
|
||||||
color: #409eff;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.pass .value {
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fail .value {
|
|
||||||
color: #f56c6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.pending .value {
|
|
||||||
color: #e6a23c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.questions-container {
|
.questionnaire-content {
|
||||||
.partition-title {
|
overflow-y: auto;
|
||||||
background: linear-gradient(135deg, #e8f4fd 0%, #f0f7ff 100%);
|
max-height: 50vh;
|
||||||
padding: 12px 20px;
|
padding-right: 8px;
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 16px;
|
&::-webkit-scrollbar {
|
||||||
margin-top: 24px;
|
width: 8px;
|
||||||
font-weight: 600;
|
}
|
||||||
color: #1a5cb8;
|
|
||||||
font-size: 16px;
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.question-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
&:first-child {
|
.question-index {
|
||||||
margin-top: 0;
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-count {
|
.question-title {
|
||||||
font-size: 12px;
|
font-size: 15px;
|
||||||
color: #909399;
|
font-weight: 500;
|
||||||
font-weight: normal;
|
color: #303133;
|
||||||
margin-left: 4px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.questions-list {
|
.answer-area {
|
||||||
display: flex;
|
padding-left: 32px;
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-item {
|
.options-container {
|
||||||
background: #fff;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
.question-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
flex-direction: column;
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.question-index {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
background: #409eff;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-title {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #303133;
|
|
||||||
line-height: 1.6;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.answer-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 16px;
|
|
||||||
background: #f4f4f5;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.answer-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #606266;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.answer-content {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.selected-options {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.option-tag {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-answer,
|
|
||||||
.date-answer,
|
|
||||||
.number-answer {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #303133;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-answer {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-answer {
|
|
||||||
color: #c0c4cc;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-section {
|
.answer-text {
|
||||||
display: flex;
|
font-size: 14px;
|
||||||
align-items: center;
|
color: #303133;
|
||||||
gap: 8px;
|
line-height: 1.6;
|
||||||
padding: 8px 16px;
|
|
||||||
background: #ecf5ff;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.score-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #409eff;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-value {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #409eff;
|
|
||||||
|
|
||||||
&.full-score {
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.correct-answer-section {
|
.empty-answer {
|
||||||
display: flex;
|
color: #c0c4cc;
|
||||||
align-items: center;
|
font-size: 14px;
|
||||||
gap: 8px;
|
font-style: italic;
|
||||||
padding: 8px 16px;
|
|
||||||
background: #f0f9eb;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.correct-label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #67c23a;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.correct-content {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #67c23a;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -283,12 +283,60 @@ const handleSubmit = async () => {
|
|||||||
// 构建答案列表
|
// 构建答案列表
|
||||||
const answerList = questions.value.map(q => {
|
const answerList = questions.value.map(q => {
|
||||||
const answer = answers.value[q.id!]
|
const answer = answers.value[q.id!]
|
||||||
// 多选时用逗号分隔
|
|
||||||
const answerStr = Array.isArray(answer) ? answer.join(',') : answer
|
// 处理多选题(type === 2)
|
||||||
|
if (q.type === 2 && Array.isArray(answer)) {
|
||||||
|
// 解析选项获取文字
|
||||||
|
let optionLabels: string[] = []
|
||||||
|
try {
|
||||||
|
const options = JSON.parse(q.options || '[]') as Array<{ label: string }>
|
||||||
|
optionLabels = answer
|
||||||
|
.map((idx: string) => options[parseInt(idx)]?.label)
|
||||||
|
.filter((label): label is string => !!label)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析选项失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
questionId: q.id!,
|
||||||
|
answer: optionLabels.join('、') || '',
|
||||||
|
optionIds: answer.map((idx: string) => parseInt(idx, 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单选题(type === 1):如果选择了"其他"选项,需要发送 optionIds
|
||||||
|
if (q.type === 1 && answer) {
|
||||||
|
const answerIndex = parseInt(answer as string, 10)
|
||||||
|
let isOtherOption = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = JSON.parse(q.options || '[]') as Array<{ isOther?: boolean }>
|
||||||
|
isOtherOption = options[answerIndex]?.isOther === true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析选项失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析选项获取文字
|
||||||
|
let answerText = answer as string
|
||||||
|
try {
|
||||||
|
const options = JSON.parse(q.options || '[]') as Array<{ label: string }>
|
||||||
|
answerText = options[answerIndex]?.label || answer as string
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析选项失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
questionId: q.id!,
|
||||||
|
answer: answerText,
|
||||||
|
optionIds: isOtherOption ? [answerIndex] : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填空题、评分题、日期题、数字题(type 3/4/5/6)
|
||||||
return {
|
return {
|
||||||
questionId: q.id!,
|
questionId: q.id!,
|
||||||
answer: answerStr || '',
|
answer: String(answer || ''),
|
||||||
optionIds: undefined as number[] | undefined
|
optionIds: undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog style="display: none;" title="问卷预览" v-model="dialogVisible" width="900px" :fullscreen="false">
|
<Dialog
|
||||||
|
style="display: none"
|
||||||
|
title="问卷预览"
|
||||||
|
v-model="dialogVisible"
|
||||||
|
width="900px"
|
||||||
|
:fullscreen="false"
|
||||||
|
>
|
||||||
<div ref="previewRef" class="questionnaire-preview" v-loading="loading">
|
<div ref="previewRef" class="questionnaire-preview" v-loading="loading">
|
||||||
<!-- 问卷头部信息 -->
|
<!-- 问卷头部信息 -->
|
||||||
<h1 class="preview-header">{{ recordInfo?.questionnaireName }}</h1>
|
<h1 class="preview-header">{{ recordInfo?.questionnaireName }}</h1>
|
||||||
@ -18,22 +24,23 @@
|
|||||||
|
|
||||||
<!-- 问题列表(按分区显示) -->
|
<!-- 问题列表(按分区显示) -->
|
||||||
<div class="preview-questions">
|
<div class="preview-questions">
|
||||||
<template v-for="partition in partitions" :key="partition.name || 'default'">
|
<template v-for="(partition, index) in partitions" :key="partition.name || 'default'">
|
||||||
<!-- 分区标题 -->
|
<!-- 分区标题 -->
|
||||||
<div v-if="partition.name" class="partition-title">
|
<div v-if="partition.name" class="partition-title">
|
||||||
|
{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}、
|
||||||
{{ partition.name }}
|
{{ partition.name }}
|
||||||
<span class="question-count">({{ partition.questions.length }} 道题)</span>
|
<span class="question-count">({{ partition.questions.length }} 道题)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 问题列表 -->
|
<!-- 问题列表 -->
|
||||||
<div class="question-items">
|
<div class="question-items">
|
||||||
<div
|
<div
|
||||||
v-for="(questionWithAnswer, index) in partition.questions"
|
v-for="(questionWithAnswer, index) in partition.questions"
|
||||||
:key="questionWithAnswer.question.id"
|
:key="questionWithAnswer.question.id"
|
||||||
class="question-item"
|
class="question-item"
|
||||||
>
|
>
|
||||||
<span class="question-index">{{ index + 1 }}.</span>
|
<span class="question-index">{{ index + 1 }}.</span>
|
||||||
<span class="question-title">{{ questionWithAnswer.question.title }}</span>
|
<span class="question-title">{{ questionWithAnswer.question.title }}:</span>
|
||||||
|
|
||||||
<!-- 帮助说明 -->
|
<!-- 帮助说明 -->
|
||||||
<span v-if="questionWithAnswer.question.helpText" class="question-help-inline">
|
<span v-if="questionWithAnswer.question.helpText" class="question-help-inline">
|
||||||
@ -41,25 +48,50 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 单选/多选题 -->
|
<!-- 单选/多选题 -->
|
||||||
<span v-if="questionWithAnswer.question.type === 1 || questionWithAnswer.question.type === 2" class="question-options-inline">
|
<span
|
||||||
|
v-if="
|
||||||
|
questionWithAnswer.question.type === 1 || questionWithAnswer.question.type === 2
|
||||||
|
"
|
||||||
|
class="question-options-inline"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-for="option in getQuestionOptions(questionWithAnswer.question)"
|
v-for="option in getQuestionOptions(questionWithAnswer.question)"
|
||||||
:key="option.label"
|
:key="option.label"
|
||||||
class="option-item"
|
class="option-item"
|
||||||
>
|
>
|
||||||
<span v-if="questionWithAnswer.question.type === 1 && questionWithAnswer.answer?.answerText?.trim() === option.label">
|
<span
|
||||||
|
v-if="
|
||||||
|
questionWithAnswer.question.type === 1 &&
|
||||||
|
questionWithAnswer.answer?.answerText?.trim() === option.label
|
||||||
|
"
|
||||||
|
>
|
||||||
☑
|
☑
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="questionWithAnswer.question.type === 1 && questionWithAnswer.answer?.answerText?.trim() !== option.label">
|
<span
|
||||||
|
v-if="
|
||||||
|
questionWithAnswer.question.type === 1 &&
|
||||||
|
questionWithAnswer.answer?.answerText?.trim() !== option.label
|
||||||
|
"
|
||||||
|
>
|
||||||
☐
|
☐
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="questionWithAnswer.question.type === 2 && getSelectedLabels(questionWithAnswer.answer).includes(option.label)">
|
<span
|
||||||
|
v-if="
|
||||||
|
questionWithAnswer.question.type === 2 &&
|
||||||
|
getSelectedLabels(questionWithAnswer.answer).includes(option.label)
|
||||||
|
"
|
||||||
|
>
|
||||||
☑
|
☑
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="questionWithAnswer.question.type === 2 && !getSelectedLabels(questionWithAnswer.answer).includes(option.label)">
|
<span
|
||||||
|
v-if="
|
||||||
|
questionWithAnswer.question.type === 2 &&
|
||||||
|
!getSelectedLabels(questionWithAnswer.answer).includes(option.label)
|
||||||
|
"
|
||||||
|
>
|
||||||
☐
|
☐
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</span>
|
</span>
|
||||||
@ -67,26 +99,48 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 填空题 -->
|
<!-- 填空题 -->
|
||||||
<span v-else-if="questionWithAnswer.question.type === 3" class="question-input-inline">
|
<span
|
||||||
|
v-else-if="questionWithAnswer.question.type === 3"
|
||||||
|
class="question-input-inline"
|
||||||
|
>
|
||||||
{{ getAnswerDisplayValue(questionWithAnswer.answer) }}
|
{{ getAnswerDisplayValue(questionWithAnswer.answer) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 评分题 -->
|
<!-- 评分题 -->
|
||||||
<span v-else-if="questionWithAnswer.question.type === 4" class="question-rating-inline">
|
<span
|
||||||
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
|
v-else-if="questionWithAnswer.question.type === 4"
|
||||||
|
class="question-rating-inline"
|
||||||
|
>
|
||||||
|
<span class="question-input-inline">{{
|
||||||
|
getAnswerDisplayValue(questionWithAnswer.answer)
|
||||||
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 日期题 -->
|
<!-- 日期题 -->
|
||||||
<span v-else-if="questionWithAnswer.question.type === 5" class="question-date-inline">
|
<span v-else-if="questionWithAnswer.question.type === 5" class="question-date-inline">
|
||||||
<span class="date-info" v-if="getRangeValue(questionWithAnswer.question, 'min') || getRangeValue(questionWithAnswer.question, 'max')">
|
<span
|
||||||
日期范围:{{ getRangeValue(questionWithAnswer.question, 'min') || '无限制' }} ~ {{ getRangeValue(questionWithAnswer.question, 'max') || '无限制' }}
|
class="date-info"
|
||||||
|
v-if="
|
||||||
|
getRangeValue(questionWithAnswer.question, 'min') ||
|
||||||
|
getRangeValue(questionWithAnswer.question, 'max')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
日期范围:{{ getRangeValue(questionWithAnswer.question, 'min') || '无限制' }} ~
|
||||||
|
{{ getRangeValue(questionWithAnswer.question, 'max') || '无限制' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
|
<span class="question-input-inline">{{
|
||||||
|
getAnswerDisplayValue(questionWithAnswer.answer)
|
||||||
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 数字题 -->
|
<!-- 数字题 -->
|
||||||
<span v-else-if="questionWithAnswer.question.type === 6" class="question-number-inline">
|
<span
|
||||||
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
|
v-else-if="questionWithAnswer.question.type === 6"
|
||||||
|
class="question-number-inline"
|
||||||
|
>
|
||||||
|
<span class="question-input-inline">{{
|
||||||
|
getAnswerDisplayValue(questionWithAnswer.answer)
|
||||||
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -137,7 +191,7 @@ const partitions = computed(() => {
|
|||||||
|
|
||||||
questions.value.forEach((q, index) => {
|
questions.value.forEach((q, index) => {
|
||||||
const partName = q.partName || ''
|
const partName = q.partName || ''
|
||||||
const answer = answers.value.find(a => a.questionId === q.id)
|
const answer = answers.value.find((a) => a.questionId === q.id)
|
||||||
|
|
||||||
if (!partMap.has(partName)) {
|
if (!partMap.has(partName)) {
|
||||||
partMap.set(partName, [])
|
partMap.set(partName, [])
|
||||||
@ -150,12 +204,11 @@ const partitions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 按分区排序
|
// 按分区排序
|
||||||
const sortedParts = Array.from(partMap.entries())
|
const sortedParts = Array.from(partMap.entries()).sort((a, b) => {
|
||||||
.sort((a, b) => {
|
const sortA = a[1][0]?.question.partSort ?? 0
|
||||||
const sortA = a[1][0]?.question.partSort ?? 0
|
const sortB = b[1][0]?.question.partSort ?? 0
|
||||||
const sortB = b[1][0]?.question.partSort ?? 0
|
return sortA - sortB
|
||||||
return sortA - sortB
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// 构建分区列表
|
// 构建分区列表
|
||||||
const result: Array<{ name: string; questions: QuestionWithAnswer[] }> = []
|
const result: Array<{ name: string; questions: QuestionWithAnswer[] }> = []
|
||||||
@ -184,20 +237,21 @@ const partitions = computed(() => {
|
|||||||
|
|
||||||
/** 根据问题ID获取答案 */
|
/** 根据问题ID获取答案 */
|
||||||
const getAnswerByQuestionId = (questionId: number): Answer | undefined => {
|
const getAnswerByQuestionId = (questionId: number): Answer | undefined => {
|
||||||
return answers.value.find(a => a.questionId === questionId)
|
return answers.value.find((a) => a.questionId === questionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取答案显示值(兼容不同字段名) */
|
/** 获取答案显示值(兼容不同字段名) */
|
||||||
const getAnswerDisplayValue = (answer?: Answer): string => {
|
const getAnswerDisplayValue = (answer?: Answer): string => {
|
||||||
if (!answer) return ''
|
if (!answer) return ''
|
||||||
// 优先使用 answerText,其次使用 optionIds
|
// 优先使用 answerText,其次使用 optionIds,并去除前后空格和特殊字符
|
||||||
return answer.answerText || answer.optionIds || '-'
|
const value = answer.answerText || answer.optionIds || ''
|
||||||
|
return String(value).trim().replace(/^["']|["']$/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 问卷类型标签 */
|
/** 问卷类型标签 */
|
||||||
const getTypeLabel = (type: number) => {
|
const getTypeLabel = (type: number) => {
|
||||||
const options = getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE)
|
const options = getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE)
|
||||||
return options.find(o => o.value === type)?.label || '未知'
|
return options.find((o) => o.value === type)?.label || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取问题选项 */
|
/** 获取问题选项 */
|
||||||
@ -233,11 +287,16 @@ const getRangeValue = (question: Question, key: 'min' | 'max') => {
|
|||||||
/** 安全获取多选答案的标签数组 */
|
/** 安全获取多选答案的标签数组 */
|
||||||
const getSelectedLabels = (answer?: Answer): string[] => {
|
const getSelectedLabels = (answer?: Answer): string[] => {
|
||||||
if (!answer?.answerText) return []
|
if (!answer?.answerText) return []
|
||||||
return answer.answerText.split(',').map(s => s.trim())
|
return answer.answerText.split(',').map((s) => s.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断选项是否被选中(支持单选和多选)- 已废弃,改用模板直接绑定 */
|
/** 判断选项是否被选中(支持单选和多选)- 已废弃,改用模板直接绑定 */
|
||||||
const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: string, questionType?: number): boolean => {
|
const isOptionSelected = (
|
||||||
|
answer?: Answer,
|
||||||
|
optionValue?: string,
|
||||||
|
optionLabel?: string,
|
||||||
|
questionType?: number
|
||||||
|
): boolean => {
|
||||||
if (!answer || !optionValue || !optionLabel) {
|
if (!answer || !optionValue || !optionLabel) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -245,7 +304,7 @@ const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: s
|
|||||||
// 多选(type === 2):answerText 是逗号分隔的标签列表
|
// 多选(type === 2):answerText 是逗号分隔的标签列表
|
||||||
if (questionType === 2 && answer.answerText && answer.answerText.includes(',')) {
|
if (questionType === 2 && answer.answerText && answer.answerText.includes(',')) {
|
||||||
const answerText = answer.answerText
|
const answerText = answer.answerText
|
||||||
const selectedLabels = answerText.split(',').map(s => s.trim())
|
const selectedLabels = answerText.split(',').map((s) => s.trim())
|
||||||
return selectedLabels.includes(optionLabel)
|
return selectedLabels.includes(optionLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +316,7 @@ const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: s
|
|||||||
// 兼容:没有 questionType 时,根据是否有逗号判断
|
// 兼容:没有 questionType 时,根据是否有逗号判断
|
||||||
if (answer.answerText && answer.answerText.includes(',')) {
|
if (answer.answerText && answer.answerText.includes(',')) {
|
||||||
const answerText = answer.answerText
|
const answerText = answer.answerText
|
||||||
const selectedLabels = answerText.split(',').map(s => s.trim())
|
const selectedLabels = answerText.split(',').map((s) => s.trim())
|
||||||
return selectedLabels.includes(optionLabel)
|
return selectedLabels.includes(optionLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +325,6 @@ const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: s
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
const open = async (recordId: number) => {
|
const open = async (recordId: number) => {
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@ -296,7 +354,9 @@ const open = async (recordId: number) => {
|
|||||||
if (recordData.questionnaireId) {
|
if (recordData.questionnaireId) {
|
||||||
try {
|
try {
|
||||||
const { QuestionnaireApi } = await import('@/api/prison/questionnaire')
|
const { QuestionnaireApi } = await import('@/api/prison/questionnaire')
|
||||||
questionnaireInfo.value = await QuestionnaireApi.getQuestionnaire(recordData.questionnaireId)
|
questionnaireInfo.value = await QuestionnaireApi.getQuestionnaire(
|
||||||
|
recordData.questionnaireId
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('加载问卷详细信息失败:', error)
|
console.warn('加载问卷详细信息失败:', error)
|
||||||
}
|
}
|
||||||
@ -327,7 +387,11 @@ const exportToWord = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取预览容器的HTML内容
|
// 获取预览容器的HTML内容
|
||||||
const previewHTML = previewRef.value.innerHTML
|
let previewHTML = previewRef.value.innerHTML
|
||||||
|
|
||||||
|
// 清理可能导致WPS显示引号的特殊字符
|
||||||
|
previewHTML = previewHTML.replace(/[\u201C\u201D\u2018\u2019]/g, '') // 移除中文引号
|
||||||
|
previewHTML = previewHTML.replace(/^["']|["']$/gm, '') // 移除行首尾的引号
|
||||||
|
|
||||||
// 构建完整的HTML文档,使用与预览页面一致的样式
|
// 构建完整的HTML文档,使用与预览页面一致的样式
|
||||||
const fullHTML = `
|
const fullHTML = `
|
||||||
@ -336,108 +400,110 @@ const exportToWord = async () => {
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>${recordInfo.value?.questionnaireName || '问卷'}</title>
|
<title>${recordInfo.value?.questionnaireName || '问卷'}</title>
|
||||||
<style>
|
<style>
|
||||||
|
/* 使用 pt 单位以避免 Word/WPS 在 px->pt 换算时产生差异 */
|
||||||
body {
|
body {
|
||||||
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 11pt;
|
||||||
line-height: 1.6;
|
margin: 15pt;
|
||||||
margin: 20px;
|
padding: 15pt;
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
|
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||||
|
font-size: 18pt;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #000;
|
color: #000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.section-title {
|
.section-title {
|
||||||
margin: 15px 0;
|
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||||
font-size: 18px;
|
margin-bottom: 15pt;
|
||||||
font-weight: 500;
|
font-size: 18pt;
|
||||||
|
font-weight: 700;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
.description-content {
|
.description-content {
|
||||||
color: #000;
|
color: #000;
|
||||||
line-height: 1.8;
|
font-size: 11pt;
|
||||||
font-size: 16px;
|
margin-bottom: 6pt;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.instruction-content {
|
.instruction-content {
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 18px;
|
font-size: 11pt;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.8;
|
margin-bottom: 6pt;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.partition-title {
|
.partition-title {
|
||||||
margin-bottom: 16px;
|
margin-top: 15pt;
|
||||||
margin-top: 24px;
|
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 17px;
|
font-size: 15pt;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.partition-title:first-child {
|
.partition-title:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.question-count {
|
.question-count {
|
||||||
font-size: 14px;
|
font-size: 11pt;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-left: 4px;
|
margin-left: 2pt;
|
||||||
}
|
}
|
||||||
.question-item {
|
.question-item {
|
||||||
font-size: 16px;
|
font-size: 11pt;
|
||||||
color: #000;
|
color: #000;
|
||||||
line-height: 1.6;
|
margin-bottom: 8pt;
|
||||||
margin-bottom: 16px;
|
line-height: 1.5;
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
.question-index {
|
.question-index {
|
||||||
margin-right: 4px;
|
margin-right: 2pt;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
.question-title {
|
.question-title {
|
||||||
margin-right: 8px;
|
margin-right: 4pt;
|
||||||
}
|
}
|
||||||
.question-help-inline {
|
.question-help-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 3pt;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 15px;
|
font-size: 11pt;
|
||||||
padding: 4px 8px;
|
|
||||||
background: #f4f4f5;
|
background: #f4f4f5;
|
||||||
border-radius: 4px;
|
border-radius: 2pt;
|
||||||
margin-right: 8px;
|
margin-right: 6pt;
|
||||||
}
|
}
|
||||||
.question-options-inline {
|
.question-options-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 8pt;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.option-item {
|
.option-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 3pt;
|
||||||
}
|
}
|
||||||
.question-rating-inline,
|
.question-rating-inline,
|
||||||
.question-date-inline,
|
.question-date-inline,
|
||||||
.question-number-inline {
|
.question-number-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8pt;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-left: 8px;
|
margin-left: 3pt;
|
||||||
}
|
}
|
||||||
.rating-info,
|
.rating-info,
|
||||||
.date-info,
|
.date-info,
|
||||||
.number-info {
|
.number-info {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 16px;
|
gap: 11pt;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 13px;
|
font-size: 11pt;
|
||||||
}
|
}
|
||||||
.question-input-inline {
|
.question-input-inline {
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 16px;
|
font-size: 11pt;
|
||||||
line-height: 2.2;
|
line-height: 1.5;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -475,100 +541,99 @@ defineExpose({ open, exportToWord })
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||||
|
font-size: 18pt;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #000;
|
color: #000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.section-title {
|
.section-title {
|
||||||
margin: 15px 0;
|
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||||
font-size: 16px;
|
margin-bottom: 15pt;
|
||||||
font-weight: 500;
|
font-size: 18pt;
|
||||||
|
font-weight: 700;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
.description-content {
|
.description-content {
|
||||||
color: #000;
|
color: #000;
|
||||||
line-height: 1.8;
|
line-height: 3;
|
||||||
font-size: 14px;
|
font-size: 11pt;
|
||||||
}
|
}
|
||||||
.instruction-content {
|
.instruction-content {
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 16px;
|
font-size: 11pt;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.8;
|
line-height: 3;
|
||||||
}
|
}
|
||||||
.partition-title {
|
.partition-title {
|
||||||
margin-bottom: 16px;
|
margin-top: 15pt;
|
||||||
margin-top: 24px;
|
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 15px;
|
font-size: 15pt;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.partition-title:first-child {
|
.partition-title:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.question-count {
|
.question-count {
|
||||||
font-size: 12px;
|
font-size: 11pt;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-left: 4px;
|
margin-left: 2pt;
|
||||||
}
|
}
|
||||||
.question-item {
|
.question-item {
|
||||||
font-size: 14px;
|
font-size: 11pt;
|
||||||
color: #000;
|
color: #000;
|
||||||
line-height: 1.6;
|
line-height: 3;
|
||||||
margin-bottom: 16px;
|
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
.question-index {
|
.question-index {
|
||||||
margin-right: 4px;
|
margin-right: 2pt;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
.question-title {
|
.question-title {
|
||||||
margin-right: 8px;
|
margin-right: 4pt;
|
||||||
}
|
}
|
||||||
.question-help-inline {
|
.question-help-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 3pt;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 13px;
|
font-size: 11pt;
|
||||||
padding: 4px 8px;
|
|
||||||
background: #f4f4f5;
|
background: #f4f4f5;
|
||||||
border-radius: 4px;
|
border-radius: 2pt;
|
||||||
margin-right: 8px;
|
margin-right: 6pt;
|
||||||
}
|
}
|
||||||
.question-options-inline {
|
.question-options-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 8pt;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.option-item {
|
.option-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 3pt;
|
||||||
}
|
}
|
||||||
.question-rating-inline,
|
.question-rating-inline,
|
||||||
.question-date-inline,
|
.question-date-inline,
|
||||||
.question-number-inline {
|
.question-number-inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8pt;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-left: 8px;
|
margin-left: 3pt;
|
||||||
}
|
}
|
||||||
.rating-info,
|
.rating-info,
|
||||||
.date-info,
|
.date-info,
|
||||||
.number-info {
|
.number-info {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 16px;
|
gap: 11pt;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 13px;
|
font-size: 11pt;
|
||||||
}
|
}
|
||||||
.question-input-inline {
|
.question-input-inline {
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 14px;
|
font-size: 11pt;
|
||||||
line-height: 2.2;
|
line-height: 3;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="formData.assessmentDate"
|
v-model="formData.assessmentDate"
|
||||||
type="date"
|
type="date"
|
||||||
value-format="x"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="选择评估日期"
|
placeholder="选择评估日期"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -72,17 +72,12 @@
|
|||||||
<el-form-item label="管控建议" prop="suggestions">
|
<el-form-item label="管控建议" prop="suggestions">
|
||||||
<el-input v-model="formData.suggestions" placeholder="请输入管控建议" />
|
<el-input v-model="formData.suggestions" placeholder="请输入管控建议" />
|
||||||
</el-form-item>
|
</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-form-item label="下次评估日期" prop="nextAssessmentDate">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="formData.nextAssessmentDate"
|
v-model="formData.nextAssessmentDate"
|
||||||
type="date"
|
type="date"
|
||||||
value-format="x"
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="选择下次评估日期"
|
placeholder="选择下次评估日期"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -129,8 +124,9 @@ const formData = ref({
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
prisonerId: undefined as number | undefined,
|
prisonerId: undefined as number | undefined,
|
||||||
prisonerNo: undefined as string | undefined,
|
prisonerNo: undefined as string | undefined,
|
||||||
|
prisonerName: undefined as string | undefined, // 罪犯姓名(用于显示)
|
||||||
assessmentType: undefined as number | undefined,
|
assessmentType: undefined as number | undefined,
|
||||||
assessmentDate: undefined as number | undefined,
|
assessmentDate: undefined as string | undefined,
|
||||||
violenceScore: undefined as number | undefined,
|
violenceScore: undefined as number | undefined,
|
||||||
escapeScore: undefined as number | undefined,
|
escapeScore: undefined as number | undefined,
|
||||||
suicideScore: undefined as number | undefined,
|
suicideScore: undefined as number | undefined,
|
||||||
@ -138,9 +134,8 @@ const formData = ref({
|
|||||||
riskLevel: undefined as number | undefined,
|
riskLevel: undefined as number | undefined,
|
||||||
riskFactors: undefined as string | undefined,
|
riskFactors: undefined as string | undefined,
|
||||||
suggestions: undefined as string | undefined,
|
suggestions: undefined as string | undefined,
|
||||||
assessorId: undefined as number | undefined,
|
// assessorId 和 assessorName 由后端自动从登录上下文获取,不从前端传递
|
||||||
assessorName: undefined as string | undefined,
|
nextAssessmentDate: undefined as string | undefined,
|
||||||
nextAssessmentDate: undefined as number | undefined,
|
|
||||||
status: 1 as number | undefined,
|
status: 1 as number | undefined,
|
||||||
remark: undefined as string | undefined
|
remark: undefined as string | undefined
|
||||||
})
|
})
|
||||||
@ -153,7 +148,7 @@ const formRules = reactive({
|
|||||||
})
|
})
|
||||||
const formRef = ref() // 表单 Ref
|
const formRef = ref() // 表单 Ref
|
||||||
|
|
||||||
/** 搜索罪犯 */
|
/** 搜索罪犯 - 支持编号和姓名双条件搜索 */
|
||||||
const searchPrisoner = async (query: string) => {
|
const searchPrisoner = async (query: string) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
prisonerList.value = []
|
prisonerList.value = []
|
||||||
@ -164,6 +159,7 @@ const searchPrisoner = async (query: string) => {
|
|||||||
const data = await PrisonerApi.getPage({
|
const data = await PrisonerApi.getPage({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
prisonerNo: query, // 按罪犯编号搜索
|
||||||
name: query // 按姓名搜索
|
name: query // 按姓名搜索
|
||||||
} as any)
|
} as any)
|
||||||
prisonerList.value = data.list || []
|
prisonerList.value = data.list || []
|
||||||
@ -236,6 +232,7 @@ const resetForm = () => {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
prisonerId: undefined,
|
prisonerId: undefined,
|
||||||
prisonerNo: undefined,
|
prisonerNo: undefined,
|
||||||
|
prisonerName: undefined,
|
||||||
assessmentType: undefined,
|
assessmentType: undefined,
|
||||||
assessmentDate: undefined,
|
assessmentDate: undefined,
|
||||||
violenceScore: undefined,
|
violenceScore: undefined,
|
||||||
@ -245,8 +242,7 @@ const resetForm = () => {
|
|||||||
riskLevel: undefined,
|
riskLevel: undefined,
|
||||||
riskFactors: undefined,
|
riskFactors: undefined,
|
||||||
suggestions: undefined,
|
suggestions: undefined,
|
||||||
assessorId: undefined,
|
// assessorId 和 assessorName 由后端自动从登录上下文获取,不从前端传递
|
||||||
assessorName: undefined,
|
|
||||||
nextAssessmentDate: undefined,
|
nextAssessmentDate: undefined,
|
||||||
status: 1,
|
status: 1,
|
||||||
remark: undefined
|
remark: undefined
|
||||||
|
|||||||
@ -290,9 +290,21 @@ const submitForm = async () => {
|
|||||||
// 提交请求
|
// 提交请求
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
// 格式化时间,保持中国时区
|
||||||
|
const formatDateTime = (timestamp: number) => {
|
||||||
|
if (!timestamp) return undefined
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
const data = {
|
const data = {
|
||||||
...formData.value,
|
...formData.value,
|
||||||
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString() : undefined
|
occurTime: formatDateTime(formData.value.occurTime)
|
||||||
} as unknown as SituationSaveReqVO
|
} as unknown as SituationSaveReqVO
|
||||||
if (formType.value === 'create') {
|
if (formType.value === 'create') {
|
||||||
await SituationApi.createSituation(data)
|
await SituationApi.createSituation(data)
|
||||||
|
|||||||
@ -385,8 +385,8 @@ const submitForm = async () => {
|
|||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
...formData.value,
|
...formData.value,
|
||||||
alertTime: formData.value.alertTime ? new Date(formData.value.alertTime).toISOString() : undefined,
|
alertTime: formData.value.alertTime ? new Date(formData.value.alertTime).toISOString().slice(0, 19).replace('T', ' ') : undefined,
|
||||||
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString() : undefined
|
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString().slice(0, 19).replace('T', ' ') : undefined
|
||||||
} as unknown as WarningSaveReqVO
|
} as unknown as WarningSaveReqVO
|
||||||
if (formType.value === 'create') {
|
if (formType.value === 'create') {
|
||||||
await WarningApi.createWarning(data)
|
await WarningApi.createWarning(data)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user