LReportUpload.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. <!-- 预警报告上传 -->
  2. <template>
  3. <div class="app-container warning-report-page" v-loading="pageLoading">
  4. <el-card shadow="never" class="page-card header-card">
  5. <div class="page-header">
  6. <div>
  7. <div class="page-title">预警报告上传</div>
  8. <div class="page-subtitle">提供预警报告附件上传功能,结合设备报警曲线、关联设备和周边要素分析,描述预警发生的具体内容</div>
  9. </div>
  10. </div>
  11. </el-card>
  12. <el-card shadow="never" class="page-card">
  13. <el-form :model="queryParams" :inline="true">
  14. <el-form-item label="预警单号">
  15. <el-input
  16. v-model="queryParams.warningNo"
  17. placeholder="请输入预警单号"
  18. clearable
  19. style="width: 220px"
  20. @keyup.enter="loadWarningOptions"
  21. />
  22. </el-form-item>
  23. <el-form-item label="预警类型">
  24. <el-select v-model="queryParams.warningType" clearable placeholder="请选择预警类型" style="width: 180px">
  25. <el-option
  26. v-for="item in warningTypeOptions"
  27. :key="item.dictValue"
  28. :label="item.dictLabel"
  29. :value="item.dictValue"
  30. />
  31. </el-select>
  32. </el-form-item>
  33. <el-form-item label="预警级别">
  34. <el-select v-model="queryParams.warningLevel" clearable placeholder="请选择预警级别" style="width: 180px">
  35. <el-option
  36. v-for="item in warningLevelOptions"
  37. :key="item.dictValue"
  38. :label="item.dictLabel"
  39. :value="item.dictValue"
  40. />
  41. </el-select>
  42. </el-form-item>
  43. <el-form-item label="选择预警">
  44. <el-select
  45. v-model="selectedWarningId"
  46. filterable
  47. clearable
  48. placeholder="请选择一条预警"
  49. style="width: 360px"
  50. @change="handleWarningChange"
  51. >
  52. <el-option
  53. v-for="item in warningOptions"
  54. :key="item.warningId"
  55. :label="item.warningNo"
  56. :value="item.warningId"
  57. >
  58. <div class="warning-option">
  59. <span>{{ item.warningNo }}</span>
  60. <el-tag :type="getLevelTagType(item.warningLevel)">{{ item.warningLevelText }}</el-tag>
  61. </div>
  62. </el-option>
  63. </el-select>
  64. </el-form-item>
  65. <el-form-item>
  66. <el-button type="primary" icon="Search" @click="loadWarningOptions">查询</el-button>
  67. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  68. </el-form-item>
  69. </el-form>
  70. </el-card>
  71. <div class="report-container">
  72. <div class="left-panel">
  73. <el-card shadow="never" class="section-card">
  74. <template #header>
  75. <div class="section-title">报告上传</div>
  76. </template>
  77. <div
  78. class="upload-area"
  79. :class="{ 'drag-over': isDragOver }"
  80. @dragover.prevent="isDragOver = true"
  81. @dragleave.prevent="isDragOver = false"
  82. @drop.prevent="handleDrop"
  83. >
  84. <el-icon class="upload-icon"><UploadFilled /></el-icon>
  85. <div class="upload-text">
  86. <div class="primary-text">点击或拖拽文件至此区域</div>
  87. <div class="support-text">支持 `.pdf .doc .docx .zip`,单个文件不超过 20MB</div>
  88. </div>
  89. <el-upload
  90. ref="uploadRef"
  91. class="inline-upload"
  92. action="#"
  93. :auto-upload="false"
  94. :show-file-list="false"
  95. :on-change="handleFileChange"
  96. :limit="1"
  97. accept=".pdf,.doc,.docx,.zip"
  98. >
  99. <el-button type="primary">选择文件</el-button>
  100. </el-upload>
  101. </div>
  102. <div v-if="uploadedFile" class="file-info">
  103. <div class="file-item">
  104. <el-icon><Document /></el-icon>
  105. <span class="file-name">{{ uploadedFile.name }}</span>
  106. <span class="file-size">{{ formatFileSize(uploadedFile.size) }}</span>
  107. <el-icon class="remove-file" @click="removeFile"><CircleCloseFilled /></el-icon>
  108. </div>
  109. </div>
  110. <div class="report-description">
  111. <div class="field-title">报告摘要</div>
  112. <el-input
  113. v-model="reportSummary"
  114. type="textarea"
  115. :rows="5"
  116. maxlength="500"
  117. show-word-limit
  118. placeholder="请输入报告摘要、诊断结论或补充说明"
  119. />
  120. </div>
  121. <el-button
  122. type="success"
  123. class="submit-btn"
  124. :disabled="!selectedWarningId || !uploadedFile"
  125. :loading="submitLoading"
  126. @click="submitReport"
  127. >
  128. <el-icon><Upload /></el-icon>
  129. 提交预警报告
  130. </el-button>
  131. </el-card>
  132. <el-card shadow="never" class="section-card">
  133. <template #header>
  134. <div class="section-title">预警基础信息</div>
  135. </template>
  136. <el-empty v-if="!warningDetail.basicInfo" description="请先选择预警" />
  137. <el-descriptions v-else :column="1" border size="small">
  138. <el-descriptions-item label="预警单号">{{ currentBasicInfo.warningNo || "-" }}</el-descriptions-item>
  139. <el-descriptions-item label="预警类型">{{ currentBasicInfo.warningTypeText || "-" }}</el-descriptions-item>
  140. <el-descriptions-item label="预警级别">{{ currentBasicInfo.warningLevelText || "-" }}</el-descriptions-item>
  141. <el-descriptions-item label="发布人">{{ currentBasicInfo.publisher || "-" }}</el-descriptions-item>
  142. <el-descriptions-item label="发布时间">{{ currentBasicInfo.publishTime || "-" }}</el-descriptions-item>
  143. <el-descriptions-item label="权属单位">{{ currentBasicInfo.ownershipUnit || "-" }}</el-descriptions-item>
  144. <el-descriptions-item label="状态">
  145. <WarningStatusTag :status="currentBasicInfo.status">
  146. {{ getStatusText(currentBasicInfo.status) }}
  147. </WarningStatusTag>
  148. </el-descriptions-item>
  149. </el-descriptions>
  150. </el-card>
  151. <el-card shadow="never" class="section-card">
  152. <template #header>
  153. <div class="section-title">已有附件</div>
  154. </template>
  155. <el-empty v-if="!attachmentList.length" description="暂无已上传附件" />
  156. <div v-else class="attachment-list">
  157. <div v-for="(item, index) in attachmentList" :key="index" class="attachment-item">
  158. <el-icon><Document /></el-icon>
  159. <el-link :href="buildAttachmentUrl(item.attachmentUrl)" target="_blank" type="primary" class="attachment-name">
  160. {{ item.fileName || item.originalName || item.attachmentName || "附件" }}
  161. </el-link>
  162. <span class="attachment-size">{{ item.attachmentSize ? formatFileSize(item.attachmentSize) : "" }}</span>
  163. <span class="attachment-time">{{ item.createTime || item.uploadTime || "-" }}</span>
  164. </div>
  165. </div>
  166. </el-card>
  167. </div>
  168. <div class="right-panel">
  169. <el-card shadow="never" class="section-card">
  170. <el-tabs v-model="activeTab" class="analysis-tabs">
  171. <el-tab-pane label="设备报警曲线" name="curve">
  172. <div ref="curveChartRef" class="curve-chart"></div>
  173. <el-alert
  174. :title="curveSummary.title"
  175. :description="curveSummary.description"
  176. type="warning"
  177. :closable="false"
  178. />
  179. </el-tab-pane>
  180. <el-tab-pane label="关联设备" name="device">
  181. <el-descriptions :column="1" border size="small">
  182. <el-descriptions-item label="设备名称">{{ currentBasicInfo.deviceName || "-" }}</el-descriptions-item>
  183. <el-descriptions-item label="设备编码">{{ currentBasicInfo.deviceCode || "-" }}</el-descriptions-item>
  184. <el-descriptions-item label="权属单位">{{ currentBasicInfo.ownershipUnit || "-" }}</el-descriptions-item>
  185. <el-descriptions-item label="处置人">{{ currentBasicInfo.handler || "-" }}</el-descriptions-item>
  186. <el-descriptions-item label="状态"><WarningStatusTag :status="currentBasicInfo.status">{{ getStatusText(currentBasicInfo.status) }}</WarningStatusTag></el-descriptions-item>
  187. </el-descriptions>
  188. </el-tab-pane>
  189. <el-tab-pane label="周边要素分析" name="environment">
  190. <div class="factors-grid">
  191. <div class="factor-card">
  192. <div class="factor-label">关联报警</div>
  193. <div class="factor-value">{{ alarmList.length }}</div>
  194. <div class="factor-desc">报警曲线分析样本数</div>
  195. </div>
  196. <div class="factor-card">
  197. <div class="factor-label">处置记录</div>
  198. <div class="factor-value">{{ disposalHistory.length }}</div>
  199. <div class="factor-desc">流程处置和跟踪情况</div>
  200. </div>
  201. <div class="factor-card">
  202. <div class="factor-label">历史附件</div>
  203. <div class="factor-value">{{ attachmentList.length }}</div>
  204. <div class="factor-desc">已上传的资料与报告数量</div>
  205. </div>
  206. <div class="factor-card">
  207. <div class="factor-label">电子存档</div>
  208. <div class="factor-value">{{ hasArchive ? "已生成" : "未生成" }}</div>
  209. <div class="factor-desc">预警归档与存档状态</div>
  210. </div>
  211. </div>
  212. <el-card shadow="never" class="analysis-text-card">
  213. <template #header>
  214. <span>综合周边要素结论</span>
  215. </template>
  216. <p>{{ environmentConclusion }}</p>
  217. </el-card>
  218. </el-tab-pane>
  219. </el-tabs>
  220. <div class="warning-description">
  221. <div class="desc-header">
  222. <el-icon><InfoFilled /></el-icon>
  223. <h4>预警具体内容描述</h4>
  224. </div>
  225. <div class="desc-content">
  226. <p class="warning-content">{{ currentBasicInfo.warningContent || "暂无预警描述" }}</p>
  227. <p v-if="reportSummary"><strong>报告补充摘要:</strong>{{ reportSummary }}</p>
  228. </div>
  229. </div>
  230. </el-card>
  231. </div>
  232. </div>
  233. </div>
  234. </template>
  235. <script setup>
  236. import WarningStatusTag from "../components/WarningStatusTag.vue"
  237. import { useWarningStatuses } from '@/views/subSystem/lifeCompany/warningWorkflow'
  238. const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
  239. import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
  240. import { ElMessage } from "element-plus";
  241. import { CircleCloseFilled, Document, InfoFilled, Upload, UploadFilled } from "@element-plus/icons-vue";
  242. import * as echarts from "echarts";
  243. import {
  244. getAlertTypeDictionaryData,
  245. getLinkedAlarmListData,
  246. uploadWarningReportData,
  247. getdbyjlistData,
  248. updateYjxxData
  249. } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
  250. import { getWarningFullDetailData } from "@/api/pipeNetwork/earlyWarning";
  251. import { buildMinioAttachmentUrl } from "@/utils/minioAttachment";
  252. const pageLoading = ref(false);
  253. const submitLoading = ref(false);
  254. const isDragOver = ref(false);
  255. const uploadedFile = ref(null);
  256. const reportSummary = ref("");
  257. const activeTab = ref("curve");
  258. const uploadRef = ref();
  259. const curveChartRef = ref(null);
  260. let curveChart = null;
  261. const warningTypeOptions = ref([]);
  262. const warningLevelOptions = ref([]);
  263. const warningOptions = ref([]);
  264. const selectedWarningId = ref("");
  265. const warningDetail = ref({});
  266. const minioBaseUrl = (import.meta.env.VITE_APP_MINIO_BASE_URL || "").trim();
  267. const queryParams = ref({
  268. warningNo: "",
  269. warningType: "",
  270. warningLevel: "",
  271. status: ""
  272. });
  273. const warningTypeLabelMap = computed(() => {
  274. return warningTypeOptions.value.reduce((acc, item) => {
  275. acc[item.dictValue] = item.dictLabel;
  276. return acc;
  277. }, {});
  278. });
  279. const warningLevelLabelMap = computed(() => {
  280. return warningLevelOptions.value.reduce((acc, item) => {
  281. acc[item.dictValue] = item.dictLabel;
  282. return acc;
  283. }, {});
  284. });
  285. const warningStatusLabelMap = databaseStatusLabels;
  286. const currentBasicInfo = computed(() => {
  287. const basic = warningDetail.value.basicInfo || {};
  288. return {
  289. deviceCode: basic.deviceCode || basic.device_code || "",
  290. deviceName: basic.deviceName || basic.device_name || "",
  291. warningId: basic.warningId || basic.warning_id || "",
  292. warningNo: basic.warningNo || basic.warning_no || "",
  293. warningType: basic.warningType || basic.warning_type || "",
  294. warningTypeText: warningTypeLabelMap.value[basic.warningType || basic.warning_type] || basic.warningType || basic.warning_type || "-",
  295. warningLevel: basic.warningLevel || basic.warning_level || "",
  296. warningLevelText: warningLevelLabelMap.value[basic.warningLevel || basic.warning_level] || basic.warningLevel || basic.warning_level || "-",
  297. ownershipUnit: basic.ownershipUnit || basic.ownership_unit || "",
  298. publisher: basic.publisher || "",
  299. publishTime: basic.publishTime || basic.publish_time || "",
  300. handler: basic.handler || "",
  301. status: basic.status || "",
  302. statusText: warningStatusLabelMap.value[basic.status] || basic.statusText || basic.status || "",
  303. warningContent: basic.warningContent || basic.warning_content || "",
  304. warningSpecial: basic.warningSpecial || basic.warning_special || "",
  305. remark: basic.remark || ""
  306. };
  307. });
  308. const alarmList = computed(() => warningDetail.value.alarmList || []);
  309. const attachmentList = computed(() => warningDetail.value.attachmentList || []);
  310. const disposalHistory = computed(() => warningDetail.value.disposalHistory || []);
  311. const hasArchive = computed(() => !!warningDetail.value.electronicArchive);
  312. const curveSummary = computed(() => {
  313. if (!alarmList.value.length) {
  314. return {
  315. title: "曲线分析",
  316. description: "当前预警暂无关联报警数据,无法生成设备报警曲线分析。"
  317. };
  318. }
  319. const maxAlarm = [...alarmList.value].sort((a, b) => Number(b.actual_value || b.actualValue || 0) - Number(a.actual_value || a.actualValue || 0))[0];
  320. return {
  321. title: "曲线分析",
  322. description: `当前共关联 ${alarmList.value.length} 条报警记录,峰值实际值为 ${maxAlarm?.actual_value || maxAlarm?.actualValue || "-"},建议结合预警附件与现场情况形成完整书面报告。`
  323. };
  324. });
  325. const environmentConclusion = computed(() => {
  326. if (!warningDetail.value.basicInfo) {
  327. return "请选择预警后查看周边要素分析结论。";
  328. }
  329. return `本次预警“${currentBasicInfo.value.warningNo || "-"}”当前包含 ${alarmList.value.length} 条关联报警、${disposalHistory.value.length} 条处置记录、${attachmentList.value.length} 份附件资料。结合关联设备“${currentBasicInfo.value.deviceName || currentBasicInfo.value.deviceCode || "-"}”与电子存档情况,建议在上传报告时补充现场核查结论、报警峰值说明和后续处置建议。`;
  330. });
  331. function getLevelTagType(level) {
  332. return {
  333. "0": "danger",
  334. "1": "danger",
  335. "2": "warning",
  336. "3": "success",
  337. "4": "info"
  338. }[level] || "info";
  339. }
  340. function getStatusText(status) {
  341. return databaseStatusLabels.value[status] || status || "-"
  342. }
  343. function validateFile(file) {
  344. const allowExt = [".pdf", ".doc", ".docx", ".zip"];
  345. const lowerName = file.name.toLowerCase();
  346. const extOk = allowExt.some((ext) => lowerName.endsWith(ext));
  347. if (!extOk) {
  348. ElMessage.error("仅支持 .pdf、.doc、.docx、.zip 格式");
  349. return false;
  350. }
  351. if (file.size > 20 * 1024 * 1024) {
  352. ElMessage.error("文件大小不能超过20MB");
  353. return false;
  354. }
  355. return true;
  356. }
  357. function handleFileChange(file) {
  358. if (!file?.raw || !validateFile(file.raw)) {
  359. return;
  360. }
  361. uploadedFile.value = file.raw;
  362. }
  363. function handleDrop(event) {
  364. isDragOver.value = false;
  365. const file = event.dataTransfer.files[0];
  366. if (file && validateFile(file)) {
  367. uploadedFile.value = file;
  368. }
  369. }
  370. function removeFile() {
  371. uploadedFile.value = null;
  372. uploadRef.value?.clearFiles?.();
  373. }
  374. function formatFileSize(bytes) {
  375. if (!bytes) return "0 B";
  376. const units = ["B", "KB", "MB", "GB"];
  377. const index = Math.floor(Math.log(bytes) / Math.log(1024));
  378. return `${(bytes / 1024 ** index).toFixed(2)} ${units[index]}`;
  379. }
  380. function buildAttachmentUrl(path) {
  381. if (!path) return "";
  382. return buildMinioAttachmentUrl(path);
  383. }
  384. function normalizeWarningRow(item) {
  385. const warningId = item.warningId || item.warning_id;
  386. const warningLevel = item.warningLevel || item.warning_level;
  387. return {
  388. warningId,
  389. warningNo: item.warningNo || item.warning_no || "",
  390. warningType: item.warningType || item.warning_type,
  391. warningLevel,
  392. warningLevelText: warningLevelLabelMap.value[warningLevel] || warningLevel
  393. };
  394. }
  395. async function loadDicts() {
  396. const [typeRes, levelRes] = await Promise.all([
  397. getAlertTypeDictionaryData({ dictType: "alert_type" }),
  398. getAlertTypeDictionaryData({ dictType: "alert_level" })
  399. ]);
  400. warningTypeOptions.value = typeRes.rows || [];
  401. warningLevelOptions.value = levelRes.rows || [];
  402. }
  403. async function loadWarningOptions() {
  404. pageLoading.value = true;
  405. try {
  406. const res = await getdbyjlistData({
  407. pageNum: 1,
  408. pageSize: 200,
  409. warningNo: queryParams.value.warningNo || undefined,
  410. warningType: queryParams.value.warningType || undefined,
  411. warningLevel: queryParams.value.warningLevel || undefined,
  412. status: queryParams.value.status || undefined
  413. });
  414. warningOptions.value = (res.data?.records || []).map(normalizeWarningRow);
  415. if (!selectedWarningId.value && warningOptions.value.length) {
  416. selectedWarningId.value = warningOptions.value[0].warningId;
  417. await loadWarningDetail(selectedWarningId.value);
  418. } else if (selectedWarningId.value && !warningOptions.value.some((item) => item.warningId === selectedWarningId.value)) {
  419. selectedWarningId.value = "";
  420. warningDetail.value = {};
  421. renderCurveChart();
  422. }
  423. } finally {
  424. pageLoading.value = false;
  425. }
  426. }
  427. async function loadWarningDetail(warningId) {
  428. if (!warningId) {
  429. warningDetail.value = {};
  430. renderCurveChart();
  431. return;
  432. }
  433. pageLoading.value = true;
  434. try {
  435. const res = await getWarningFullDetailData(warningId);
  436. warningDetail.value = res.data || {};
  437. await nextTick();
  438. renderCurveChart();
  439. } finally {
  440. pageLoading.value = false;
  441. }
  442. }
  443. async function handleWarningChange(value) {
  444. await loadWarningDetail(value);
  445. }
  446. function buildChartData() {
  447. const alarms = [...alarmList.value].sort((a, b) => String(a.createTime || "").localeCompare(String(b.createTime || "")));
  448. return {
  449. xData: alarms.map((item) => String(item.createTime || "").slice(5, 16) || "-"),
  450. actualData: alarms.map((item) => Number(item.actual_value || item.actualValue || 0)),
  451. thresholdData: alarms.map((item) => {
  452. const raw = item.warning_value || item.warningValue || "";
  453. const numeric = String(raw).match(/-?\d+(\.\d+)?/);
  454. return numeric ? Number(numeric[0]) : null;
  455. })
  456. };
  457. }
  458. function renderCurveChart() {
  459. if (!curveChartRef.value) return;
  460. if (!curveChart) {
  461. curveChart = echarts.init(curveChartRef.value);
  462. window.addEventListener("resize", resizeChart);
  463. }
  464. const chartData = buildChartData();
  465. curveChart.setOption({
  466. tooltip: { trigger: "axis" },
  467. legend: { top: 0, data: ["实际值", "预警阈值"] },
  468. grid: { left: 40, right: 20, top: 36, bottom: 30, containLabel: true },
  469. xAxis: {
  470. type: "category",
  471. data: chartData.xData.length ? chartData.xData : ["暂无数据"]
  472. },
  473. yAxis: {
  474. type: "value",
  475. name: "数值"
  476. },
  477. series: [
  478. {
  479. name: "实际值",
  480. type: "line",
  481. smooth: true,
  482. data: chartData.actualData.length ? chartData.actualData : [0],
  483. lineStyle: { color: "#e6a23c", width: 2 },
  484. areaStyle: { color: "rgba(230,162,60,0.12)" }
  485. },
  486. {
  487. name: "预警阈值",
  488. type: "line",
  489. smooth: true,
  490. data: chartData.thresholdData.length ? chartData.thresholdData : [0],
  491. lineStyle: { color: "#f56c6c", type: "dashed", width: 2 }
  492. }
  493. ]
  494. });
  495. }
  496. function resizeChart() {
  497. curveChart?.resize();
  498. }
  499. async function submitReport() {
  500. if (!selectedWarningId.value) {
  501. ElMessage.warning("请先选择预警");
  502. return;
  503. }
  504. if (!uploadedFile.value) {
  505. ElMessage.warning("请先选择报告文件");
  506. return;
  507. }
  508. submitLoading.value = true;
  509. try {
  510. const res = await uploadWarningReportData(selectedWarningId.value, [uploadedFile.value]);
  511. if (res.code !== 200) {
  512. ElMessage.error(res.msg || "报告上传失败");
  513. return;
  514. }
  515. ElMessage.success("预警报告上传成功");
  516. removeFile();
  517. reportSummary.value = "";
  518. await loadWarningDetail(selectedWarningId.value);
  519. } finally {
  520. submitLoading.value = false;
  521. }
  522. }
  523. function resetQuery() {
  524. queryParams.value = {
  525. warningNo: "",
  526. warningType: "",
  527. warningLevel: "",
  528. status: ""
  529. };
  530. selectedWarningId.value = "";
  531. warningDetail.value = {};
  532. loadWarningOptions();
  533. }
  534. onMounted(async () => {
  535. await loadDicts();
  536. await loadWarningOptions();
  537. });
  538. onBeforeUnmount(() => {
  539. window.removeEventListener("resize", resizeChart);
  540. curveChart?.dispose();
  541. curveChart = null;
  542. });
  543. </script>
  544. <style scoped>
  545. .warning-report-page {
  546. background: #f5f7fb;
  547. }
  548. .page-card {
  549. border-radius: 16px;
  550. margin-bottom: 16px;
  551. }
  552. .header-card {
  553. background: linear-gradient(135deg, #eff6ff, #f8fbff);
  554. border: 1px solid #dbeafe;
  555. }
  556. .page-header {
  557. display: flex;
  558. justify-content: space-between;
  559. align-items: center;
  560. gap: 16px;
  561. }
  562. .page-title {
  563. font-size: 22px;
  564. font-weight: 700;
  565. color: #1e3a8a;
  566. }
  567. .page-subtitle {
  568. margin-top: 8px;
  569. font-size: 13px;
  570. color: #64748b;
  571. }
  572. .warning-option {
  573. display: flex;
  574. justify-content: space-between;
  575. align-items: center;
  576. }
  577. .report-container {
  578. display: grid;
  579. grid-template-columns: 360px minmax(0, 1fr);
  580. gap: 16px;
  581. }
  582. .section-card {
  583. border-radius: 16px;
  584. margin-bottom: 16px;
  585. }
  586. .section-title {
  587. font-size: 16px;
  588. font-weight: 600;
  589. color: #1f2937;
  590. }
  591. .upload-area {
  592. border: 2px dashed #dcdfe6;
  593. border-radius: 12px;
  594. padding: 28px 16px;
  595. text-align: center;
  596. transition: all 0.2s ease;
  597. background: #fafafa;
  598. }
  599. .upload-area.drag-over {
  600. border-color: #409eff;
  601. background: #ecf5ff;
  602. }
  603. .upload-icon {
  604. font-size: 46px;
  605. color: #909399;
  606. margin-bottom: 10px;
  607. }
  608. .upload-text {
  609. margin-bottom: 14px;
  610. }
  611. .primary-text {
  612. font-size: 14px;
  613. color: #374151;
  614. }
  615. .support-text {
  616. margin-top: 6px;
  617. font-size: 12px;
  618. color: #9ca3af;
  619. }
  620. .file-info,
  621. .attachment-list {
  622. margin-top: 16px;
  623. }
  624. .file-item,
  625. .attachment-item {
  626. display: flex;
  627. align-items: center;
  628. gap: 8px;
  629. padding: 10px 12px;
  630. border-radius: 10px;
  631. background: #f8fafc;
  632. border: 1px solid #edf2f7;
  633. }
  634. .attachment-item + .attachment-item {
  635. margin-top: 8px;
  636. }
  637. .file-name,
  638. .attachment-name {
  639. flex: 1;
  640. font-size: 13px;
  641. color: #303133;
  642. }
  643. .file-size,
  644. .attachment-time {
  645. font-size: 12px;
  646. color: #909399;
  647. }
  648. .remove-file {
  649. color: #f56c6c;
  650. cursor: pointer;
  651. font-size: 16px;
  652. }
  653. .report-description {
  654. margin-top: 20px;
  655. }
  656. .field-title {
  657. margin-bottom: 10px;
  658. font-size: 14px;
  659. font-weight: 600;
  660. color: #374151;
  661. }
  662. .submit-btn {
  663. width: 100%;
  664. margin-top: 16px;
  665. }
  666. .curve-chart {
  667. width: 100%;
  668. height: 280px;
  669. margin-bottom: 16px;
  670. }
  671. .factors-grid {
  672. display: grid;
  673. grid-template-columns: repeat(4, minmax(0, 1fr));
  674. gap: 12px;
  675. margin-bottom: 16px;
  676. }
  677. .factor-card {
  678. padding: 16px;
  679. border-radius: 14px;
  680. background: #f8fafc;
  681. border: 1px solid #edf2f7;
  682. }
  683. .factor-label {
  684. font-size: 13px;
  685. color: #64748b;
  686. }
  687. .factor-value {
  688. margin: 10px 0 6px;
  689. font-size: 24px;
  690. font-weight: 700;
  691. color: #111827;
  692. }
  693. .factor-desc {
  694. font-size: 12px;
  695. color: #94a3b8;
  696. }
  697. .analysis-text-card {
  698. margin-top: 8px;
  699. }
  700. .warning-description {
  701. margin-top: 20px;
  702. background: #fef9e6;
  703. border-left: 4px solid #e6a23c;
  704. border-radius: 12px;
  705. padding: 16px 20px;
  706. }
  707. .desc-header {
  708. display: flex;
  709. align-items: center;
  710. gap: 8px;
  711. margin-bottom: 12px;
  712. }
  713. .desc-header h4 {
  714. margin: 0;
  715. font-size: 16px;
  716. font-weight: 600;
  717. color: #e6a23c;
  718. }
  719. .desc-content {
  720. font-size: 14px;
  721. color: #303133;
  722. line-height: 1.8;
  723. }
  724. .desc-content p {
  725. margin: 0 0 8px;
  726. }
  727. @media (max-width: 1200px) {
  728. .report-container {
  729. grid-template-columns: 1fr;
  730. }
  731. .factors-grid {
  732. grid-template-columns: repeat(2, minmax(0, 1fr));
  733. }
  734. }
  735. @media (max-width: 768px) {
  736. .factors-grid {
  737. grid-template-columns: 1fr;
  738. }
  739. }
  740. </style>