DisPlanApproval.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <!-- 应急预案审批 -->
  2. <template>
  3. <div class="app-container">
  4. <!-- 搜索区 -->
  5. <el-form :model="queryParams" :inline="true" class="search-form">
  6. <el-form-item label="预案名称">
  7. <el-input v-model="queryParams.planName" placeholder="请输入预案名称" clearable style="width: 220px" />
  8. </el-form-item>
  9. <el-form-item>
  10. <el-button type="primary" @click="handleSearch">搜索</el-button>
  11. <el-button @click="handleReset">重置</el-button>
  12. </el-form-item>
  13. </el-form>
  14. <!-- 数据表格 -->
  15. <el-table :data="pageList" border stripe style="width: 100%">
  16. <el-table-column label="序号" type="index" width="60" align="center" />
  17. <el-table-column prop="planName" label="预案名称" min-width="180" show-overflow-tooltip />
  18. <el-table-column prop="planType" label="预案类型" width="130" />
  19. <el-table-column prop="planLevel" label="预案级别" width="100" align="center" />
  20. <el-table-column prop="reviewers" label="审批人员" min-width="180" show-overflow-tooltip />
  21. <el-table-column prop="auditRecord" label="审批记录/意见" min-width="240" show-overflow-tooltip />
  22. <el-table-column prop="status" label="状态" width="100" align="center">
  23. <template #default="{ row }">
  24. <el-tag :type="statusTagType(row.status)">{{ row.status }}</el-tag>
  25. </template>
  26. </el-table-column>
  27. <el-table-column label="操作" width="130" align="center">
  28. <template #default="{ row }">
  29. <el-button link type="primary" @click="handleView(row)">查看</el-button>
  30. <el-button link type="primary" @click="handleApprove(row)">审批</el-button>
  31. </template>
  32. </el-table-column>
  33. </el-table>
  34. <!-- 分页 -->
  35. <el-pagination
  36. style="margin-top: 16px; justify-content: flex-end"
  37. v-model:current-page="pagination.page"
  38. v-model:page-size="pagination.size"
  39. :page-sizes="[10, 20, 50]"
  40. :total="tableData.length"
  41. layout="total, sizes, prev, pager, next, jumper"
  42. @size-change="onSizeChange"
  43. @current-change="onPageChange"
  44. />
  45. <!-- 查看详情弹窗 -->
  46. <el-dialog v-model="detailVisible" title="预案详情" width="720px" destroy-on-close>
  47. <div class="detail-section">
  48. <div class="section-title">预案基本信息</div>
  49. <el-descriptions :column="2" border>
  50. <el-descriptions-item label="预案名称">{{ currentRow.planName }}</el-descriptions-item>
  51. <el-descriptions-item label="预案类型">{{ currentRow.planType }}</el-descriptions-item>
  52. <el-descriptions-item label="预案等级">{{ currentRow.planLevel }}</el-descriptions-item>
  53. <el-descriptions-item label="审批人员">{{ currentRow.reviewers }}</el-descriptions-item>
  54. </el-descriptions>
  55. </div>
  56. <!-- 审批流程 2x2 卡片 -->
  57. <div class="detail-section">
  58. <div class="section-title">审批流程</div>
  59. <div class="flow-grid">
  60. <div class="flow-card blue">
  61. <div class="flow-step">环节一:预案起草人提交</div>
  62. <div class="flow-info">审批人:安全总监 · 权限:初审/退回</div>
  63. </div>
  64. <div class="flow-card green">
  65. <div class="flow-step">环节二:专家技术评审</div>
  66. <div class="flow-info">审批人:专家组(3人) · 权限:修正建议/通过</div>
  67. </div>
  68. <div class="flow-card blue">
  69. <div class="flow-step">环节三:领导层终审</div>
  70. <div class="flow-info">审批人:应急总指挥 · 权限:批准/驳回</div>
  71. </div>
  72. <div class="flow-card yellow">
  73. <div class="flow-step">环节四:签发与归档</div>
  74. <div class="flow-info">状态:完成</div>
  75. </div>
  76. </div>
  77. </div>
  78. <!-- 预案调整 -->
  79. <div class="detail-section">
  80. <div class="section-title">预案调整</div>
  81. <div class="adjust-box">
  82. <div class="adjust-title">{{ currentRow.planName }} V{{ currentRow.version || '2.1' }}</div>
  83. <div class="adjust-content">
  84. 根据刘宏专家评审意见,优化预警阈值及撤离路线;增加物资调度流程。
  85. 评审结果:"科学性提升,建议补充应急通讯保障" — 已修正。
  86. </div>
  87. </div>
  88. </div>
  89. <!-- 预案发布 -->
  90. <div class="detail-section">
  91. <div class="section-title">预案发布</div>
  92. <div class="publish-box">
  93. <template v-if="currentRow.status === '已发布'">
  94. <el-tag type="success" size="large">已发布</el-tag>
  95. </template>
  96. <template v-else>
  97. <div class="unpublished-tip">未发布预案(应急处置不可用)</div>
  98. <el-button type="success" @click="handlePublish">发布</el-button>
  99. </template>
  100. </div>
  101. </div>
  102. </el-dialog>
  103. <!-- 审批弹窗 -->
  104. <el-dialog v-model="approveVisible" title="审批流程" width="600px" destroy-on-close>
  105. <!-- 案件基本信息 -->
  106. <div class="approve-header">
  107. <el-descriptions :column="1" border>
  108. <el-descriptions-item label="案件名称">{{ currentRow.planName }}</el-descriptions-item>
  109. <el-descriptions-item label="案件类型">{{ currentRow.planType }}</el-descriptions-item>
  110. <el-descriptions-item label="案件等级">{{ currentRow.planLevel }}</el-descriptions-item>
  111. </el-descriptions>
  112. </div>
  113. <!-- 审批环节列表 -->
  114. <div class="approve-steps">
  115. <div v-for="(step, idx) in approveSteps" :key="idx" :class="['approve-step-card', step.colorClass]">
  116. <div class="step-header">
  117. <span class="step-dot"></span>
  118. <span class="step-title">{{ step.title }}</span>
  119. </div>
  120. <div class="step-info">审批人:{{ step.approver }} · 权限:{{ step.permission }}</div>
  121. <div class="step-opinion">
  122. <span class="opinion-label">审批意见:</span>
  123. <el-input v-model="step.opinion" type="textarea" :rows="2" placeholder="请输入审批意见" />
  124. </div>
  125. </div>
  126. </div>
  127. <template #footer>
  128. <el-button type="danger" @click="handleReject">驳回</el-button>
  129. <el-button type="primary" @click="handleComplete">完成</el-button>
  130. </template>
  131. </el-dialog>
  132. </div>
  133. </template>
  134. <script setup>
  135. import { ref, computed, onMounted } from 'vue'
  136. import { ElMessage, ElMessageBox } from 'element-plus'
  137. import { getPlanApprovalPage, getPlanApprovalById, approvePlan, rejectPlan, publishPlan, getApprovalSteps } from '@/api/dispatch'
  138. // ---------- 查询 ----------
  139. const queryParams = ref({ planName: '' })
  140. // ---------- 数据 ----------
  141. const tableData = ref([])
  142. async function loadData() {
  143. try {
  144. const res = await getPlanApprovalPage(1, 999, queryParams.value)
  145. tableData.value = res?.data?.records ?? res?.data ?? res ?? []
  146. } catch {
  147. ElMessage.error('加载数据失败')
  148. }
  149. }
  150. onMounted(() => { loadData() })
  151. // ---------- 分页 ----------
  152. const pagination = ref({ page: 1, size: 10 })
  153. const pageList = computed(() => {
  154. const start = (pagination.value.page - 1) * pagination.value.size
  155. return tableData.value.slice(start, start + pagination.value.size)
  156. })
  157. const onSizeChange = () => { pagination.value.page = 1 }
  158. const onPageChange = () => {}
  159. // ---------- 搜索 / 重置 ----------
  160. const handleSearch = () => { pagination.value.page = 1; loadData() }
  161. const handleReset = () => {
  162. queryParams.value.planName = ''
  163. pagination.value.page = 1
  164. loadData()
  165. }
  166. // ---------- 状态 Tag 类型 ----------
  167. const statusTagType = (status) => {
  168. if (status === '终审中') return 'warning'
  169. if (status === '待修正') return 'danger'
  170. if (status === '已发布') return 'success'
  171. return 'info'
  172. }
  173. // ---------- 当前操作行 ----------
  174. const currentRow = ref({})
  175. // ---------- 查看详情弹窗 ----------
  176. const detailVisible = ref(false)
  177. const handleView = async (row) => {
  178. currentRow.value = { ...row }
  179. try {
  180. const res = await getApprovalSteps(row.id)
  181. currentRow.value.approvalSteps = res?.data?.records ?? res?.data ?? res ?? []
  182. } catch { /* ignore */ }
  183. detailVisible.value = true
  184. }
  185. // ---------- 发布 ----------
  186. const handlePublish = () => {
  187. ElMessageBox.confirm('确认发布该预案?发布后将在应急处置中启用。', '提示', { type: 'warning' }).then(async () => {
  188. try {
  189. await publishPlan(currentRow.value.id)
  190. currentRow.value.status = '已发布'
  191. ElMessage.success('发布成功')
  192. loadData()
  193. } catch {
  194. ElMessage.error('发布失败')
  195. }
  196. }).catch(() => {})
  197. }
  198. // ---------- 审批弹窗 ----------
  199. const approveVisible = ref(false)
  200. const approveSteps = ref([])
  201. const handleApprove = async (row) => {
  202. currentRow.value = { ...row }
  203. try {
  204. const res = await getApprovalSteps(row.id)
  205. const steps = res?.data?.records ?? res?.data ?? res ?? []
  206. if (steps.length > 0) {
  207. approveSteps.value = steps
  208. } else {
  209. approveSteps.value = [
  210. { title: '环节一:预案起草人提交', approver: '安全总监', permission: '初审/退回', colorClass: 'blue', opinion: '' },
  211. { title: '环节二:专家技术评审', approver: '专家组(3人)', permission: '修正建议/通过', colorClass: 'green', opinion: '' },
  212. { title: '环节三:领导层终审', approver: '应急总指挥', permission: '批准/驳回', colorClass: 'blue', opinion: '' },
  213. ]
  214. }
  215. } catch {
  216. approveSteps.value = [
  217. { title: '环节一:预案起草人提交', approver: '安全总监', permission: '初审/退回', colorClass: 'blue', opinion: '' },
  218. { title: '环节二:专家技术评审', approver: '专家组(3人)', permission: '修正建议/通过', colorClass: 'green', opinion: '' },
  219. { title: '环节三:领导层终审', approver: '应急总指挥', permission: '批准/驳回', colorClass: 'blue', opinion: '' },
  220. ]
  221. }
  222. approveVisible.value = true
  223. }
  224. const handleReject = async () => {
  225. try {
  226. await rejectPlan(currentRow.value.id)
  227. approveVisible.value = false
  228. ElMessage.success('已驳回,状态变更为待修正')
  229. loadData()
  230. } catch {
  231. ElMessage.error('驳回失败')
  232. }
  233. }
  234. const handleComplete = async () => {
  235. try {
  236. await approvePlan(currentRow.value.id)
  237. approveVisible.value = false
  238. ElMessage.success('审批完成')
  239. loadData()
  240. } catch {
  241. ElMessage.error('审批失败')
  242. }
  243. }
  244. </script>
  245. <style scoped lang="scss">
  246. .search-form {
  247. margin-bottom: 12px;
  248. :deep(.el-form-item) {
  249. margin-bottom: 8px;
  250. }
  251. }
  252. /* ---- 查看详情样式 ---- */
  253. .detail-section {
  254. margin-bottom: 20px;
  255. .section-title {
  256. font-weight: bold;
  257. font-size: 15px;
  258. margin-bottom: 10px;
  259. padding-left: 8px;
  260. border-left: 3px solid #409eff;
  261. }
  262. }
  263. .flow-grid {
  264. display: grid;
  265. grid-template-columns: 1fr 1fr;
  266. gap: 12px;
  267. }
  268. .flow-card {
  269. border-radius: 6px;
  270. padding: 14px 16px;
  271. color: #fff;
  272. .flow-step {
  273. font-weight: bold;
  274. font-size: 14px;
  275. margin-bottom: 6px;
  276. }
  277. .flow-info {
  278. font-size: 13px;
  279. opacity: 0.9;
  280. }
  281. &.blue { background: #409eff; }
  282. &.green { background: #67c23a; }
  283. &.yellow { background: #e6a23c; }
  284. }
  285. .adjust-box {
  286. background: #fafafa;
  287. border: 1px solid #ebeef5;
  288. border-radius: 4px;
  289. padding: 12px 16px;
  290. .adjust-title {
  291. font-weight: bold;
  292. margin-bottom: 6px;
  293. }
  294. .adjust-content {
  295. font-size: 13px;
  296. color: #606266;
  297. line-height: 1.6;
  298. }
  299. }
  300. .publish-box {
  301. background: #f0f9eb;
  302. border: 1px solid #e1f3d8;
  303. border-radius: 4px;
  304. padding: 16px;
  305. display: flex;
  306. align-items: center;
  307. justify-content: space-between;
  308. .unpublished-tip {
  309. font-size: 14px;
  310. color: #e6a23c;
  311. font-weight: bold;
  312. }
  313. }
  314. /* ---- 审批弹窗样式 ---- */
  315. .approve-header {
  316. margin-bottom: 16px;
  317. }
  318. .approve-steps {
  319. display: flex;
  320. flex-direction: column;
  321. gap: 12px;
  322. }
  323. .approve-step-card {
  324. border-radius: 6px;
  325. padding: 14px 16px;
  326. .step-header {
  327. display: flex;
  328. align-items: center;
  329. margin-bottom: 6px;
  330. .step-dot {
  331. display: inline-block;
  332. width: 10px;
  333. height: 10px;
  334. border-radius: 50%;
  335. margin-right: 8px;
  336. background: #fff;
  337. border: 2px solid #fff;
  338. }
  339. .step-title {
  340. font-weight: bold;
  341. font-size: 14px;
  342. color: #fff;
  343. }
  344. }
  345. .step-info {
  346. font-size: 13px;
  347. color: rgba(255, 255, 255, 0.9);
  348. margin-bottom: 8px;
  349. }
  350. .step-opinion {
  351. .opinion-label {
  352. font-size: 13px;
  353. color: #fff;
  354. display: block;
  355. margin-bottom: 4px;
  356. }
  357. }
  358. &.blue {
  359. background: #409eff;
  360. .step-dot { border-color: #fff; }
  361. }
  362. &.green {
  363. background: #67c23a;
  364. .step-dot { border-color: #fff; }
  365. }
  366. }
  367. </style>