LRelease.vue 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215
  1. <!-- 预警发布 -->
  2. <template>
  3. <div class="app-container warning-release-page">
  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 class="header-actions">
  11. <el-button v-hasPermi="['warning:warning:add']" type="primary" icon="Plus" @click="handleAdd">新增预警</el-button>
  12. <el-button icon="Refresh" @click="loadList">刷新</el-button>
  13. </div>
  14. </div>
  15. </el-card>
  16. <el-card shadow="never" class="page-card">
  17. <el-form :model="queryParams" :inline="true">
  18. <el-form-item label="预警名称">
  19. <el-input
  20. v-model="queryParams.warningName"
  21. placeholder="请输入预警名称"
  22. clearable
  23. style="width: 220px"
  24. @keyup.enter="handleQuery"
  25. />
  26. </el-form-item>
  27. <el-form-item label="预警类型">
  28. <el-select v-model="queryParams.warningType" placeholder="请选择预警类型" clearable style="width: 180px">
  29. <el-option
  30. v-for="item in warningTypeOptions"
  31. :key="item.dictValue"
  32. :label="item.dictLabel"
  33. :value="item.dictValue"
  34. />
  35. </el-select>
  36. </el-form-item>
  37. <el-form-item label="预警级别">
  38. <el-select v-model="queryParams.warningLevel" placeholder="请选择预警级别" clearable style="width: 180px">
  39. <el-option
  40. v-for="item in warningLevelOptions"
  41. :key="item.dictValue"
  42. :label="item.dictLabel"
  43. :value="item.dictValue"
  44. />
  45. </el-select>
  46. </el-form-item>
  47. <el-form-item label="发布时间">
  48. <el-date-picker
  49. v-model="queryParams.dateRange"
  50. type="datetimerange"
  51. value-format="YYYY-MM-DD HH:mm:ss"
  52. range-separator="至"
  53. start-placeholder="开始时间"
  54. end-placeholder="结束时间"
  55. style="width: 360px"
  56. />
  57. </el-form-item>
  58. <el-form-item>
  59. <el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
  60. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  61. </el-form-item>
  62. </el-form>
  63. </el-card>
  64. <el-card shadow="never" class="page-card">
  65. <el-table v-loading="loading" :data="warningList">
  66. <el-table-column label="序号" width="70" align="center">
  67. <template #default="scope">
  68. {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
  69. </template>
  70. </el-table-column>
  71. <el-table-column label="预警编号" prop="warningNo" width="180" />
  72. <el-table-column label="预警名称" prop="warningName" min-width="180" show-overflow-tooltip />
  73. <el-table-column label="预警类型" prop="warningTypeText" width="120" />
  74. <el-table-column label="预警级别" width="110" align="center">
  75. <template #default="scope">
  76. <el-tag :type="getLevelTagType(scope.row.warningLevel)">
  77. {{ scope.row.warningLevelText || scope.row.warningLevel || "-" }}
  78. </el-tag>
  79. </template>
  80. </el-table-column>
  81. <el-table-column label="预警位置" prop="location" min-width="180" show-overflow-tooltip />
  82. <el-table-column label="发布时间" width="180">
  83. <template #default="scope">{{ formatDateTime(scope.row.publishTime) }}</template>
  84. </el-table-column>
  85. <el-table-column label="发布人" prop="publisher" width="120" />
  86. <el-table-column label="状态" width="120" align="center">
  87. <template #default="scope">
  88. <el-tag :type="getStatusTagType(scope.row.status)">
  89. {{ getStatusText(scope.row.status) }}
  90. </el-tag>
  91. </template>
  92. </el-table-column>
  93. <el-table-column label="操作" fixed="right" width="280" align="center">
  94. <template #default="scope">
  95. <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
  96. <el-button v-if="scope.row.status === 'DRAFT'" v-hasPermi="['warning:warning:edit']" link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
  97. <el-button v-if="scope.row.status === 'DRAFT'" v-hasPermi="['warning:warning:publish']" link type="success" @click="handlePublish(scope.row)">发布</el-button>
  98. <el-button v-if="scope.row.status === 'DRAFT'" v-hasPermi="['warning:warning:remove']" link type="danger" @click="handleDelete(scope.row)">删除</el-button>
  99. </template>
  100. </el-table-column>
  101. </el-table>
  102. <pagination
  103. v-show="total > 0"
  104. :total="total"
  105. v-model:page="queryParams.pageNum"
  106. v-model:limit="queryParams.pageSize"
  107. @pagination="loadList"
  108. />
  109. </el-card>
  110. <el-dialog v-model="editVisible" :title="dialogTitle" width="860px" append-to-body @close="resetForm">
  111. <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
  112. <el-row :gutter="16">
  113. <el-col :span="12">
  114. <el-form-item label="预警类型" prop="warningType">
  115. <el-select v-model="formData.warningType" placeholder="请选择预警类型" style="width: 100%" @change="handleTypeLevelChange">
  116. <el-option
  117. v-for="item in warningTypeOptions"
  118. :key="item.dictValue"
  119. :label="item.dictLabel"
  120. :value="item.dictValue"
  121. />
  122. </el-select>
  123. </el-form-item>
  124. </el-col>
  125. <el-col :span="12">
  126. <el-form-item label="预警级别" prop="warningLevel">
  127. <el-select v-model="formData.warningLevel" placeholder="请选择预警级别" style="width: 100%" @change="handleTypeLevelChange">
  128. <el-option
  129. v-for="item in warningLevelOptions"
  130. :key="item.dictValue"
  131. :label="item.dictLabel"
  132. :value="item.dictValue"
  133. />
  134. </el-select>
  135. </el-form-item>
  136. </el-col>
  137. <el-col :span="12">
  138. <el-form-item label="预警专项">
  139. <el-input v-model="formData.warningSpecial" placeholder="请输入预警专项" />
  140. </el-form-item>
  141. </el-col>
  142. <el-col :span="12">
  143. <el-form-item label="权属单位">
  144. <el-input v-model="formData.ownershipUnit" placeholder="请输入权属单位" />
  145. </el-form-item>
  146. </el-col>
  147. <el-col :span="12">
  148. <el-form-item label="发布人">
  149. <el-input :model-value="formData.publisher || '发布时确定'" disabled />
  150. </el-form-item>
  151. </el-col>
  152. <el-col :span="12">
  153. <el-form-item label="处置人">
  154. <el-input v-model="formData.handler" placeholder="请选择处置人" readonly @click="openUserPicker">
  155. <template #suffix>
  156. <el-icon style="cursor: pointer;" @click="openUserPicker"><Search /></el-icon>
  157. </template>
  158. </el-input>
  159. </el-form-item>
  160. </el-col>
  161. <el-col :span="12">
  162. <el-form-item label="核查人">
  163. <el-input v-model="formData.reviewer" placeholder="默认发布人,可选择其他核查人" readonly clearable @click="openReviewerPicker">
  164. <template #suffix><el-icon style="cursor:pointer" @click="openReviewerPicker"><Search /></el-icon></template>
  165. </el-input>
  166. </el-form-item>
  167. </el-col>
  168. <el-col :span="24">
  169. <el-form-item label="预警名称" prop="warningName">
  170. <el-input v-model="formData.warningName" placeholder="请输入预警名称" maxlength="100" show-word-limit />
  171. </el-form-item>
  172. </el-col>
  173. <el-col :span="24">
  174. <el-form-item label="预警位置" prop="location">
  175. <el-input v-model="formData.location" placeholder="请输入预警位置" />
  176. </el-form-item>
  177. </el-col>
  178. <el-col :span="24">
  179. <el-form-item label="关联设备">
  180. <el-input v-model="formData.deviceName" placeholder="点击选择关联设备" readonly @click="openDevicePicker">
  181. <template #append>
  182. <el-button :icon="Search" @click="openDevicePicker">选择</el-button>
  183. </template>
  184. </el-input>
  185. </el-form-item>
  186. </el-col>
  187. <el-col :span="12">
  188. <el-form-item label="经度">
  189. <el-input v-model="formData.longitude" placeholder="请输入经度" />
  190. </el-form-item>
  191. </el-col>
  192. <el-col :span="12">
  193. <el-form-item label="纬度">
  194. <el-input v-model="formData.latitude" placeholder="请输入纬度" />
  195. </el-form-item>
  196. </el-col>
  197. <el-col :span="24">
  198. <el-form-item label="内容描述" prop="warningContent">
  199. <el-input
  200. v-model="formData.warningContent"
  201. type="textarea"
  202. :rows="4"
  203. maxlength="500"
  204. show-word-limit
  205. placeholder="请输入预警内容描述"
  206. />
  207. </el-form-item>
  208. </el-col>
  209. <el-col :span="24" v-if="reasonOptions.length">
  210. <el-form-item label="推荐原因">
  211. <div class="reason-wrap">
  212. <el-tag
  213. v-for="item in reasonOptions"
  214. :key="item.reasonId"
  215. class="reason-tag"
  216. effect="plain"
  217. @click="appendReason(item)"
  218. >
  219. {{ item.reasonName }}
  220. </el-tag>
  221. </div>
  222. </el-form-item>
  223. </el-col>
  224. <el-col :span="24">
  225. <el-form-item label="备注">
  226. <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="请输入备注" />
  227. </el-form-item>
  228. </el-col>
  229. <el-col :span="24">
  230. <el-form-item label="附件上传">
  231. <div class="attachment-panel">
  232. <el-upload
  233. ref="uploadRef"
  234. action="#"
  235. :auto-upload="false"
  236. :show-file-list="false"
  237. multiple
  238. :on-change="handleUploadChange"
  239. :on-exceed="handleUploadExceed"
  240. :limit="9"
  241. accept=".pdf,.doc,.docx,.xls,.xlsx,.zip,.jpg,.jpeg,.png"
  242. >
  243. <el-button type="primary">选择附件</el-button>
  244. </el-upload>
  245. <div class="attachment-tip">支持常见文档、压缩包和图片,新增或编辑时会随保存接口一起上传。</div>
  246. <div v-if="formAttachments.length" class="attachment-list">
  247. <div v-for="item in formAttachments" :key="item.uid || item.attachmentId || item.name" class="attachment-item">
  248. <el-link
  249. v-if="item.url"
  250. :href="item.url"
  251. target="_blank"
  252. type="primary"
  253. class="attachment-link"
  254. >
  255. {{ item.name }}
  256. </el-link>
  257. <span v-else class="attachment-link">{{ item.name }}</span>
  258. <span class="attachment-meta">{{ item.sizeText || item.createTime || "" }}</span>
  259. <el-button link type="danger" @click="removeAttachment(item)">移除</el-button>
  260. </div>
  261. </div>
  262. </div>
  263. </el-form-item>
  264. </el-col>
  265. </el-row>
  266. </el-form>
  267. <template #footer>
  268. <div class="dialog-footer">
  269. <el-button @click="editVisible = false">取 消</el-button>
  270. <el-button type="primary" :loading="submitLoading" @click="submitForm">保 存</el-button>
  271. </div>
  272. </template>
  273. </el-dialog>
  274. <el-drawer v-model="detailVisible" title="预警详情" size="900px" append-to-body class="detail-drawer" direction="rtl">
  275. <template v-if="detailData.basicInfo">
  276. <el-descriptions :column="2" border>
  277. <el-descriptions-item label="预警编号">{{ detailData.basicInfo.warningNo || "-" }}</el-descriptions-item>
  278. <el-descriptions-item label="预警名称">{{ detailData.basicInfo.warningName || "-" }}</el-descriptions-item>
  279. <el-descriptions-item label="预警类型">{{ formatTypeText(detailData.basicInfo.warningType) }}</el-descriptions-item>
  280. <el-descriptions-item label="预警级别">
  281. <el-tag :type="getLevelTagType(detailData.basicInfo.warningLevel)">
  282. {{ formatLevelText(detailData.basicInfo.warningLevel) }}
  283. </el-tag>
  284. </el-descriptions-item>
  285. <el-descriptions-item label="预警专项">{{ detailData.basicInfo.warningSpecial || "-" }}</el-descriptions-item>
  286. <el-descriptions-item label="权属单位">{{ detailData.basicInfo.ownershipUnit || "-" }}</el-descriptions-item>
  287. <el-descriptions-item label="发布人">{{ detailData.basicInfo.publisher || "-" }}</el-descriptions-item>
  288. <el-descriptions-item label="处置人">{{ detailData.basicInfo.handler || "-" }}</el-descriptions-item>
  289. <el-descriptions-item label="预警位置" :span="2">{{ detailData.basicInfo.location || "-" }}</el-descriptions-item>
  290. <el-descriptions-item label="关联设备" :span="2">
  291. <template v-if="detailData.basicInfo.deviceCode || detailData.basicInfo.deviceName">
  292. {{ detailData.basicInfo.deviceName || "-" }}
  293. <span v-if="detailData.basicInfo.deviceCode" style="color: #909399;">({{ detailData.basicInfo.deviceCode }})</span>
  294. </template>
  295. <span v-else>-</span>
  296. </el-descriptions-item>
  297. <el-descriptions-item label="发布时间">{{ formatDateTime(detailData.basicInfo.publishTime) }}</el-descriptions-item>
  298. <el-descriptions-item label="状态">{{ getStatusText(detailData.basicInfo.status) }}</el-descriptions-item>
  299. <el-descriptions-item label="内容描述" :span="2">{{ detailData.basicInfo.warningContent || "-" }}</el-descriptions-item>
  300. <el-descriptions-item label="备注" :span="2">{{ detailData.basicInfo.remark || "-" }}</el-descriptions-item>
  301. </el-descriptions>
  302. <el-divider content-position="left">关联报警</el-divider>
  303. <el-table :data="detailData.alarmList || []" border size="small">
  304. <el-table-column label="报警ID" prop="alarmId" min-width="160" />
  305. <el-table-column label="报警名称" prop="alarmName" min-width="180" show-overflow-tooltip />
  306. <el-table-column label="报警类型" min-width="120">
  307. <template #default="scope">
  308. {{ formatTypeText(scope.row.alarmType || scope.row.warningType) }}
  309. </template>
  310. </el-table-column>
  311. </el-table>
  312. <el-divider content-position="left">处置记录</el-divider>
  313. <el-table :data="detailData.disposalHistory || detailData.disposalList || []" border size="small">
  314. <el-table-column label="处置类型" prop="disposalTypeName" min-width="120" />
  315. <el-table-column label="处置人" prop="disposalUser" min-width="120" />
  316. <el-table-column label="处置内容" prop="disposalContent" min-width="220" show-overflow-tooltip />
  317. <el-table-column label="处置时间" prop="createTime" min-width="180" />
  318. </el-table>
  319. <el-divider content-position="left">附件信息</el-divider>
  320. <div v-if="(detailData.attachmentList || []).length" class="detail-attachment-list">
  321. <div v-for="item in detailData.attachmentList" :key="item.attachmentId || item.attachmentName" class="detail-attachment-item">
  322. <el-link :href="buildAttachmentUrl(item.attachmentUrl)" target="_blank" type="primary">
  323. {{ item.attachmentName || "附件" }}
  324. </el-link>
  325. <span>{{ item.createTime || "-" }}</span>
  326. </div>
  327. </div>
  328. <el-empty v-else description="暂无附件信息" />
  329. </template>
  330. </el-drawer>
  331. <!-- 关联设备选择对话框 -->
  332. <el-dialog v-model="devicePickerVisible" title="选择关联设备" width="860px" append-to-body @open="loadDevicePickerData">
  333. <div class="device-picker-container">
  334. <div class="device-picker-left">
  335. <div class="picker-title">设备类型</div>
  336. <button
  337. :class="{ active: deviceRootTypeId === '' }"
  338. class="device-type-item"
  339. @click="selectDeviceRootType('')"
  340. >
  341. 全部类型
  342. </button>
  343. <button
  344. v-for="item in deviceRootTypes"
  345. :key="item.id"
  346. :class="{ active: String(deviceRootTypeId) === String(item.id) }"
  347. class="device-type-item"
  348. @click="selectDeviceRootType(item.id)"
  349. >
  350. {{ item.typeName }}
  351. </button>
  352. </div>
  353. <div class="device-picker-right">
  354. <el-form :inline="true" @submit.prevent>
  355. <el-form-item>
  356. <el-input v-model="deviceQuery.equipmentCode" placeholder="设备编码" clearable style="width: 180px" @keyup.enter="loadDeviceList" />
  357. </el-form-item>
  358. <el-form-item>
  359. <el-input v-model="deviceQuery.equipmentName" placeholder="设备名称" clearable style="width: 180px" @keyup.enter="loadDeviceList" />
  360. </el-form-item>
  361. <el-form-item>
  362. <el-button type="primary" @click="loadDeviceList">查询</el-button>
  363. </el-form-item>
  364. </el-form>
  365. <el-table v-loading="deviceLoading" :data="deviceList" border size="small" height="320">
  366. <el-table-column prop="equipmentCode" label="设备编码" min-width="180" show-overflow-tooltip />
  367. <el-table-column prop="equipmentName" label="设备名称" min-width="180" show-overflow-tooltip />
  368. <el-table-column label="设备类型" min-width="140">
  369. <template #default="{ row }">{{ deviceTypeNameMap[row.equipmentTypeId] || row.equipmentTypeName || "-" }}</template>
  370. </el-table-column>
  371. <el-table-column label="操作" width="90" align="center">
  372. <template #default="{ row }">
  373. <el-button link type="primary" @click="confirmDevice(row)">选择</el-button>
  374. </template>
  375. </el-table-column>
  376. </el-table>
  377. <el-pagination
  378. v-model:current-page="deviceQuery.pageNum"
  379. v-model:page-size="deviceQuery.pageSize"
  380. :total="deviceTotal"
  381. :page-sizes="[10, 20, 50]"
  382. layout="total, sizes, prev, pager, next"
  383. style="margin-top: 12px; justify-content: flex-end"
  384. @size-change="loadDeviceList"
  385. @current-change="loadDeviceList"
  386. />
  387. </div>
  388. </div>
  389. </el-dialog>
  390. <!-- 处置人选择对话框 -->
  391. <el-dialog v-model="userPickerVisible" :title="pickerTarget === 'reviewer' ? '选择核查人' : '选择处置人'" width="900px" append-to-body @open="loadUserPickerData">
  392. <div class="user-picker-container">
  393. <!-- 左侧部门树 -->
  394. <div class="user-picker-left">
  395. <div class="picker-title">部门列表</div>
  396. <el-input v-model="deptFilterText" placeholder="搜索部门" clearable style="margin-bottom: 12px;" />
  397. <el-tree
  398. ref="deptTreeRef"
  399. :data="deptTree"
  400. :props="{ label: 'label', children: 'children' }"
  401. :filter-node-method="filterDeptNode"
  402. :highlight-current="true"
  403. default-expand-all
  404. @node-click="handleDeptClick"
  405. />
  406. </div>
  407. <!-- 右侧用户列表 -->
  408. <div class="user-picker-right">
  409. <div class="picker-title">
  410. 用户列表
  411. <span v-if="currentDeptName" class="current-dept">({{ currentDeptName }})</span>
  412. </div>
  413. <el-input v-model="userFilterText" placeholder="搜索用户" clearable style="margin-bottom: 12px;" @input="filterUserList" />
  414. <el-table
  415. v-loading="userPickerLoading"
  416. :data="filteredUserList"
  417. highlight-current-row
  418. @current-change="handleUserCurrentChange"
  419. height="400"
  420. >
  421. <el-table-column label="用户名称" prop="userName" min-width="120" show-overflow-tooltip />
  422. <el-table-column label="用户昵称" prop="nickName" min-width="120" show-overflow-tooltip />
  423. <el-table-column label="部门" prop="deptName" min-width="140" show-overflow-tooltip />
  424. <el-table-column label="手机号码" prop="phonenumber" width="130" />
  425. </el-table>
  426. </div>
  427. </div>
  428. <template #footer>
  429. <div class="dialog-footer">
  430. <el-button @click="userPickerVisible = false">取 消</el-button>
  431. <el-button type="primary" :disabled="!selectedUser" @click="confirmSelectUser">确 定</el-button>
  432. </div>
  433. </template>
  434. </el-dialog>
  435. </div>
  436. </template>
  437. <script setup>
  438. import {computed, onMounted, reactive, ref, watch} from "vue";
  439. import {ElMessage, ElMessageBox} from "element-plus";
  440. import { Search } from "@element-plus/icons-vue";
  441. import { parseTime } from "@/utils/ruoyi";
  442. import { warningStatusLabels } from "../warningWorkflow";
  443. import { deptTreeSelect, listUser } from "@/api/system/user";
  444. import { listEquipment, listEquipmentTypeAll } from "@/api/pipeNetwork/pdeviceInform";
  445. import {
  446. deleteYjxxData,
  447. getAlertTypeDictionaryData,
  448. getdbyjlistData,
  449. getWarningDetailData,
  450. getWarningReasonListData,
  451. releaseYjxxData,
  452. submitYjxxData,
  453. updateYjxxData
  454. } from "@/api/smxyjldczxt/yjxxfb/yjfb.js";
  455. const loading = ref(false);
  456. const submitLoading = ref(false);
  457. const total = ref(0);
  458. const warningList = ref([]);
  459. const warningTypeOptions = ref([]);
  460. const warningLevelOptions = ref([]);
  461. const reasonOptions = ref([]);
  462. const uploadRef = ref();
  463. const formAttachments = ref([]);
  464. const minioBaseUrl = (import.meta.env.VITE_APP_MINIO_BASE_URL || "").trim();
  465. const editVisible = ref(false);
  466. const detailVisible = ref(false);
  467. const userPickerVisible = ref(false);
  468. const pickerTarget = ref('handler');
  469. const dialogTitle = ref("新增预警");
  470. const isEdit = ref(false);
  471. const formRef = ref();
  472. const detailData = ref({});
  473. // 关联设备选择器
  474. const devicePickerVisible = ref(false);
  475. const deviceRootTypeId = ref("");
  476. const deviceRootTypes = ref([]);
  477. const deviceList = ref([]);
  478. const deviceTotal = ref(0);
  479. const deviceLoading = ref(false);
  480. const deviceTypeNameMap = ref({});
  481. const deviceQuery = reactive({
  482. pageNum: 1,
  483. pageSize: 10,
  484. equipmentCode: "",
  485. equipmentName: ""
  486. });
  487. // 用户选择器相关
  488. const deptTree = ref([]);
  489. const userList = ref([]);
  490. const filteredUserList = ref([]);
  491. const selectedDeptId = ref("");
  492. const currentDeptName = ref("");
  493. const selectedUser = ref(null);
  494. const userPickerLoading = ref(false);
  495. const deptFilterText = ref("");
  496. const userFilterText = ref("");
  497. const deptTreeRef = ref();
  498. const queryParams = reactive({
  499. pageNum: 1,
  500. pageSize: 10,
  501. warningName: "",
  502. warningType: "",
  503. warningLevel: "",
  504. dateRange: []
  505. });
  506. const formData = reactive(createEmptyForm());
  507. const formRules = {
  508. warningType: [{ required: true, message: "请选择预警类型", trigger: "change" }],
  509. warningLevel: [{ required: true, message: "请选择预警级别", trigger: "change" }],
  510. warningName: [{ required: true, message: "请输入预警名称", trigger: "blur" }],
  511. location: [{ required: true, message: "请输入预警位置", trigger: "blur" }],
  512. warningContent: [{ required: true, message: "请输入预警内容描述", trigger: "blur" }]
  513. };
  514. const warningTypeLabelMap = computed(() => {
  515. return warningTypeOptions.value.reduce((acc, item) => {
  516. acc[item.dictValue] = item.dictLabel;
  517. return acc;
  518. }, {});
  519. });
  520. const warningLevelLabelMap = computed(() => {
  521. return warningLevelOptions.value.reduce((acc, item) => {
  522. acc[item.dictValue] = item.dictLabel;
  523. return acc;
  524. }, {});
  525. });
  526. function createEmptyForm() {
  527. return {
  528. warningId: "",
  529. warningNo: "",
  530. warningName: "",
  531. warningType: "",
  532. warningLevel: "",
  533. warningSpecial: "",
  534. deviceCode: "",
  535. deviceName: "",
  536. location: "",
  537. longitude: "",
  538. latitude: "",
  539. ownershipUnit: "",
  540. publisher: "",
  541. publishTime: "",
  542. handler: "",
  543. reviewer: "",
  544. status: "DRAFT",
  545. warningContent: "",
  546. remark: ""
  547. };
  548. }
  549. function resetForm() {
  550. Object.assign(formData, createEmptyForm());
  551. reasonOptions.value = [];
  552. formAttachments.value = [];
  553. uploadRef.value?.clearFiles?.();
  554. formRef.value?.clearValidate();
  555. }
  556. function formatFileSize(bytes) {
  557. if (!bytes) return "";
  558. const units = ["B", "KB", "MB", "GB"];
  559. const index = Math.floor(Math.log(bytes) / Math.log(1024));
  560. return `${(bytes / 1024 ** index).toFixed(2)} ${units[index]}`;
  561. }
  562. function buildAttachmentUrl(path) {
  563. if (!path) return "";
  564. if (/^https?:\/\//i.test(path)) {
  565. return path;
  566. }
  567. const base = minioBaseUrl.endsWith("/") ? minioBaseUrl : `${minioBaseUrl}/`;
  568. return `${base}${String(path).replace(/^\/+/, "")}`;
  569. }
  570. function normalizeExistingAttachment(item) {
  571. return {
  572. uid: item.attachmentId || item.attachmentName,
  573. name: item.attachmentName || "附件",
  574. url: buildAttachmentUrl(item.attachmentUrl),
  575. attachmentId: item.attachmentId,
  576. attachmentUrl: item.attachmentUrl,
  577. createTime: item.createTime || "",
  578. raw: null,
  579. sizeText: item.attachmentSize ? formatFileSize(item.attachmentSize) : ""
  580. };
  581. }
  582. function handleUploadChange(file) {
  583. if (!file?.raw) return;
  584. formAttachments.value.push({
  585. uid: file.uid,
  586. name: file.name,
  587. raw: file.raw,
  588. url: "",
  589. sizeText: formatFileSize(file.size)
  590. });
  591. }
  592. function handleUploadExceed() {
  593. ElMessage.warning("最多上传 9 个附件");
  594. }
  595. function removeAttachment(item) {
  596. formAttachments.value = formAttachments.value.filter((current) => current.uid !== item.uid);
  597. }
  598. function formatTypeText(value) {
  599. return warningTypeLabelMap.value[value] || value || "-";
  600. }
  601. function formatLevelText(value) {
  602. return warningLevelLabelMap.value[value] || value || "-";
  603. }
  604. function formatDateTime(value) {
  605. return value ? parseTime(value) : "-";
  606. }
  607. function getLevelTagType(level) {
  608. return {
  609. "0": "danger",
  610. "1": "danger",
  611. "2": "warning",
  612. "3": "success",
  613. "4": "info"
  614. }[level] || "info";
  615. }
  616. function getStatusText(status) {
  617. return warningStatusLabels[status] || status || "-";
  618. }
  619. function getStatusTagType(status) {
  620. // 按照预警状态语义设置 tag 颜色
  621. return {
  622. DRAFT: "info", // 草稿 - 灰色(未发布)
  623. PENDING: "warning", // 待办 - 黄色(等待处置)
  624. RECEIVED: "warning", // 已接收 - 黄色
  625. PROCESSING: "warning", // 处置中 - 黄色(进行中)
  626. HANDLED: "success", // 已处置 - 绿色(已完成)
  627. CLOSED: "info" // 已解除 - 灰色
  628. }[status] || "info";
  629. }
  630. function normalizeWarningRow(item) {
  631. const row = { ...item };
  632. row.warningId = item.warningId || item.warning_id || "";
  633. row.warningNo = item.warningNo || item.warning_no || "";
  634. row.warningName = item.warningName || item.warning_name || "";
  635. row.warningType = item.warningType || item.warning_type || "";
  636. row.warningLevel = item.warningLevel || item.warning_level || "";
  637. row.warningSpecial = item.warningSpecial || item.warning_special || "";
  638. row.location = item.location || "";
  639. row.longitude = item.longitude || "";
  640. row.latitude = item.latitude || "";
  641. row.ownershipUnit = item.ownershipUnit || item.ownership_unit || "";
  642. row.publisher = item.publisher || "";
  643. row.publishTime = item.publishTime || item.publish_time || "";
  644. row.handler = item.handler || "";
  645. row.reviewer = item.reviewer || "";
  646. row.status = item.status || "";
  647. row.warningContent = item.warningContent || item.warning_content || "";
  648. row.deviceCode = item.deviceCode || item.device_code || "";
  649. row.deviceName = item.deviceName || item.device_name || "";
  650. row.remark = item.remark || "";
  651. row.warningTypeText = formatTypeText(row.warningType);
  652. row.warningLevelText = formatLevelText(row.warningLevel);
  653. return row;
  654. }
  655. function normalizeAlarmRow(item) {
  656. return {
  657. ...item,
  658. alarmId: item.alarmId || item.alarm_id || "",
  659. alarmName: item.alarmName || item.alarm_name || item.deviceName || item.device_name || "",
  660. alarmType: item.alarmType || item.alarm_type || item.warningType || item.warning_type || ""
  661. };
  662. }
  663. function normalizeDisposalRow(item) {
  664. return {
  665. ...item,
  666. disposalTypeName: item.disposalTypeName || item.disposal_type_name || item.disposalType || item.disposal_type || "",
  667. disposalUser: item.disposalUser || item.disposal_user || "",
  668. disposalContent: item.disposalContent || item.disposal_content || "",
  669. createTime: item.createTime || item.create_time || ""
  670. };
  671. }
  672. async function loadDicts() {
  673. const [typeResult, levelResult] = await Promise.allSettled([
  674. getAlertTypeDictionaryData({ dictType: "alert_type" }),
  675. getAlertTypeDictionaryData({ dictType: "alert_level" })
  676. ]);
  677. if (typeResult.status === "fulfilled") {
  678. warningTypeOptions.value = typeResult.value.rows || [];
  679. }
  680. if (levelResult.status === "fulfilled") {
  681. warningLevelOptions.value = levelResult.value.rows || [];
  682. }
  683. }
  684. async function loadReasonOptions() {
  685. if (!formData.warningType || !formData.warningLevel) {
  686. reasonOptions.value = [];
  687. return;
  688. }
  689. const res = await getWarningReasonListData({
  690. warningType: formData.warningType,
  691. warningLevel: formData.warningLevel
  692. });
  693. reasonOptions.value = res.data || [];
  694. }
  695. function handleTypeLevelChange() {
  696. loadReasonOptions();
  697. }
  698. function appendReason(item) {
  699. const content = item.description ? `${item.reasonName}:${item.description}` : item.reasonName;
  700. formData.warningContent = formData.warningContent
  701. ? `${formData.warningContent}\n${content}`
  702. : content;
  703. }
  704. async function loadList() {
  705. loading.value = true;
  706. try {
  707. const params = {
  708. pageNum: queryParams.pageNum,
  709. pageSize: queryParams.pageSize,
  710. warningName: queryParams.warningName,
  711. warningType: queryParams.warningType,
  712. warningLevel: queryParams.warningLevel,
  713. startTime: queryParams.dateRange?.[0] || "",
  714. endTime: queryParams.dateRange?.[1] || ""
  715. };
  716. const res = await getdbyjlistData(params);
  717. warningList.value = (res.data?.records || []).map(normalizeWarningRow);
  718. total.value = res.data?.total || 0;
  719. } catch {
  720. warningList.value = [];
  721. total.value = 0;
  722. } finally {
  723. loading.value = false;
  724. }
  725. }
  726. function handleQuery() {
  727. queryParams.pageNum = 1;
  728. loadList();
  729. }
  730. function resetQuery() {
  731. queryParams.pageNum = 1;
  732. queryParams.pageSize = 10;
  733. queryParams.warningName = "";
  734. queryParams.warningType = "";
  735. queryParams.warningLevel = "";
  736. queryParams.dateRange = [];
  737. loadList();
  738. }
  739. // 部门树过滤
  740. watch(deptFilterText, (val) => {
  741. deptTreeRef.value?.filter(val);
  742. });
  743. function filterDeptNode(value, data) {
  744. if (!value) return true;
  745. return data.label.indexOf(value) !== -1;
  746. }
  747. // 用户选择器相关函数
  748. async function loadUserPickerData() {
  749. userPickerLoading.value = true;
  750. try {
  751. await loadDeptTree();
  752. // 默认加载第一个部门的用户
  753. if (deptTree.value.length > 0) {
  754. const firstDept = deptTree.value[0];
  755. selectedDeptId.value = firstDept.id;
  756. currentDeptName.value = firstDept.label;
  757. await loadUserList(firstDept.id);
  758. }
  759. } finally {
  760. userPickerLoading.value = false;
  761. }
  762. }
  763. async function loadDeptTree() {
  764. const res = await deptTreeSelect();
  765. deptTree.value = res.data || [];
  766. }
  767. async function loadUserList(deptId) {
  768. userPickerLoading.value = true;
  769. try {
  770. const res = await listUser({ deptId, pageNum: 1, pageSize: 100 });
  771. userList.value = res.rows || [];
  772. filteredUserList.value = userList.value;
  773. } finally {
  774. userPickerLoading.value = false;
  775. }
  776. }
  777. function handleDeptClick(data) {
  778. selectedDeptId.value = data.id;
  779. currentDeptName.value = data.label;
  780. loadUserList(data.id);
  781. selectedUser.value = null;
  782. userFilterText.value = "";
  783. }
  784. function filterUserList() {
  785. const keyword = userFilterText.value.toLowerCase();
  786. if (!keyword) {
  787. filteredUserList.value = userList.value;
  788. return;
  789. }
  790. filteredUserList.value = userList.value.filter((user) => {
  791. const userName = (user.userName || "").toLowerCase();
  792. const nickName = (user.nickName || "").toLowerCase();
  793. const deptName = (user.deptName || "").toLowerCase();
  794. return userName.includes(keyword) || nickName.includes(keyword) || deptName.includes(keyword);
  795. });
  796. }
  797. function handleUserCurrentChange(row) {
  798. selectedUser.value = row;
  799. }
  800. function openUserPicker() {
  801. pickerTarget.value = 'handler';
  802. selectedUser.value = null;
  803. userFilterText.value = "";
  804. deptFilterText.value = "";
  805. userPickerVisible.value = true;
  806. }
  807. function openReviewerPicker() {
  808. pickerTarget.value = 'reviewer';
  809. selectedUser.value = null;
  810. userFilterText.value = '';
  811. deptFilterText.value = '';
  812. userPickerVisible.value = true;
  813. }
  814. function confirmSelectUser() {
  815. if (!selectedUser.value) {
  816. ElMessage.warning("请选择用户");
  817. return;
  818. }
  819. formData[pickerTarget.value] = selectedUser.value.userName;
  820. userPickerVisible.value = false;
  821. }
  822. function openDevicePicker() {
  823. devicePickerVisible.value = true;
  824. }
  825. async function loadDevicePickerData() {
  826. deviceQuery.pageNum = 1;
  827. deviceQuery.equipmentCode = "";
  828. deviceQuery.equipmentName = "";
  829. try {
  830. const typeRes = await listEquipmentTypeAll();
  831. const allTypes = typeRes.data || [];
  832. deviceRootTypes.value = allTypes.filter((t) => String(t.parentId) === "0" || t.parentId === 0 || t.parentId === null || t.parentId === undefined || t.parentId === "");
  833. deviceTypeNameMap.value = allTypes.reduce((acc, t) => {
  834. acc[t.id] = t.typeName;
  835. return acc;
  836. }, {});
  837. } catch (e) {
  838. deviceRootTypes.value = [];
  839. }
  840. await loadDeviceList();
  841. }
  842. function selectDeviceRootType(rootTypeId) {
  843. deviceRootTypeId.value = rootTypeId === "" ? "" : String(rootTypeId);
  844. deviceQuery.pageNum = 1;
  845. loadDeviceList();
  846. }
  847. async function loadDeviceList() {
  848. deviceLoading.value = true;
  849. try {
  850. const res = await listEquipment({
  851. pageNum: deviceQuery.pageNum,
  852. pageSize: deviceQuery.pageSize,
  853. equipmentCode: deviceQuery.equipmentCode || undefined,
  854. equipmentName: deviceQuery.equipmentName || undefined,
  855. rootTypeId: deviceRootTypeId.value || undefined
  856. });
  857. deviceList.value = res.records || [];
  858. deviceTotal.value = Number(res.total || 0);
  859. } catch (e) {
  860. ElMessage.error(e?.msg || e?.message || "加载设备失败");
  861. } finally {
  862. deviceLoading.value = false;
  863. }
  864. }
  865. function confirmDevice(row) {
  866. formData.deviceCode = row.equipmentCode || "";
  867. formData.deviceName = row.equipmentName || "";
  868. devicePickerVisible.value = false;
  869. }
  870. function handleAdd() {
  871. isEdit.value = false;
  872. dialogTitle.value = "新增预警";
  873. resetForm();
  874. editVisible.value = true;
  875. }
  876. async function handleEdit(row) {
  877. isEdit.value = true;
  878. dialogTitle.value = "编辑预警";
  879. resetForm();
  880. Object.assign(formData, { ...row });
  881. const detailRes = await getWarningDetailData(row.warningId);
  882. const detail = detailRes.data || {};
  883. formData.reviewer = detail.basicInfo?.reviewer || "";
  884. formAttachments.value = (detail.attachmentList || []).map(normalizeExistingAttachment);
  885. await loadReasonOptions();
  886. editVisible.value = true;
  887. }
  888. async function submitForm() {
  889. const valid = await formRef.value?.validate().catch(() => false);
  890. if (!valid) return;
  891. submitLoading.value = true;
  892. try {
  893. const payload = {
  894. warning: {
  895. warningId: formData.warningId,
  896. warningNo: formData.warningNo,
  897. warningName: formData.warningName,
  898. warningType: formData.warningType,
  899. warningLevel: formData.warningLevel,
  900. warningSpecial: formData.warningSpecial,
  901. location: formData.location,
  902. longitude: formData.longitude,
  903. latitude: formData.latitude,
  904. ownershipUnit: formData.ownershipUnit,
  905. handler: formData.handler,
  906. reviewer: formData.reviewer,
  907. warningContent: formData.warningContent,
  908. deviceCode: formData.deviceCode || "",
  909. deviceName: formData.deviceName || "",
  910. remark: formData.remark
  911. },
  912. files: formAttachments.value.map((item) => item.raw).filter(Boolean)
  913. };
  914. const res = isEdit.value ? await updateYjxxData(payload) : await submitYjxxData(payload);
  915. if (res.code !== 200) {
  916. ElMessage.error(res.msg || "保存失败");
  917. return;
  918. }
  919. ElMessage.success(isEdit.value ? "修改成功" : "新增成功");
  920. editVisible.value = false;
  921. loadList();
  922. } finally {
  923. submitLoading.value = false;
  924. }
  925. }
  926. async function handleView(row) {
  927. const res = await getWarningDetailData(row.warningId);
  928. const detail = res.data || {};
  929. const basicSource = detail.basicInfo || detail;
  930. detailData.value = {
  931. ...detail,
  932. basicInfo: normalizeWarningRow({ ...row, ...basicSource }),
  933. alarmList: (detail.alarmList || []).map(normalizeAlarmRow),
  934. disposalHistory: (detail.disposalHistory || detail.disposalList || []).map(normalizeDisposalRow)
  935. };
  936. detailVisible.value = true;
  937. }
  938. async function handleDelete(row) {
  939. await ElMessageBox.confirm(`确认删除预警“${row.warningName}”吗?`, "提示", { type: "warning" });
  940. const res = await deleteYjxxData(row.warningId);
  941. if (res.code !== 200) {
  942. ElMessage.error(res.msg || "删除失败");
  943. return;
  944. }
  945. ElMessage.success("删除成功");
  946. loadList();
  947. }
  948. async function handlePublish(row) {
  949. if (!row.handler) {
  950. ElMessage.warning("发布前请先编辑预警并选择处置人");
  951. return;
  952. }
  953. await ElMessageBox.confirm(`确认发布预警“${row.warningName}”吗?`, "提示", { type: "warning" });
  954. const res = await releaseYjxxData(row.warningId, { reviewer: row.reviewer || undefined });
  955. if (res.code !== 200) {
  956. ElMessage.error(res.msg || "发布失败");
  957. return;
  958. }
  959. ElMessage.success("发布成功");
  960. loadList();
  961. }
  962. onMounted(async () => {
  963. await loadDicts();
  964. await loadList();
  965. });
  966. </script>
  967. <style scoped>
  968. .warning-release-page {
  969. background: #f5f7fb;
  970. }
  971. .page-card {
  972. border-radius: 16px;
  973. margin-bottom: 16px;
  974. }
  975. .header-card {
  976. background: linear-gradient(135deg, #eff6ff, #f8fbff);
  977. border: 1px solid #dbeafe;
  978. }
  979. .page-header {
  980. display: flex;
  981. justify-content: space-between;
  982. align-items: center;
  983. gap: 16px;
  984. }
  985. .page-title {
  986. font-size: 22px;
  987. font-weight: 700;
  988. color: #1e3a8a;
  989. }
  990. .page-subtitle {
  991. margin-top: 8px;
  992. font-size: 13px;
  993. color: #64748b;
  994. }
  995. .header-actions {
  996. display: flex;
  997. gap: 10px;
  998. flex-wrap: wrap;
  999. }
  1000. .reason-wrap {
  1001. display: flex;
  1002. flex-wrap: wrap;
  1003. gap: 8px;
  1004. }
  1005. .reason-tag {
  1006. cursor: pointer;
  1007. }
  1008. .attachment-panel {
  1009. width: 100%;
  1010. }
  1011. .attachment-tip {
  1012. margin-top: 8px;
  1013. font-size: 12px;
  1014. color: #64748b;
  1015. }
  1016. .attachment-list,
  1017. .detail-attachment-list {
  1018. margin-top: 12px;
  1019. display: flex;
  1020. flex-direction: column;
  1021. gap: 8px;
  1022. }
  1023. .attachment-item,
  1024. .detail-attachment-item {
  1025. display: flex;
  1026. align-items: center;
  1027. justify-content: space-between;
  1028. gap: 12px;
  1029. padding: 10px 12px;
  1030. border: 1px solid #e5e7eb;
  1031. border-radius: 10px;
  1032. background: #f8fafc;
  1033. }
  1034. .attachment-link {
  1035. flex: 1;
  1036. min-width: 0;
  1037. }
  1038. .attachment-meta {
  1039. font-size: 12px;
  1040. color: #94a3b8;
  1041. white-space: nowrap;
  1042. }
  1043. @media (max-width: 768px) {
  1044. .page-header {
  1045. flex-direction: column;
  1046. align-items: flex-start;
  1047. }
  1048. }
  1049. /* 用户选择器样式 */
  1050. .user-picker-container {
  1051. display: flex;
  1052. gap: 16px;
  1053. height: 460px;
  1054. }
  1055. .user-picker-left {
  1056. width: 280px;
  1057. border: 1px solid #e5e7eb;
  1058. border-radius: 8px;
  1059. padding: 12px;
  1060. overflow-y: auto;
  1061. }
  1062. .user-picker-right {
  1063. flex: 1;
  1064. border: 1px solid #e5e7eb;
  1065. border-radius: 8px;
  1066. padding: 12px;
  1067. overflow: hidden;
  1068. }
  1069. .picker-title {
  1070. font-size: 14px;
  1071. font-weight: 600;
  1072. color: #374151;
  1073. margin-bottom: 12px;
  1074. }
  1075. .picker-title .current-dept {
  1076. font-weight: 400;
  1077. color: #6b7280;
  1078. font-size: 13px;
  1079. }
  1080. .device-picker-container {
  1081. display: flex;
  1082. gap: 16px;
  1083. height: 460px;
  1084. }
  1085. .device-picker-left {
  1086. width: 180px;
  1087. border: 1px solid #e5e7eb;
  1088. border-radius: 8px;
  1089. padding: 12px;
  1090. overflow-y: auto;
  1091. display: flex;
  1092. flex-direction: column;
  1093. gap: 6px;
  1094. }
  1095. .device-picker-right {
  1096. flex: 1;
  1097. border: 1px solid #e5e7eb;
  1098. border-radius: 8px;
  1099. padding: 12px;
  1100. overflow: hidden;
  1101. }
  1102. .device-type-item {
  1103. padding: 9px 12px;
  1104. text-align: left;
  1105. color: #4b5563;
  1106. border: 1px solid #e5e7eb;
  1107. border-radius: 8px;
  1108. background: #fff;
  1109. cursor: pointer;
  1110. font-size: 13px;
  1111. }
  1112. .device-type-item:hover {
  1113. background: #f3f4f6;
  1114. }
  1115. .device-type-item.active {
  1116. color: #2563eb;
  1117. border-color: #2563eb;
  1118. background: #eff6ff;
  1119. font-weight: 600;
  1120. }
  1121. </style>