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