| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952 |
- <template>
- <div class="p-2 accident-page">
- <!-- 顶部数据卡片 -->
- <el-row :gutter="20" class="mb-2">
- <el-col :span="6">
- <el-card shadow="hover" class="stat-card">
- <div class="stat-card-content">
- <div class="stat-info">
- <div class="stat-title">本月上报事故</div>
- <div class="stat-value">{{ stats.monthTotal || 0 }}</div>
- <div class="stat-extra" :class="(stats.monthRate || 0) >= 0 ? 'stat-extra-up' : 'stat-extra-down'">
- <el-icon v-if="(stats.monthRate || 0) >= 0"><CaretTop /></el-icon>
- <el-icon v-else><CaretBottom /></el-icon>
- <span>{{ Math.abs(stats.monthRate || 0) }}% 较上月</span>
- </div>
- </div>
- <div class="stat-icon stat-icon-blue">
- <el-icon><Document /></el-icon>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover" class="stat-card">
- <div class="stat-card-content">
- <div class="stat-info">
- <div class="stat-title">待处理</div>
- <div class="stat-value">{{ stats.investigating || 0 }}</div>
- <div class="stat-extra" :class="(stats.investigatingRate || 0) >= 0 ? 'stat-extra-up' : 'stat-extra-down'">
- <el-icon v-if="(stats.investigatingRate || 0) >= 0"><CaretTop /></el-icon>
- <el-icon v-else><CaretBottom /></el-icon>
- <span>{{ Math.abs(stats.investigatingRate || 0) }}% 较上月</span>
- </div>
- </div>
- <div class="stat-icon stat-icon-orange">
- <el-icon><Search /></el-icon>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover" class="stat-card">
- <div class="stat-card-content">
- <div class="stat-info">
- <div class="stat-title">归档</div>
- <div class="stat-value">{{ stats.finished || 0 }}</div>
- <div class="stat-extra" :class="(stats.finishedRate || 0) >= 0 ? 'stat-extra-up' : 'stat-extra-down'">
- <el-icon v-if="(stats.finishedRate || 0) >= 0"><CaretTop /></el-icon>
- <el-icon v-else><CaretBottom /></el-icon>
- <span>{{ Math.abs(stats.finishedRate || 0) }}% 较上月</span>
- </div>
- </div>
- <div class="stat-icon stat-icon-green">
- <el-icon><CircleCheck /></el-icon>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover" class="stat-card">
- <div class="stat-card-content">
- <div class="stat-info">
- <div class="stat-title">待考核</div>
- <div class="stat-value">{{ stats.pendingAssess || 0 }}</div>
- <div class="stat-extra" :class="(stats.pendingAssessRate || 0) >= 0 ? 'stat-extra-up' : 'stat-extra-down'">
- <el-icon v-if="(stats.pendingAssessRate || 0) >= 0"><CaretTop /></el-icon>
- <el-icon v-else><CaretBottom /></el-icon>
- <span>{{ Math.abs(stats.pendingAssessRate || 0) }}% 较上月</span>
- </div>
- </div>
- <div class="stat-icon stat-icon-red">
- <el-icon><EditPen /></el-icon>
- </div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- <!-- 搜索区域 -->
- <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
- <div v-show="showSearch" class="mb-[10px]">
- <el-card shadow="hover">
- <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="80">
- <el-form-item label="关键词" prop="keyword">
- <el-input v-model="queryParams.keyword" placeholder="编号/名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
- </el-form-item>
- <el-form-item label="状态" prop="handleStatus">
- <el-select v-model="queryParams.handleStatus" placeholder="所有状态" clearable style="width: 200px">
- <el-option v-for="dict in accident_status_dict" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="等级" prop="accidentLevel">
- <el-select v-model="queryParams.accidentLevel" placeholder="事故等级" clearable style="width: 200px">
- <el-option v-for="dict in accident_level_dict" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="类型" prop="accidentType">
- <el-select v-model="queryParams.accidentType" placeholder="事故类型" clearable style="width: 200px">
- <el-option v-for="dict in accident_type_dict" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
- <el-button icon="Refresh" @click="resetQuery">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- </div>
- </transition>
- <!-- 数据表格区域 -->
- <el-card shadow="hover" class="list-card">
- <div class="list-toolbar">
- <span class="list-total">共 {{ total }} 条记录</span>
- <div class="toolbar-actions">
- <el-button type="primary" plain icon="Plus" @click="handleAdd">新增上报</el-button>
- <right-toolbar v-model:show-search="showSearch" @query-table="getList" style="margin-left: 8px;" />
- </div>
- </div>
- <el-table v-loading="loading" :data="accidentList" border>
- <el-table-column label="事故编号" align="center" prop="accidentCode" width="220" :show-overflow-tooltip="true" />
- <el-table-column label="事故名称" align="center" prop="accidentTitle" :show-overflow-tooltip="true" />
- <el-table-column label="事故时间" align="center" prop="accidentTime" width="170" />
- <el-table-column label="事故地点" align="center" prop="accidentLocation" width="160" :show-overflow-tooltip="true" />
- <el-table-column label="等级" align="center" prop="accidentLevel" width="100">
- <template #default="scope">
- <el-tag :type="getLevelTag(scope.row.accidentLevel)">{{ scope.row.accidentLevel }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="类型" align="center" prop="accidentType" width="100" />
- <el-table-column label="状态" align="center" prop="handleStatus" width="100">
- <template #default="scope">
- <el-tag :type="getStatusTag(scope.row.handleStatus)">{{ scope.row.handleStatus }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" fixed="right" align="center" width="280" class-name="small-padding fixed-width">
- <template #default="scope">
- <el-button v-hasPermi="['risk:accident:query']" link type="primary" icon="View" @click="handleView(scope.row)">详情</el-button>
- <el-button v-if="scope.row.handleStatus === '待处理'" v-hasPermi="['risk:accident:handle']" link type="primary" icon="CircleCheck" @click="openHandleDialog(scope.row)">处理</el-button>
- <el-button v-hasPermi="['risk:accident:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)">编辑</el-button>
- <el-button v-hasPermi="['risk:accident:remove']" link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
- </el-card>
- <!-- 近12个月事故统计 -->
- <el-card shadow="hover" class="mt-2">
- <template #header>
- <span class="section-title">近12个月事故统计</span>
- </template>
- <div ref="trendChartRef" class="trend-chart"></div>
- </el-card>
- <!-- 新增/编辑对话框 -->
- <el-dialog v-model="dialog.visible" :title="dialog.title" width="780px" append-to-body @close="closeDialog">
- <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="事故时间" prop="accidentTime">
- <el-date-picker v-model="form.accidentTime" type="datetime" placeholder="请选择事故时间" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="事故地点" prop="accidentLocation">
- <el-input v-model="form.accidentLocation" placeholder="如:XX车间XX工位 / XX园区XX路段" maxlength="200" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="事故等级" prop="accidentLevel">
- <el-select v-model="form.accidentLevel" placeholder="请选择事故等级" style="width: 100%">
- <el-option v-for="dict in accident_level_dict" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="事故类型" prop="accidentType">
- <el-select v-model="form.accidentType" placeholder="请选择事故类型" style="width: 100%">
- <el-option v-for="dict in accident_type_dict" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="事故名称" prop="accidentTitle">
- <el-input v-model="form.accidentTitle" placeholder="如:XX工位机械夹伤事故" maxlength="200" />
- </el-form-item>
- <el-form-item label="涉及人员" prop="participants">
- <el-input v-model="form.participants" placeholder="如:张三(操作工)、李四(班组长)" maxlength="500" />
- </el-form-item>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="死亡人数" prop="deathCount">
- <el-input-number v-model="form.deathCount" :min="0" style="width: 100%" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="受伤人数" prop="injuryCount">
- <el-input-number v-model="form.injuryCount" :min="0" style="width: 100%" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="事故经过" prop="accidentDesc">
- <el-input v-model="form.accidentDesc" type="textarea" :rows="4" placeholder="请详细描述事故发生经过(包含时间、地点、事件、后果)" maxlength="2000" show-word-limit />
- </el-form-item>
- <el-form-item label="初步原因" prop="accidentCause">
- <el-input v-model="form.accidentCause" type="textarea" :rows="3" placeholder="初步判断的事故原因,如操作违规、设备故障、环境因素" maxlength="1000" show-word-limit />
- </el-form-item>
- <el-form-item label="上报人" prop="reporterName">
- <el-input v-model="form.reporterName" placeholder="上报人姓名(默认当前登录用户)" maxlength="50" />
- </el-form-item>
- <el-form-item label="现场照片" prop="photoList">
- <el-upload v-model:file-list="photoFileList" :http-request="customUpload" list-type="picture-card" :limit="9" accept=".jpg,.png,.jpeg" :on-success="handleUploadSuccess" :on-error="handleUploadError" :on-exceed="handleExceed" :before-upload="beforeUpload">
- <el-icon><Plus /></el-icon>
- </el-upload>
- <div class="el-upload__tip">最多上传9张,支持 jpg/png/jpeg 格式</div>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button type="primary" @click="submitForm">确 定</el-button>
- <el-button @click="cancel">取 消</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 查看详情对话框 -->
- <el-dialog v-model="viewDialog.visible" title="事故详情" width="800px" append-to-body>
- <el-descriptions :column="2" border>
- <el-descriptions-item label="事故编号">{{ viewForm.accidentCode }}</el-descriptions-item>
- <el-descriptions-item label="事故时间">{{ viewForm.accidentTime }}</el-descriptions-item>
- <el-descriptions-item label="事故名称" :span="2">{{ viewForm.accidentTitle }}</el-descriptions-item>
- <el-descriptions-item label="事故地点">{{ viewForm.accidentLocation }}</el-descriptions-item>
- <el-descriptions-item label="事故等级">
- <el-tag :type="getLevelTag(viewForm.accidentLevel)">{{ viewForm.accidentLevel }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="事故类型">{{ viewForm.accidentType }}</el-descriptions-item>
- <el-descriptions-item label="处理状态">
- <el-tag :type="getStatusTag(viewForm.handleStatus)">{{ viewForm.handleStatus }}</el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="上报人">{{ viewForm.reporterName }}</el-descriptions-item>
- <el-descriptions-item label="上报时间">{{ viewForm.reportTime }}</el-descriptions-item>
- <el-descriptions-item label="涉及人员" :span="2">{{ viewForm.participants }}</el-descriptions-item>
- <el-descriptions-item label="死亡人数">{{ viewForm.deathCount }}</el-descriptions-item>
- <el-descriptions-item label="受伤人数">{{ viewForm.injuryCount }}</el-descriptions-item>
- <el-descriptions-item label="经济损失">{{ viewForm.lossAmount }}</el-descriptions-item>
- <el-descriptions-item label="处理人">{{ viewForm.handlerName }}</el-descriptions-item>
- <el-descriptions-item label="事故经过" :span="2">{{ viewForm.accidentDesc }}</el-descriptions-item>
- <el-descriptions-item label="初步原因" :span="2">{{ viewForm.accidentCause }}</el-descriptions-item>
- <el-descriptions-item label="处理措施" :span="2">{{ viewForm.handleMeasures }}</el-descriptions-item>
- <el-descriptions-item label="调查结果" :span="2">{{ viewForm.investigationResult }}</el-descriptions-item>
- <el-descriptions-item label="改进措施" :span="2">{{ viewForm.improvementMeasures }}</el-descriptions-item>
- <el-descriptions-item v-if="viewPhotoList.length > 0" label="现场照片" :span="2">
- <el-image v-for="(img, idx) in viewPhotoList" :key="idx" :src="img" :preview-src-list="viewPhotoList" :initial-index="idx" fit="cover" style="width: 80px; height: 80px; margin-right: 8px; border-radius: 4px" />
- </el-descriptions-item>
- <el-descriptions-item label="备注" :span="2">{{ viewForm.remark }}</el-descriptions-item>
- </el-descriptions>
- <template #footer>
- <el-button @click="viewDialog.visible = false">关 闭</el-button>
- </template>
- </el-dialog>
- <!-- 事故处理弹窗(独立,字段:处理方式/处理意见/责任人/处理期限/附件) -->
- <el-dialog
- v-model="handleDialog.visible"
- :title="handleDialog.title"
- width="640px"
- append-to-body
- :close-on-click-modal="false"
- @close="cancelHandle"
- >
- <el-form ref="handleFormRef" :model="handleForm" :rules="handleRules" label-width="100px">
- <el-form-item label="事故编号">
- <span class="readonly-text">{{ handleForm.accidentCode }}</span>
- </el-form-item>
- <el-form-item label="处理方式" prop="handleResult">
- <el-select v-model="handleForm.handleResult" placeholder="请选择处理方式" style="width: 100%" clearable>
- <el-option v-for="dict in accident_handle_way_dict" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="处理意见" prop="handleMeasures">
- <el-input
- v-model="handleForm.handleMeasures"
- type="textarea"
- :rows="5"
- placeholder="请详细填写本次事故处理过程、措施与结论..."
- maxlength="2000"
- show-word-limit
- />
- </el-form-item>
- <el-form-item label="责任人" prop="handlerName">
- <el-input v-model="handleForm.handlerName" placeholder="请输入责任人姓名(默认当前登录用户)" maxlength="50" />
- </el-form-item>
- <el-form-item label="处理期限" prop="handleDeadline">
- <el-date-picker
- v-model="handleForm.handleDeadline"
- type="date"
- placeholder="请选择处理期限"
- value-format="YYYY-MM-DD"
- style="width: 100%"
- />
- </el-form-item>
- <el-form-item label="附件上传">
- <el-upload
- v-model:file-list="handleAttachmentFileList"
- :http-request="customHandleAttachmentUpload"
- list-type="text"
- :limit="1"
- :on-success="onHandleAttachmentSuccess"
- :on-error="onHandleAttachmentError"
- :on-exceed="onHandleAttachmentExceed"
- :on-remove="onHandleAttachmentRemove"
- :before-upload="beforeHandleAttachmentUpload"
- >
- <el-button type="primary" plain icon="Upload">点击上传</el-button>
- <template #tip>
- <div class="el-upload__tip">支持任意格式文件,大小不超过 20MB</div>
- </template>
- </el-upload>
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="cancelHandle">取 消</el-button>
- <el-button
- v-hasPermi="['risk:accident:handle']"
- type="primary"
- :loading="handleSubmitting"
- @click="submitHandle"
- >提交处理</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup name="Accident" lang="ts">
- import { ref, reactive, toRefs, onMounted, onBeforeUnmount, nextTick, getCurrentInstance } from 'vue';
- import * as echarts from 'echarts';
- import {
- listAccident,
- getAccident,
- addAccident,
- updateAccident,
- delAccident,
- getAccidentStats,
- getAccidentTrend,
- handleAccident
- } from '@/api/risk/accident';
- import type { RiskAccidentBo, RiskAccidentVo, AccidentStatsVo, AccidentTrendVo } from '@/api/risk/types';
- import { FormInstance, UploadFile, UploadRawFile, UploadUserFile } from 'element-plus';
- import { UPLOAD_ACTION_URL, getMinioFileUrl, splitFileUrls } from '@/utils/minio';
- import { uploadFile } from '@/api/risk/file';
- import type { ComponentInternalInstance } from 'vue';
- import { CaretTop, CaretBottom, Document, Search, CircleCheck, EditPen } from '@element-plus/icons-vue';
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- interface AccidentForm extends RiskAccidentBo {
- photoList?: string[];
- }
- const accidentList = ref<RiskAccidentVo[]>([]);
- const loading = ref(true);
- const showSearch = ref(true);
- const total = ref(0);
- const queryFormRef = ref<FormInstance>();
- const formRef = ref<FormInstance>();
- const photoFileList = ref<UploadUserFile[]>([]);
- const viewPhotoList = ref<string[]>([]);
- // 字典
- const {
- risk_accident_level: accident_level_dict,
- risk_accident_type: accident_type_dict,
- risk_accident_status: accident_status_dict,
- risk_accident_handle_way: accident_handle_way_dict
- } = toRefs<any>(
- proxy?.useDict(
- 'risk_accident_level',
- 'risk_accident_type',
- 'risk_accident_status',
- 'risk_accident_handle_way'
- )
- );
- // 统计
- const stats = ref<AccidentStatsVo>({
- monthTotal: 0,
- lastMonthTotal: 0,
- monthRate: 0,
- investigating: 0,
- lastMonthInvestigating: 0,
- investigatingRate: 0,
- finished: 0,
- lastMonthFinished: 0,
- finishedRate: 0,
- pendingAssess: 0,
- lastMonthPendingAssess: 0,
- pendingAssessRate: 0
- });
- // 趋势
- const trendList = ref<AccidentTrendVo[]>([]);
- const trendChartRef = ref<HTMLElement>();
- let trendChart: echarts.ECharts | null = null;
- const data = reactive<any>({
- form: {
- id: undefined,
- accidentCode: '',
- accidentTitle: '',
- accidentType: '',
- accidentLevel: '',
- accidentLocation: '',
- accidentTime: '',
- accidentCause: '',
- accidentDesc: '',
- deathCount: 0,
- injuryCount: 0,
- lossAmount: 0,
- reporterName: '',
- reportTime: '',
- handleStatus: '待处理' as any,
- participants: '',
- photoList: [] as string[]
- } as any,
- queryParams: {
- pageNum: 1,
- pageSize: 10,
- keyword: '',
- handleStatus: '',
- accidentLevel: '',
- accidentType: ''
- },
- rules: {
- accidentTitle: [{ required: true, message: '事故名称不能为空', trigger: 'blur' }],
- accidentTime: [{ required: true, message: '请选择事故时间', trigger: 'change' }],
- accidentLocation: [{ required: true, message: '请输入事故地点', trigger: 'blur' }],
- accidentLevel: [{ required: true, message: '请选择事故等级', trigger: 'change' }],
- accidentType: [{ required: true, message: '请选择事故类型', trigger: 'change' }],
- accidentDesc: [{ required: true, message: '请输入事故经过', trigger: 'blur' }],
- participants: [{ required: true, message: '请输入涉及人员', trigger: 'blur' }],
- deathCount: [{ required: true, message: '请填写死亡人数', trigger: 'change' }],
- injuryCount: [{ required: true, message: '请填写受伤人数', trigger: 'change' }],
- reporterName: [{ required: true, message: '请输入上报人', trigger: 'blur' }]
- }
- });
- const { queryParams, form, rules } = toRefs(data);
- const dialog = reactive<DialogOption>({ visible: false, title: '' });
- const viewDialog = reactive({ visible: false });
- const viewForm = ref<any>({});
- // 事故处理弹窗(独立,不与新增/编辑表单混用)
- const handleDialog = reactive({ visible: false, title: '事故处理' });
- const handleSubmitting = ref(false);
- const handleForm = ref<any>({
- id: undefined,
- accidentCode: '',
- handleResult: '',
- handleMeasures: '',
- handlerName: '',
- handleDeadline: '',
- handleAttachmentUrl: '',
- handleAttachmentName: ''
- });
- const handleFormRef = ref<FormInstance>();
- const handleAttachmentFileList = ref<UploadUserFile[]>([]);
- const handleRules = {
- handleResult: [{ required: true, message: '请选择处理方式', trigger: 'change' }],
- handleMeasures: [{ required: true, message: '请输入处理意见文案', trigger: 'blur' }],
- handlerName: [{ required: true, message: '请输入责任人', trigger: 'blur' }],
- handleDeadline: [{ required: true, message: '请选择处理期限', trigger: 'change' }]
- };
- const getList = async () => {
- loading.value = true;
- try {
- const res = await listAccident(queryParams.value);
- accidentList.value = res.rows || [];
- total.value = res.total || 0;
- } finally {
- loading.value = false;
- }
- };
- const loadStats = async () => {
- const res = await getAccidentStats();
- if (res.data) {
- stats.value = res.data;
- }
- };
- const loadTrend = async () => {
- const res = await getAccidentTrend();
- trendList.value = res.data || [];
- await nextTick();
- renderTrendChart();
- };
- const renderTrendChart = () => {
- if (!trendChartRef.value) return;
- if (!trendChart) {
- trendChart = echarts.init(trendChartRef.value);
- }
- const months = trendList.value.map((it) => it.month?.slice(2) || '');
- const reportArr = trendList.value.map((it) => it.reportCount || 0);
- const finishedArr = trendList.value.map((it) => it.finishedCount || 0);
- trendChart.setOption({
- tooltip: { trigger: 'axis' },
- legend: {
- data: ['上报表数', '已结束'],
- top: 0,
- icon: 'circle'
- },
- grid: { left: 40, right: 30, top: 40, bottom: 30 },
- xAxis: {
- type: 'category',
- data: months,
- axisLine: { lineStyle: { color: '#e0e0e0' } },
- axisLabel: { color: '#666' }
- },
- yAxis: {
- type: 'value',
- minInterval: 1,
- splitLine: { lineStyle: { type: 'dashed', color: '#eee' } },
- axisLabel: { color: '#666' }
- },
- series: [
- {
- name: '上报表数',
- type: 'bar',
- barWidth: '32%',
- data: reportArr,
- itemStyle: { color: '#409EFF' }
- },
- {
- name: '已结束',
- type: 'bar',
- barWidth: '32%',
- data: finishedArr,
- itemStyle: { color: '#67C23A' }
- }
- ]
- });
- };
- const handleResize = () => {
- trendChart?.resize();
- };
- const loadAll = async () => {
- await Promise.all([getList(), loadStats(), loadTrend()]);
- };
- const handleQuery = () => {
- queryParams.value.pageNum = 1;
- getList();
- };
- const resetQuery = () => {
- queryFormRef.value?.resetFields();
- queryParams.value.pageNum = 1;
- queryParams.value.keyword = '';
- queryParams.value.handleStatus = '';
- queryParams.value.accidentLevel = '';
- queryParams.value.accidentType = '';
- handleQuery();
- };
- const handleAdd = () => {
- reset();
- dialog.visible = true;
- dialog.title = '事故上报';
- };
- const handleUpdate = async (row: RiskAccidentVo) => {
- reset();
- const res = await getAccident(row.id);
- Object.assign(form.value, res.data);
- const photoArr = parsePhotoList(form.value.photoList as any);
- if (photoArr.length > 0) {
- photoFileList.value = photoArr.map((url, idx) => ({
- uid: idx,
- name: `photo-${idx}`,
- status: 'success',
- url
- }));
- }
- dialog.visible = true;
- dialog.title = '编辑事故';
- };
- const handleView = async (row: RiskAccidentVo) => {
- const res = await getAccident(row.id);
- viewForm.value = res.data;
- const photoArr = parsePhotoList(viewForm.value.photoList as any);
- viewPhotoList.value = photoArr;
- viewDialog.visible = true;
- };
- // ============ 事故处理(独立弹窗) ============
- // 打开处理弹窗:重置表单 → 加载事故详情到只读字段 → 显示弹窗
- const openHandleDialog = async (row: RiskAccidentVo) => {
- if (!row || row.handleStatus !== '待处理') {
- proxy?.$modal.msgWarning('只有"待处理"状态的事故才能处理');
- return;
- }
- resetHandleForm();
- const res = await getAccident(row.id);
- const data: any = res.data || {};
- handleForm.value.id = data.id;
- handleForm.value.accidentCode = data.accidentCode || '';
- // 责任人默认当前登录用户名(由后端 LoginHelper.getUsername() 兜底,前端可空)
- handleForm.value.handlerName = data.handlerName || '';
- handleDialog.visible = true;
- };
- // 提交处理(后端强制将状态置为"归档")
- const submitHandle = () => {
- handleFormRef.value?.validate(async (valid: boolean) => {
- if (!valid) return;
- handleSubmitting.value = true;
- try {
- await handleAccident(handleForm.value.id, handleForm.value as RiskAccidentBo);
- proxy?.$modal.msgSuccess('处理成功,事故已归档');
- handleDialog.visible = false;
- await loadAll();
- } finally {
- handleSubmitting.value = false;
- }
- });
- };
- const cancelHandle = () => {
- handleDialog.visible = false;
- resetHandleForm();
- };
- const resetHandleForm = () => {
- handleForm.value = {
- id: undefined,
- accidentCode: '',
- handleResult: '',
- handleMeasures: '',
- handlerName: '',
- handleDeadline: '',
- handleAttachmentUrl: '',
- handleAttachmentName: ''
- };
- handleAttachmentFileList.value = [];
- handleFormRef.value?.resetFields();
- };
- // ============ 处理弹窗附件上传(走 axios baseURL,避免依赖 :action) ============
- const customHandleAttachmentUpload = async (options: any) => {
- try {
- const res: any = await uploadFile(options.file);
- if (res && res.code === 200 && res.data) {
- options.onSuccess(res, options.file);
- } else {
- options.onError(new Error(res?.msg || '上传失败'));
- }
- } catch (err: any) {
- options.onError(err);
- }
- };
- const beforeHandleAttachmentUpload = (file: UploadRawFile) => {
- const isLt20M = file.size / 1024 / 1024 < 20;
- if (!isLt20M) {
- proxy?.$modal.msgError('附件大小不能超过 20MB!');
- return false;
- }
- return true;
- };
- const onHandleAttachmentSuccess = (response: any, uploadFile: UploadFile) => {
- const url = response?.fileName || response?.data?.fileName || response?.url || response?.data?.url || '';
- uploadFile.url = getMinioFileUrl(url);
- handleForm.value.handleAttachmentUrl = uploadFile.url;
- handleForm.value.handleAttachmentName = uploadFile.name;
- proxy?.$modal.msgSuccess('上传成功');
- };
- const onHandleAttachmentError = () => {
- proxy?.$modal.msgError('附件上传失败,请重试');
- };
- const onHandleAttachmentExceed = () => {
- proxy?.$modal.msgWarning('最多只能上传 1 个附件');
- };
- const onHandleAttachmentRemove = () => {
- handleForm.value.handleAttachmentUrl = '';
- handleForm.value.handleAttachmentName = '';
- };
- const parsePhotoList = (val: any): string[] => {
- if (!val) return [];
- if (Array.isArray(val)) return val as string[];
- if (typeof val === 'string') {
- const trimmed = val.trim();
- if (!trimmed) return [];
- if (trimmed.startsWith('[')) {
- try {
- const parsed = JSON.parse(trimmed);
- return Array.isArray(parsed) ? parsed : [];
- } catch {
- return splitFileUrls(trimmed);
- }
- }
- return splitFileUrls(trimmed);
- }
- return [];
- };
- const handleDelete = async (row: RiskAccidentVo) => {
- await proxy?.$modal.confirm('是否确认删除事故编号为 "' + row.accidentCode + '" 的数据项?');
- await delAccident(row.id);
- proxy?.$modal.msgSuccess('删除成功');
- await getList();
- };
- const submitForm = () => {
- formRef.value?.validate(async (valid: boolean) => {
- if (valid) {
- const uploadedUrls = photoFileList.value
- .filter((f: any) => f.status === 'success')
- .map((f: any) => f.url || getMinioFileUrl(f.response?.fileName || f.response?.data?.fileName));
- form.value.photoList = uploadedUrls.length > 0 ? JSON.stringify(uploadedUrls) : '';
- if (form.value.id) {
- await updateAccident(form.value as RiskAccidentBo);
- proxy?.$modal.msgSuccess('修改成功');
- } else {
- await addAccident(form.value as RiskAccidentBo);
- proxy?.$modal.msgSuccess('上报成功');
- }
- dialog.visible = false;
- await loadAll();
- }
- });
- };
- const cancel = () => {
- dialog.visible = false;
- reset();
- };
- const reset = () => {
- form.value = {
- id: undefined,
- accidentCode: '',
- accidentTitle: '',
- accidentType: '',
- accidentLevel: '',
- accidentLocation: '',
- accidentTime: '',
- accidentCause: '',
- accidentDesc: '',
- deathCount: 0,
- injuryCount: 0,
- lossAmount: 0,
- reporterName: '',
- reportTime: '',
- handleStatus: '待处理',
- participants: '',
- photoList: []
- } as any;
- photoFileList.value = [];
- formRef.value?.resetFields();
- };
- const closeDialog = () => {
- dialog.visible = false;
- reset();
- };
- const handleUploadSuccess = (response: any, uploadFile: UploadFile) => {
- uploadFile.url = getMinioFileUrl(response.fileName || response.data?.fileName);
- proxy?.$modal.msgSuccess('上传成功');
- };
- const handleUploadError = () => {
- proxy?.$modal.msgError('上传失败,请重试');
- };
- const customUpload = async (options: any) => {
- try {
- const res: any = await uploadFile(options.file);
- if (res && res.code === 200 && res.data) {
- options.onSuccess(res, options.file);
- } else {
- options.onError(new Error(res?.msg || '上传失败'));
- }
- } catch (err: any) {
- options.onError(err);
- }
- };
- const beforeUpload = (file: UploadRawFile) => {
- const isValidType = ['image/jpeg', 'image/png', 'image/jpg'].includes(file.type);
- const isLt10M = file.size / 1024 / 1024 < 10;
- if (!isValidType) {
- proxy?.$modal.msgError('只能上传 jpg/png/jpeg 格式的图片!');
- return false;
- }
- if (!isLt10M) {
- proxy?.$modal.msgError('单张图片大小不能超过 10MB!');
- return false;
- }
- return true;
- };
- const handleExceed = () => {
- proxy?.$modal.msgWarning('最多只能上传9张图片');
- };
- const getLevelTag = (level: string): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
- const map: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
- 特别重大: 'danger',
- 重大: 'danger',
- 较大: 'warning',
- 一般: 'primary'
- };
- return (map[level] || 'info') as any;
- };
- const getStatusTag = (status: string): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
- const map: Record<string, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
- 待处理: 'danger',
- 归档: 'success'
- };
- return (map[status] || 'info') as any;
- };
- onMounted(async () => {
- await loadAll();
- window.addEventListener('resize', handleResize);
- });
- onBeforeUnmount(() => {
- window.removeEventListener('resize', handleResize);
- trendChart?.dispose();
- trendChart = null;
- });
- </script>
- <style scoped lang="scss">
- .accident-page {
- padding: 16px;
- height: 100%;
- overflow-y: auto;
- box-sizing: border-box;
- }
- .stat-card {
- border-radius: 8px;
- :deep(.el-card__body) { padding: 20px; }
- }
- .stat-card-content {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .stat-info { flex: 1; }
- .stat-title {
- font-size: 13px;
- color: #909399;
- margin-bottom: 8px;
- white-space: nowrap;
- }
- .stat-value {
- font-size: 28px;
- font-weight: 700;
- color: #303133;
- line-height: 1.2;
- margin-bottom: 8px;
- }
- .stat-extra {
- font-size: 12px;
- color: #909399;
- display: flex;
- align-items: center;
- gap: 2px;
- }
- .stat-extra-up { color: #f56c6c; }
- .stat-extra-down { color: #67c23a; }
- .stat-icon {
- width: 48px;
- height: 48px;
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 24px;
- color: #fff;
- }
- .stat-icon-blue { background: #ecf5ff; color: #409EFF; }
- .stat-icon-orange { background: #fdf6ec; color: #E6A23C; }
- .stat-icon-green { background: #f0f9eb; color: #67C23A; }
- .stat-icon-red { background: #fef0f0; color: #F56C6C; }
- .section-title { font-size: 16px; font-weight: 600; color: #303133; }
- .mb-2 { margin-bottom: 8px; }
- .mt-2 { margin-top: 8px; }
- .trend-chart { width: 100%; height: 360px; }
- .el-upload__tip { font-size: 12px; color: #909399; line-height: 1.5; margin-top: 4px; }
- .list-toolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- }
- .list-total {
- font-size: 13px;
- color: #606266;
- }
- .toolbar-actions {
- display: flex;
- align-items: center;
- }
- /* ============ 事故处理弹窗专用样式 ============ */
- .readonly-text {
- display: inline-block;
- width: 100%;
- padding: 0 12px;
- line-height: 32px;
- background: #f5f7fa;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- color: #606266;
- font-size: 14px;
- }
- .upload-row {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 4px;
- }
- .upload-tip {
- font-size: 12px;
- color: #909399;
- line-height: 1.5;
- }
- </style>
|