WaterDevicePage.vue 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366
  1. <template>
  2. <div class="water-page device-page">
  3. <div class="page-heading">
  4. <div class="page-title">
  5. <span class="page-title-icon"><el-icon><component :is="pageIcon"/></el-icon></span>
  6. <div><h2>{{ title }}</h2><span class="muted">{{ subtitle }}</span></div>
  7. </div>
  8. <el-button class="heading-refresh" :icon="Refresh" :loading="loading" @click="loadData">刷新</el-button>
  9. </div>
  10. <div v-if="view === 'abnormal'" class="metric-grid">
  11. <div v-for="item in abnormalStats" :key="item.label" class="metric">
  12. <div class="metric-icon">
  13. <el-icon>
  14. <component :is="item.icon"/>
  15. </el-icon>
  16. </div>
  17. <div class="metric-body">
  18. <span>{{ item.label }}</span>
  19. <div class="metric-value"><strong>{{ item.value }}</strong><small>{{ item.unit }}</small></div>
  20. </div>
  21. </div>
  22. </div>
  23. <div v-if="view === 'ledger'" class="metric-grid">
  24. <div v-for="item in ledgerStatsCards" :key="item.label" class="metric">
  25. <div class="metric-icon">
  26. <el-icon>
  27. <component :is="item.icon"/>
  28. </el-icon>
  29. </div>
  30. <div class="metric-body">
  31. <span>{{ item.label }}</span>
  32. <div class="metric-value"><strong>{{ item.value }}</strong><small>{{ item.unit }}</small></div>
  33. </div>
  34. </div>
  35. </div>
  36. <el-card shadow="never" class="filter-card">
  37. <el-form :inline="true" :model="query">
  38. <el-form-item v-if="view !== 'gis'" label="设备名称">
  39. <el-input v-model="query.equipmentName" clearable/>
  40. </el-form-item>
  41. <el-form-item v-if="view !== 'gis'" label="设备编码">
  42. <el-input v-model="query.equipmentCode" clearable/>
  43. </el-form-item>
  44. <el-form-item v-if="view === 'ledger'" label="报警状态">
  45. <el-select v-model="query.alarmStatus" clearable class="filter-select" style="width:180px">
  46. <el-option label="正常" :value="0"/>
  47. <el-option label="报警" :value="1"/>
  48. </el-select>
  49. </el-form-item>
  50. <el-form-item v-if="view === 'ledger' || view === 'dispatch' || view === 'abnormal'" label="报警等级">
  51. <el-select v-model="query.alertLevel" clearable class="filter-select" style="width:180px">
  52. <el-option v-for="item in levels" :key="item.value" v-bind="item"/>
  53. </el-select>
  54. </el-form-item>
  55. <el-form-item>
  56. <el-button type="primary" @click="loadData">查询</el-button>
  57. <el-button @click="reset">重置</el-button>
  58. </el-form-item>
  59. </el-form>
  60. </el-card>
  61. <el-card shadow="never" class="water-panel">
  62. <div class="card-title"><span><el-icon><Monitor/></el-icon> {{ title }}清单</span>
  63. <div class="panel-actions">
  64. <el-button v-if="view === 'ledger'" type="primary" plain size="small" :icon="Plus"
  65. @click="openCreate">新增
  66. </el-button>
  67. <el-button v-if="view === 'ledger'" type="danger" plain size="small" :icon="Delete"
  68. :disabled="!selectedDevices.length" @click="removeSelectedDevices">删除
  69. </el-button>
  70. <el-button v-if="view === 'ledger'" type="info" plain size="small" :icon="Upload"
  71. @click="openImport">导入
  72. </el-button>
  73. <el-button v-if="view !== 'gis'" type="success" plain size="small" :icon="Download"
  74. @click="exportData">导出
  75. </el-button>
  76. </div>
  77. </div>
  78. <el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" class="load-error">
  79. <el-button size="small" type="danger" plain @click="loadData">重试</el-button>
  80. </el-alert>
  81. <el-empty v-if="view === 'gis' && !loading && !loadError && !page.records.length"
  82. description="暂无设备 GIS 点位数据"/>
  83. <el-table :data="page.records" border stripe v-loading="loading" row-key="equipmentId"
  84. @selection-change="handleDeviceSelectionChange">
  85. <el-table-column v-if="view === 'ledger'" type="selection" width="48" align="center"/>
  86. <el-table-column type="index" label="#" width="55"/>
  87. <el-table-column prop="equipmentCode" label="设备编码" min-width="130"/>
  88. <el-table-column prop="equipmentName" label="设备名称" min-width="150"/>
  89. <el-table-column prop="equipmentTypeName" label="设备类型" min-width="120"/>
  90. <el-table-column prop="equipmentLocation" label="安装位置" min-width="180" show-overflow-tooltip/>
  91. <el-table-column v-if="view === 'ledger'" label="设备图片" width="110" align="center">
  92. <template #default="{ row }">
  93. <el-image v-if="row.equipmentImageUrl" :src="row.equipmentImageUrl"
  94. :preview-src-list="[row.equipmentImageUrl]" preview-teleported hide-on-click-modal
  95. fit="cover" class="device-image"/>
  96. <span v-else class="image-placeholder">暂无图片</span></template>
  97. </el-table-column>
  98. <el-table-column v-if="view !== 'maintain'" label="设备状态" width="100">
  99. <template #default="{ row }">
  100. <el-tag :type="Number(row.currentStatus) === 3 ? 'danger' : 'success'">{{ row.currentStatusText
  101. }}
  102. </el-tag>
  103. </template>
  104. </el-table-column>
  105. <el-table-column v-if="view !== 'maintain'" label="在线状态" width="100">
  106. <template #default="{ row }">
  107. <el-tag :type="Number(row.onlineStatus) === 1 ? 'success' : 'info'">{{ row.onlineStatusText }}
  108. </el-tag>
  109. </template>
  110. </el-table-column>
  111. <el-table-column v-if="view !== 'maintain'" label="报警状态" width="100">
  112. <template #default="{ row }">
  113. <el-tag :type="Number(row.alarmStatus) === 1 ? 'danger' : 'success'">{{ row.alarmStatusText }}
  114. </el-tag>
  115. </template>
  116. </el-table-column>
  117. <el-table-column v-if="view !== 'maintain'" label="报警等级" width="110">
  118. <template #default="{ row }">
  119. <el-tag :type="alarmLevelType(row.alertLevel)">{{ row.alarmLevelText }}</el-tag>
  120. </template>
  121. </el-table-column>
  122. <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="异常原因" min-width="150"
  123. show-overflow-tooltip>
  124. <template #default="{ row }">{{ row.abnormalReasonText || '-' }}</template>
  125. </el-table-column>
  126. <el-table-column label="关联工单" min-width="190" show-overflow-tooltip>
  127. <template #default="{ row }">{{ view === 'maintain' ? (row.orderNo || '-') : row.relatedOrderNo }}
  128. </template>
  129. </el-table-column>
  130. <el-table-column label="工单状态" min-width="110">
  131. <template #default="{ row }">
  132. <el-tag v-if="view !== 'maintain' && row.workOrderStatusText !== '暂无工单'"
  133. :type="workOrderStatusType(row.workOrderStatusCode)">{{ row.workOrderStatusText }}
  134. </el-tag>
  135. <span v-else>{{ view === 'maintain' ? (row.status || '暂无') : row.workOrderStatusText }}</span>
  136. </template>
  137. </el-table-column>
  138. <el-table-column v-if="view === 'abnormal' || view === 'dispatch'" label="接单责任人" min-width="110">
  139. <template #default="{ row }">{{ view === 'maintain' ? (row.maintainPerson || '-') :
  140. (row.relatedWorkOrderPerson || '-') }}
  141. </template>
  142. </el-table-column>
  143. <el-table-column label="操作" width="210" fixed="right">
  144. <template #default="{ row }">
  145. <div class="facility-row-actions">
  146. <el-tooltip content="查看详情" placement="top">
  147. <el-button class="facility-row-action is-detail" aria-label="查看详情" size="small"
  148. type="primary" plain circle :icon="View" @click="selectPoint(row)"/>
  149. </el-tooltip>
  150. <el-tooltip v-if="view === 'ledger'" content="修改信息" placement="top">
  151. <el-button class="facility-row-action is-edit" aria-label="修改信息" size="small"
  152. type="warning" plain circle :icon="Edit" @click="openEdit(row)"/>
  153. </el-tooltip>
  154. <el-tooltip v-if="canDispatch(row)" content="维修派单" placement="top">
  155. <el-button class="facility-row-action is-dispatch" aria-label="维修派单" size="small"
  156. type="warning" plain circle :icon="Position" @click="openOrder(row)"/>
  157. </el-tooltip>
  158. <el-tooltip v-if="view === 'ledger'" content="删除设备" placement="top">
  159. <el-button class="facility-row-action is-delete" aria-label="删除设备" size="small"
  160. type="danger" plain circle :icon="Delete" @click="removeDevice(row)"/>
  161. </el-tooltip>
  162. <el-tooltip v-if="view === 'maintain'" content="删除记录" placement="top">
  163. <el-button class="facility-row-action is-delete" aria-label="删除记录" size="small"
  164. type="danger" plain circle :icon="Delete" @click="removeRecord(row)"/>
  165. </el-tooltip>
  166. </div>
  167. </template>
  168. </el-table-column>
  169. </el-table>
  170. <el-pagination v-model:current-page="query.pageNum" v-model:page-size="query.pageSize" :total="page.total"
  171. layout="total, sizes, prev, pager, next" :page-sizes="[10, 20, 50]" @change="loadData"/>
  172. </el-card>
  173. <el-drawer v-model="detailVisible" class="detail-drawer device-detail-drawer" title="设备详情" direction="rtl"
  174. size="680px" append-to-body>
  175. <div class="detail-summary"><span class="detail-name">{{ detail.equipmentName || '-' }}</span>
  176. <el-tag :type="Number(detail.currentStatus) === 3 ? 'danger' : 'success'">{{ detail.currentStatusText ||
  177. '-' }}
  178. </el-tag>
  179. <el-tag :type="Number(detail.alarmStatus) === 1 ? 'danger' : 'success'">{{ detail.alarmStatusText || '-'
  180. }}
  181. </el-tag>
  182. <el-tag v-if="detail.workOrderStatusText && detail.workOrderStatusText !== '暂无工单'"
  183. :type="workOrderStatusType(detail.workOrderStatusCode)">{{ detail.workOrderStatusText }}
  184. </el-tag>
  185. </div>
  186. <div v-if="view === 'ledger'" class="detail-media">
  187. <div class="detail-media-label">设备图片</div>
  188. <el-image v-if="detail.equipmentImageUrl" :src="detail.equipmentImageUrl"
  189. :preview-src-list="[detail.equipmentImageUrl]" preview-teleported hide-on-click-modal
  190. fit="cover"
  191. class="detail-image"/>
  192. <div v-else class="detail-image-placeholder">暂无图片</div>
  193. </div>
  194. <el-descriptions :column="2" border>
  195. <el-descriptions-item v-for="item in detailFields" :key="item.label" :label="item.label">{{ item.value
  196. }}
  197. </el-descriptions-item>
  198. </el-descriptions>
  199. <template #footer>
  200. <el-button @click="detailVisible = false">关闭</el-button>
  201. </template>
  202. </el-drawer>
  203. <el-drawer v-model="editVisible" class="detail-drawer device-edit-drawer"
  204. :title="editMode === 'create' ? '新增设备信息' : '修改设备信息'" direction="rtl" size="680px"
  205. append-to-body>
  206. <el-form ref="editFormRef" :model="editForm" label-width="110px" :rules="editRules">
  207. <el-row :gutter="16">
  208. <el-col :span="12">
  209. <el-form-item label="设备编码" prop="equipmentCode">
  210. <el-input v-model="editForm.equipmentCode" :disabled="editMode === 'edit'" maxlength="50"/>
  211. </el-form-item>
  212. </el-col>
  213. <el-col :span="12">
  214. <el-form-item label="设备类型" prop="equipmentTypeId">
  215. <el-select v-if="editMode === 'create'" v-model="editForm.equipmentTypeId" filterable
  216. class="full-input">
  217. <el-option v-for="item in waterDeviceTypes" :key="item.id" :label="item.typeName"
  218. :value="item.id"/>
  219. </el-select>
  220. <el-input v-else v-model="editForm.equipmentTypeName" disabled/>
  221. </el-form-item>
  222. </el-col>
  223. <el-col :span="12">
  224. <el-form-item label="设备名称" prop="equipmentName">
  225. <el-input v-model="editForm.equipmentName" clearable/>
  226. </el-form-item>
  227. </el-col>
  228. <el-col :span="12">
  229. <el-form-item label="设备型号">
  230. <el-input v-model="editForm.equipmentModel" clearable/>
  231. </el-form-item>
  232. </el-col>
  233. <el-col :span="12">
  234. <el-form-item label="所属片区">
  235. <el-select v-model="editForm.district" clearable class="full-input">
  236. <el-option label="城南" value="城南"/>
  237. <el-option label="城北" value="城北"/>
  238. <el-option label="太常" value="太常"/>
  239. </el-select>
  240. </el-form-item>
  241. </el-col>
  242. <el-col :span="12">
  243. <el-form-item label="制造商">
  244. <el-input v-model="editForm.manufacturer" clearable/>
  245. </el-form-item>
  246. </el-col>
  247. <el-col :span="12">
  248. <el-form-item label="经度">
  249. <el-input-number v-model="editForm.longitude" :precision="8" :controls="false"
  250. class="full-input"/>
  251. </el-form-item>
  252. </el-col>
  253. <el-col :span="12">
  254. <el-form-item label="纬度">
  255. <el-input-number v-model="editForm.latitude" :precision="8" :controls="false"
  256. class="full-input"/>
  257. </el-form-item>
  258. </el-col>
  259. <el-col :span="24">
  260. <el-form-item label="安装位置">
  261. <el-input v-model="editForm.equipmentLocation" clearable/>
  262. </el-form-item>
  263. </el-col>
  264. <el-col :span="12">
  265. <el-form-item label="设备负责人">
  266. <el-input v-model="editForm.maintainer" clearable/>
  267. </el-form-item>
  268. </el-col>
  269. <el-col :span="12">
  270. <el-form-item label="联系方式">
  271. <el-input v-model="editForm.maintainerPhone" clearable/>
  272. </el-form-item>
  273. </el-col>
  274. <el-col :span="24">
  275. <el-form-item label="权属单位">
  276. <el-input v-model="editForm.ownershipUnit" clearable/>
  277. </el-form-item>
  278. </el-col>
  279. <el-col :span="24">
  280. <el-form-item label="设备图片">
  281. <ImageUpload v-model="editForm.equipmentImage" :limit="1" :file-size="5"/>
  282. </el-form-item>
  283. </el-col>
  284. <el-col :span="24">
  285. <el-form-item label="备注">
  286. <el-input v-model="editForm.remark" type="textarea" :rows="3" maxlength="500"
  287. show-word-limit/>
  288. </el-form-item>
  289. </el-col>
  290. </el-row>
  291. </el-form>
  292. <template #footer>
  293. <el-button @click="editVisible = false">取消</el-button>
  294. <el-button type="primary" :loading="editSaving" @click="submitEdit">保存</el-button>
  295. </template>
  296. </el-drawer>
  297. <el-dialog v-model="dispatchVisible" class="dispatch-dialog" title="设备维修派单" width="860px">
  298. <div class="dispatch-layout">
  299. <div class="assignee-picker">
  300. <aside class="assignee-depts">
  301. <el-input v-model="deptFilterText" clearable placeholder="筛选部门" size="small"/>
  302. <el-tree ref="deptTreeRef" :data="deptTreeData" node-key="deptId" highlight-current
  303. :props="{ label: 'deptName', children: 'children' }"
  304. :filter-node-method="filterDeptNode" class="assignee-tree"
  305. @node-click="handleDeptClick"/>
  306. </aside>
  307. <section class="assignee-users">
  308. <div class="assignee-search">
  309. <el-input v-model="assigneeKeyword" clearable placeholder="搜索用户名、昵称或手机号"
  310. @keyup.enter="searchAssigneeUsers"/>
  311. <el-button type="primary" :loading="userLoading" @click="searchAssigneeUsers">查询
  312. </el-button>
  313. </div>
  314. <el-alert v-if="userLoadError" :title="userLoadError" type="error" show-icon :closable="false">
  315. <el-button size="small" type="danger" plain @click="retryAssigneeLoading">重试</el-button>
  316. </el-alert>
  317. <el-table :data="users" border row-key="userId" highlight-current-row
  318. :current-row-key="assigneeDraft?.userId" @row-click="chooseAssignee">
  319. <el-table-column label="选择" width="58" align="center">
  320. <template #default="{ row }">
  321. <el-radio :model-value="assigneeDraft?.userId" :label="row.userId"
  322. @click.stop="chooseAssignee(row)">&nbsp;
  323. </el-radio>
  324. </template>
  325. </el-table-column>
  326. <el-table-column prop="nickName" label="昵称" min-width="100"/>
  327. <el-table-column prop="userName" label="用户名" min-width="110"/>
  328. <el-table-column prop="deptName" label="部门" min-width="120" show-overflow-tooltip/>
  329. <el-table-column prop="phone" label="手机号" min-width="130"/>
  330. <el-table-column prop="statusText" label="账号状态" width="90"/>
  331. </el-table>
  332. <el-empty v-if="!userLoading && !userLoadError && !users.length"
  333. description="暂无符合条件的责任人" :image-size="80"/>
  334. <el-pagination v-model:current-page="assigneePageNum" v-model:page-size="assigneePageSize"
  335. :total="assigneeTotal" layout="total, sizes, prev, pager, next"
  336. :page-sizes="[10, 20, 50]" @current-change="loadAssigneeUsers"
  337. @size-change="handleAssigneeSizeChange"/>
  338. <div class="assignee-picked">已选责任人:<strong v-if="assigneeDraft">{{ assigneeDraft.nickName
  339. || assigneeDraft.userName }}</strong><span v-else>未选择</span>
  340. <el-button size="small" type="primary" plain :disabled="!assigneeDraft"
  341. @click="confirmAssignee">确认选择
  342. </el-button>
  343. </div>
  344. </section>
  345. </div>
  346. <el-form class="dispatch-form" :model="dispatchForm" label-width="100px">
  347. <el-form-item label="设备"><span>{{ dispatchDevice.equipmentName }}({{ dispatchDevice.equipmentCode }})</span>
  348. </el-form-item>
  349. <el-form-item label="工单类型"><span>1-故障维修</span></el-form-item>
  350. <el-form-item label="优先级" required>
  351. <el-select v-model="dispatchForm.orderLevel" placeholder="请选择优先级">
  352. <el-option label="紧急" :value="1"/>
  353. <el-option label="一般" :value="2"/>
  354. <el-option label="低" :value="3"/>
  355. </el-select>
  356. </el-form-item>
  357. <el-form-item label="问题描述" required>
  358. <el-input v-model="dispatchForm.description" type="textarea" :rows="5" maxlength="500"
  359. show-word-limit placeholder="请输入故障或异常描述"/>
  360. </el-form-item>
  361. <el-form-item label="接单责任人" required>
  362. <span v-if="selectedAssignee" class="assignee-summary">{{ selectedAssignee.nickName || selectedAssignee.userName }}({{ selectedAssignee.deptName || '-' }})</span>
  363. <span v-else class="assignee-placeholder">请在左侧选择责任人</span>
  364. </el-form-item>
  365. </el-form>
  366. </div>
  367. <template #footer>
  368. <el-button @click="dispatchVisible = false">取消</el-button>
  369. <el-button type="primary" :loading="dispatching" @click="submitDispatch">确认派单</el-button>
  370. </template>
  371. </el-dialog>
  372. <el-dialog v-model="importVisible" title="供水设备批量导入" width="520px" append-to-body @closed="resetImport">
  373. <el-upload ref="importUploadRef" drag :limit="1" accept=".xlsx,.xls" :auto-upload="false"
  374. :file-list="importFileList" :on-change="handleImportChange" :on-remove="handleImportRemove"
  375. :disabled="importUploading">
  376. <el-icon class="el-icon--upload">
  377. <Upload/>
  378. </el-icon>
  379. <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
  380. <template #tip>
  381. <div class="el-upload__tip import-tip">仅支持 xls、xlsx;设备类型必须属于供水类型树。</div>
  382. </template>
  383. </el-upload>
  384. <el-checkbox v-model="importUpdateSupport" class="import-update">更新已存在的设备编码</el-checkbox>
  385. <template #footer>
  386. <el-button type="primary" :loading="importUploading" @click="submitImport">开始导入</el-button>
  387. <el-button @click="importVisible = false">取消</el-button>
  388. </template>
  389. </el-dialog>
  390. </div>
  391. </template>
  392. <script setup>
  393. import {computed, onMounted, ref, watch} from 'vue'
  394. import {ElMessage, ElMessageBox} from 'element-plus'
  395. import {
  396. Monitor,
  397. Refresh,
  398. Warning,
  399. Bell,
  400. Tools,
  401. DataLine,
  402. Upload,
  403. Download,
  404. Search,
  405. Plus,
  406. Delete,
  407. View,
  408. Edit,
  409. Position
  410. } from '@element-plus/icons-vue'
  411. import {listUser} from '@/api/system/user'
  412. import {listDept} from '@/api/system/dept'
  413. import {handleTree} from '@/utils/ruoyi'
  414. import {
  415. createAbnormalOrder,
  416. createWaterDevice,
  417. deleteMaintainRecord,
  418. deleteWaterDevices,
  419. exportAbnormalDevices,
  420. exportDeviceLedger,
  421. getDeviceLedgerStatistics,
  422. getWaterDeviceById,
  423. importDeviceLedger,
  424. listAbnormalDevices,
  425. listDeviceGisPoints,
  426. listDeviceLedger,
  427. listMaintainRecords,
  428. updateWaterDevice
  429. } from '@/api/pipeNetwork/waterSupply'
  430. import {listEquipmentTypeAll} from '@/api/pipeNetwork/pdeviceInform'
  431. import {normalizePage, readMonitoringExportError} from '@/utils/waterSupplyModel'
  432. import {
  433. buildAssigneeQuery,
  434. buildMaintainOrderPayload,
  435. canDispatchDevice,
  436. normalizeAssigneeUser,
  437. normalizeDeviceGisPoints,
  438. normalizeDeviceRow,
  439. normalizeDispatchOrderResponse,
  440. preserveDevicePageOnError,
  441. selectSingleAssignee
  442. } from '@/utils/waterSupplyUiModel'
  443. import {saveAs} from 'file-saver'
  444. const props = defineProps({
  445. title: {type: String, required: true},
  446. subtitle: {type: String, default: '设备信息、运行状态与维护过程管理'},
  447. view: {type: String, default: 'ledger'}
  448. })
  449. const {title, subtitle, view} = props
  450. const query = ref({
  451. pageNum: 1,
  452. pageSize: 10,
  453. equipmentName: '',
  454. equipmentCode: '',
  455. alarmStatus: undefined,
  456. alertLevel: undefined
  457. })
  458. const importVisible = ref(false);
  459. const importUploading = ref(false);
  460. const importUpdateSupport = ref(false);
  461. const importFileList = ref([]);
  462. const importUploadRef = ref(null);
  463. const editVisible = ref(false);
  464. const editSaving = ref(false);
  465. const editFormRef = ref(null);
  466. const editForm = ref({})
  467. const editMode = ref('edit');
  468. const selectedDevices = ref([]);
  469. const waterDeviceTypes = ref([])
  470. const editRules = {
  471. equipmentCode: [{required: true, message: '请输入设备编码', trigger: 'blur'}],
  472. equipmentTypeId: [{required: true, message: '请选择设备类型', trigger: 'change'}],
  473. equipmentName: [{required: true, message: '请输入设备名称', trigger: 'blur'}]
  474. }
  475. const page = ref({records: [], total: 0});
  476. const ledgerStats = ref({total: 0, online: 0, offline: 0, onlineRate: 0});
  477. const loading = ref(false);
  478. const loadError = ref('');
  479. const detailVisible = ref(false);
  480. const detail = ref({});
  481. const dispatchVisible = ref(false);
  482. const dispatching = ref(false);
  483. const dispatchDevice = ref({});
  484. const dispatchForm = ref({description: '', receiveUserId: undefined});
  485. const users = ref([]);
  486. const userLoading = ref(false);
  487. const userLoadError = ref('');
  488. const deptLoadError = ref('');
  489. const selectedAssignee = ref(null);
  490. const assigneeDraft = ref(null);
  491. const deptTreeRef = ref(null);
  492. const deptTreeData = ref([]);
  493. const deptFilterText = ref('');
  494. const assigneeKeyword = ref('');
  495. const assigneeDeptId = ref(undefined);
  496. const assigneePageNum = ref(1);
  497. const assigneePageSize = ref(10);
  498. const assigneeTotal = ref(0)
  499. const levels = [{label: '一级报警', value: 1}, {label: '二级报警', value: 2}, {
  500. label: '三级报警',
  501. value: 3
  502. }, {label: '四级报警', value: 4}];
  503. const pageIcon = Monitor;
  504. const alarmLevelType = level => ({1: 'danger', 2: 'warning', 3: 'primary', 4: 'success'}[Number(level)] || 'info')
  505. function canDispatch(row) {
  506. if (view !== 'dispatch' && view !== 'abnormal') return false
  507. return canDispatchDevice(row)
  508. }
  509. function workOrderStatusType(code) {
  510. return ({
  511. 1: 'info',
  512. 2: 'warning',
  513. 3: 'primary',
  514. 4: 'warning',
  515. 5: 'danger',
  516. 6: 'success',
  517. 7: 'info',
  518. 8: 'warning',
  519. 9: 'warning'
  520. })[Number(code)] || 'info'
  521. }
  522. const abnormalStats = computed(() => {
  523. const rows = page.value.records
  524. const alarmCount = rows.filter(row => Number(row.alarmStatus) === 1).length
  525. const repairCount = rows.filter(row => Number(row.currentStatus) === 3).length
  526. const pendingCount = rows.filter(row => row.workOrderStatusCode && !row.workOrderTerminal).length
  527. return [
  528. {label: '异常设备', value: page.value.total, unit: '台', icon: Warning},
  529. {label: '报警中', value: alarmCount, unit: '台', icon: Bell},
  530. {label: '维修中', value: repairCount, unit: '台', icon: Tools},
  531. {label: '待闭环', value: pendingCount, unit: '条', icon: DataLine}
  532. ]
  533. })
  534. const ledgerStatsCards = computed(() => [
  535. {label: '设备数量', value: ledgerStats.value.total, unit: '台', icon: Monitor},
  536. {label: '在线设备', value: ledgerStats.value.online, unit: '台', icon: DataLine},
  537. {label: '离线设备', value: ledgerStats.value.offline, unit: '台', icon: Warning},
  538. {label: '在线率', value: ledgerStats.value.onlineRate, unit: '%', icon: Tools}
  539. ])
  540. const detailFields = computed(() => [['设备编码', detail.value.equipmentCode], ['设备类型', detail.value.equipmentTypeName || detail.value.typeName], ['安装位置', detail.value.equipmentLocation], ['设备状态', detail.value.currentStatusText], ['在线状态', detail.value.onlineStatusText], ['报警状态', detail.value.alarmStatusText], ['报警等级', detail.value.alarmLevelText], ['异常原因', detail.value.abnormalReasonText], ['关联工单', detail.value.relatedOrderNo || detail.value.orderNo], ['工单状态', detail.value.workOrderStatusText || detail.value.status], ['接单责任人', detail.value.relatedWorkOrderPerson || detail.value.maintainPerson], ['状态更新时间', detail.value.statusUpdateTime]].map(([label, value]) => ({
  541. label,
  542. value: value ?? '-'
  543. })))
  544. async function loadData() {
  545. loading.value = true;
  546. loadError.value = '';
  547. try {
  548. if (view === 'gis') {
  549. const response = await listDeviceGisPoints({...query.value});
  550. const records = normalizeDeviceGisPoints(response).map(normalizeDeviceRow);
  551. page.value = {records, total: records.length, current: 1, size: records.length || 10};
  552. return
  553. }
  554. let response;
  555. if (view === 'maintain') response = await listMaintainRecords({...query.value}); else if (view === 'dispatch' || view === 'abnormal') response = await listAbnormalDevices({...query.value, ...(view === 'dispatch' ? {dispatchableOnly: true} : {})}); else response = await listDeviceLedger({...query.value});
  556. const normalized = normalizePage(response);
  557. page.value = {...normalized, records: normalized.records.map(normalizeDeviceRow)}
  558. if (view === 'ledger') await loadLedgerStats()
  559. } catch (error) {
  560. const retained = preserveDevicePageOnError(page.value);
  561. page.value = retained.page;
  562. loadError.value = retained.error;
  563. console.warn(`[${title}] 数据加载失败`, error)
  564. } finally {
  565. loading.value = false
  566. }
  567. }
  568. async function loadLedgerStats() {
  569. try {
  570. const response = await getDeviceLedgerStatistics()
  571. const stats = response?.data ?? response ?? {}
  572. ledgerStats.value = {
  573. total: Number(stats.total ?? 0) || 0,
  574. online: Number(stats.online ?? 0) || 0,
  575. offline: Number(stats.offline ?? 0) || 0,
  576. onlineRate: Number(stats.onlineRate ?? 0) || 0
  577. }
  578. } catch (error) {
  579. console.warn('设备台账统计加载失败', error)
  580. }
  581. }
  582. function reset() {
  583. query.value = {
  584. pageNum: 1,
  585. pageSize: 10,
  586. equipmentName: '',
  587. equipmentCode: '',
  588. alarmStatus: undefined,
  589. alertLevel: undefined
  590. };
  591. loadData()
  592. }
  593. function selectPoint(row) {
  594. detail.value = normalizeDeviceRow(row);
  595. detailVisible.value = true
  596. }
  597. watch(deptFilterText, value => deptTreeRef.value?.filter(value))
  598. function filterDeptNode(value, data) {
  599. return !value || String(data.deptName || '').includes(value)
  600. }
  601. async function loadDeptTree(force = false) {
  602. if (!force && deptTreeData.value.length) return true;
  603. deptLoadError.value = '';
  604. try {
  605. const response = await listDept();
  606. deptTreeData.value = handleTree(response.data || [], 'deptId');
  607. return true
  608. } catch (error) {
  609. deptLoadError.value = '部门列表加载失败,请点击重试';
  610. userLoadError.value = deptLoadError.value;
  611. return false
  612. }
  613. }
  614. async function loadAssigneeUsers() {
  615. userLoading.value = true;
  616. userLoadError.value = '';
  617. try {
  618. const response = await listUser(buildAssigneeQuery({
  619. deptId: assigneeDeptId.value,
  620. keyword: assigneeKeyword.value,
  621. pageNum: assigneePageNum.value,
  622. pageSize: assigneePageSize.value
  623. }));
  624. const rows = response.rows || response.data?.rows || [];
  625. users.value = rows.map(normalizeAssigneeUser).filter(user => user.eligible);
  626. assigneeTotal.value = Number(response.total ?? response.data?.total ?? users.value.length) || 0
  627. } catch (error) {
  628. userLoadError.value = '责任人列表加载失败,请点击重试'
  629. } finally {
  630. userLoading.value = false
  631. }
  632. }
  633. function searchAssigneeUsers() {
  634. assigneePageNum.value = 1;
  635. loadAssigneeUsers()
  636. }
  637. function handleAssigneeSizeChange() {
  638. assigneePageNum.value = 1;
  639. loadAssigneeUsers()
  640. }
  641. async function retryAssigneeLoading() {
  642. const deptLoaded = await loadDeptTree(true);
  643. if (!deptLoaded) return;
  644. await loadAssigneeUsers()
  645. }
  646. async function openOrder(row) {
  647. dispatchDevice.value = row
  648. dispatchForm.value = {
  649. description: row.abnormalReason || row.abnormalReasonText || row.description || '',
  650. orderLevel: 2,
  651. receiveUserId: undefined
  652. }
  653. selectedAssignee.value = null
  654. assigneeDraft.value = null
  655. assigneeDeptId.value = undefined
  656. assigneeKeyword.value = ''
  657. assigneePageNum.value = 1
  658. dispatchVisible.value = true
  659. await retryAssigneeLoading()
  660. }
  661. function handleDeptClick(node) {
  662. assigneeDeptId.value = node.deptId;
  663. assigneePageNum.value = 1;
  664. loadAssigneeUsers()
  665. }
  666. function chooseAssignee(row) {
  667. assigneeDraft.value = selectSingleAssignee([row])
  668. }
  669. function confirmAssignee() {
  670. selectedAssignee.value = assigneeDraft.value;
  671. dispatchForm.value.receiveUserId = assigneeDraft.value?.userId
  672. }
  673. async function submitDispatch() {
  674. if (!dispatchForm.value.description?.trim()) return ElMessage.warning('请填写问题描述')
  675. if (!dispatchForm.value.receiveUserId) return ElMessage.warning('请选择接单责任人')
  676. dispatching.value = true
  677. try {
  678. const response = await createAbnormalOrder(buildMaintainOrderPayload(dispatchDevice.value, dispatchForm.value))
  679. const summary = normalizeDispatchOrderResponse(response)
  680. if (!summary) throw new Error('派单失败:服务端未返回有效的待接单工单')
  681. ElMessage.success(`已派单,等待接单(${summary.orderNo})`)
  682. dispatchVisible.value = false
  683. await loadData()
  684. } catch (error) {
  685. const responseData = error?.response?.data
  686. const message = responseData?.msg || responseData?.message || error?.message || '派单失败,请检查权限或服务状态'
  687. // A conflict means another order now owns the device; refresh the list so its order number is visible.
  688. if (/已有|冲突|重复|处理中的工单|关联工单/.test(String(message))) {
  689. await loadData()
  690. ElMessage.warning(`该设备已有相关处置工单:${message}`)
  691. } else {
  692. ElMessage.error(message)
  693. }
  694. } finally {
  695. dispatching.value = false
  696. }
  697. }
  698. async function removeRecord(row) {
  699. try {
  700. await deleteMaintainRecord(row.maintainId || row.id);
  701. ElMessage.success('记录已删除');
  702. await loadData()
  703. } catch (error) {
  704. ElMessage.error('删除失败')
  705. }
  706. }
  707. function emptyDeviceForm() {
  708. return {
  709. equipmentId: undefined, equipmentCode: '', equipmentTypeId: undefined, equipmentTypeName: '',
  710. equipmentName: '', equipmentModel: '', district: '', manufacturer: '', longitude: undefined,
  711. latitude: undefined, equipmentLocation: '', maintainer: '', maintainerPhone: '', ownershipUnit: '',
  712. equipmentImage: '', remark: ''
  713. }
  714. }
  715. function isWaterDeviceType(type, typesById) {
  716. if (!type || (type.typeName === '供水' && type.parentTypeId === '0')) return false
  717. let cursor = type
  718. let guard = 0
  719. while (cursor && guard++ <= typesById.size) {
  720. if (cursor.typeName === '供水' && cursor.parentTypeId === '0') return true
  721. cursor = typesById.get(cursor.parentTypeId)
  722. }
  723. return false
  724. }
  725. async function loadWaterTypes() {
  726. if (waterDeviceTypes.value.length) return
  727. try {
  728. const response = await listEquipmentTypeAll()
  729. const rows = response?.data || []
  730. const typesById = new Map(rows.map(item => [item.id, item]))
  731. waterDeviceTypes.value = rows.filter(item => isWaterDeviceType(item, typesById))
  732. .map(({id, typeName}) => ({id, typeName}))
  733. } catch (error) {
  734. ElMessage.warning('供水设备类型加载失败,请重试')
  735. }
  736. }
  737. async function openCreate() {
  738. editMode.value = 'create'
  739. editVisible.value = true
  740. editSaving.value = false
  741. editForm.value = emptyDeviceForm()
  742. await loadWaterTypes()
  743. }
  744. function handleDeviceSelectionChange(rows) {
  745. selectedDevices.value = rows
  746. }
  747. async function confirmDeleteDevices(ids) {
  748. await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 台设备吗?删除后不可恢复。`, '删除设备', {type: 'warning'})
  749. await deleteWaterDevices(ids)
  750. ElMessage.success('删除成功')
  751. selectedDevices.value = []
  752. query.value.pageNum = 1
  753. await loadData()
  754. }
  755. async function removeDevice(row) {
  756. if (!row?.equipmentId) return
  757. await confirmDeleteDevices([row.equipmentId])
  758. }
  759. async function removeSelectedDevices() {
  760. const ids = selectedDevices.value.map(row => row.equipmentId).filter(Boolean)
  761. if (!ids.length) return
  762. await confirmDeleteDevices(ids)
  763. }
  764. async function openEdit(row) {
  765. editMode.value = 'edit'
  766. editVisible.value = true
  767. editSaving.value = false
  768. editForm.value = {...row}
  769. try {
  770. const response = await getWaterDeviceById(row.equipmentId)
  771. const full = response?.data || response
  772. if (full) editForm.value = {...editForm.value, ...full}
  773. } catch (error) {
  774. ElMessage.warning('设备详情加载失败,已使用列表数据填充')
  775. }
  776. }
  777. async function submitEdit() {
  778. try {
  779. await editFormRef.value?.validate()
  780. } catch {
  781. return
  782. }
  783. editSaving.value = true
  784. try {
  785. const creating = editMode.value === 'create'
  786. if (creating) await createWaterDevice(editForm.value)
  787. else await updateWaterDevice(editForm.value)
  788. ElMessage.success(creating ? '新增成功' : '修改成功')
  789. editVisible.value = false
  790. if (creating) query.value.pageNum = 1
  791. await loadData()
  792. } catch (error) {
  793. ElMessage.error(error?.response?.data?.msg || error?.message || (editMode.value === 'create' ? '新增失败,请稍后重试' : '修改失败,请稍后重试'))
  794. } finally {
  795. editSaving.value = false
  796. }
  797. }
  798. function openImport() {
  799. importVisible.value = true
  800. }
  801. function handleImportChange(file, fileList) {
  802. if (!/\.(xlsx|xls)$/iu.test(file?.name || '')) {
  803. ElMessage.error('仅支持 xls、xlsx 文件')
  804. importFileList.value = []
  805. return false
  806. }
  807. importFileList.value = fileList.slice(-1)
  808. }
  809. function handleImportRemove(file, fileList) {
  810. importFileList.value = fileList
  811. }
  812. function resetImport() {
  813. importUploading.value = false
  814. importUpdateSupport.value = false
  815. importFileList.value = []
  816. importUploadRef.value?.clearFiles()
  817. }
  818. async function submitImport() {
  819. if (!importFileList.value.length) return ElMessage.warning('请选择 xls 或 xlsx 文件')
  820. importUploading.value = true
  821. try {
  822. const response = await importDeviceLedger(importFileList.value[0].raw, importUpdateSupport.value)
  823. if (response?.code && response.code !== 200) throw new Error(response.msg || response.message || '导入失败,请检查文件内容')
  824. ElMessage.success(response?.msg || response?.data || '导入成功')
  825. importVisible.value = false
  826. await loadData()
  827. } catch (error) {
  828. ElMessage.error(error?.response?.data?.msg || error?.response?.data?.message || error?.message || '导入失败,请稍后重试')
  829. } finally {
  830. importUploading.value = false
  831. }
  832. }
  833. async function exportData() {
  834. try {
  835. const response = await (view === 'dispatch' || view === 'abnormal' ? exportAbnormalDevices : exportDeviceLedger)({...query.value})
  836. const errorMessage = await readMonitoringExportError(response)
  837. if (errorMessage) throw new Error(errorMessage)
  838. const blob = response instanceof Blob ? response : new Blob([response], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})
  839. saveAs(blob, `${title}_${new Date().toISOString().slice(0, 10)}.xlsx`)
  840. ElMessage.success('导出成功')
  841. } catch (error) {
  842. const backendMessage = await readMonitoringExportError(error?.response?.data)
  843. ElMessage.error(backendMessage || error?.message || '导出失败,请稍后重试')
  844. }
  845. }
  846. onMounted(loadData)
  847. </script>
  848. <style scoped>
  849. .device-page {
  850. padding: 20px;
  851. background: linear-gradient(135deg, #eff6ff 0%, #f8fafc 45%, #eef2f7 100%);
  852. min-height: calc(100vh - 84px)
  853. }
  854. .page-heading {
  855. display: flex;
  856. justify-content: space-between;
  857. align-items: center;
  858. gap: 16px;
  859. margin-bottom: 16px
  860. }
  861. .page-title {
  862. display: flex;
  863. align-items: center;
  864. gap: 12px;
  865. min-width: 0
  866. }
  867. .page-title-icon {
  868. display: grid;
  869. place-items: center;
  870. width: 42px;
  871. height: 42px;
  872. color: #fff;
  873. background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
  874. border-radius: 8px;
  875. box-shadow: 0 8px 16px rgba(37, 99, 235, .2)
  876. }
  877. .page-title h2 {
  878. margin: 0 0 4px;
  879. font-size: 20px;
  880. color: #1e293b
  881. }
  882. .muted {
  883. font-size: 13px;
  884. color: #64748b
  885. }
  886. .heading-refresh {
  887. border-color: #bfdbfe;
  888. color: #2563eb;
  889. background: #fff
  890. }
  891. .filter-card, .water-panel {
  892. border: 1px solid #e2e8f0;
  893. border-radius: 8px;
  894. box-shadow: 0 6px 14px rgba(15, 23, 42, .04)
  895. }
  896. .filter-card {
  897. margin-bottom: 16px;
  898. background: #fff
  899. }
  900. .filter-card :deep(.el-card__header) {
  901. padding: 14px 18px;
  902. border-bottom: 1px solid #e2e8f0;
  903. background: #fbfdff
  904. }
  905. .filter-card :deep(.el-form-item) {
  906. margin-top: 8px;
  907. margin-right: 16px;
  908. margin-bottom: 8px
  909. }
  910. .filter-card :deep(.el-form-item__label) {
  911. color: #475569
  912. }
  913. .metric-grid {
  914. display: grid;
  915. grid-template-columns:repeat(4, minmax(0, 1fr));
  916. gap: 14px;
  917. margin-bottom: 16px
  918. }
  919. .metric {
  920. display: flex;
  921. align-items: center;
  922. gap: 14px;
  923. min-width: 0;
  924. padding: 16px 18px;
  925. background: #fff;
  926. border: 1px solid #e2e8f0;
  927. border-radius: 8px;
  928. box-shadow: 0 6px 14px rgba(15, 23, 42, .04)
  929. }
  930. .metric-icon {
  931. position: static;
  932. flex: 0 0 42px;
  933. display: grid;
  934. place-items: center;
  935. width: 42px;
  936. height: 42px;
  937. color: #2563eb;
  938. background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
  939. border-radius: 8px
  940. }
  941. .metric-icon .el-icon {
  942. font-size: 22px
  943. }
  944. .metric-body {
  945. min-width: 0;
  946. text-align: left
  947. }
  948. .metric-body span {
  949. display: block;
  950. overflow: hidden;
  951. margin-bottom: 3px;
  952. color: #64748b;
  953. font-size: 13px;
  954. text-overflow: ellipsis;
  955. white-space: nowrap
  956. }
  957. .metric-value {
  958. display: flex;
  959. align-items: baseline;
  960. gap: 5px;
  961. min-width: 0
  962. }
  963. .metric-value strong {
  964. font-size: 24px;
  965. line-height: 1.2;
  966. color: #1e293b
  967. }
  968. .metric-value small {
  969. color: #94a3b8
  970. }
  971. .water-panel {
  972. overflow: hidden
  973. }
  974. .water-panel :deep(.el-card__header) {
  975. padding: 14px 18px;
  976. border-bottom: 1px solid #e2e8f0;
  977. background: #fbfdff
  978. }
  979. .card-title {
  980. display: flex;
  981. align-items: center;
  982. gap: 8px;
  983. color: #1e293b;
  984. font-weight: 600;
  985. margin-bottom: 16px
  986. }
  987. .card-title .el-icon {
  988. color: #2563eb;
  989. font-size: 18px
  990. }
  991. .table-toolbar {
  992. display: flex;
  993. justify-content: space-between;
  994. align-items: center
  995. }
  996. .panel-actions {
  997. display: flex;
  998. align-items: center;
  999. gap: 8px
  1000. }
  1001. .load-error {
  1002. margin-bottom: 14px
  1003. }
  1004. .device-page :deep(.el-table) {
  1005. --el-table-border-color: #e2e8f0;
  1006. --el-table-header-bg-color: #f8fafc;
  1007. --el-table-header-text-color: #475569;
  1008. --el-table-row-hover-bg-color: #eff6ff
  1009. }
  1010. .device-page :deep(.el-table__row) {
  1011. cursor: pointer
  1012. }
  1013. .device-image {
  1014. width: 66px;
  1015. height: 44px;
  1016. border-radius: 6px;
  1017. overflow: hidden
  1018. }
  1019. .image-placeholder {
  1020. display: inline-grid;
  1021. place-items: center;
  1022. width: 66px;
  1023. height: 44px;
  1024. color: #94a3b8;
  1025. font-size: 12px;
  1026. background: #f1f5f9;
  1027. border-radius: 6px
  1028. }
  1029. .detail-summary {
  1030. display: flex;
  1031. align-items: center;
  1032. gap: 12px;
  1033. margin-bottom: 16px
  1034. }
  1035. .detail-name {
  1036. font-size: 18px;
  1037. font-weight: 700;
  1038. color: #17324d
  1039. }
  1040. .device-detail-drawer :deep(.el-drawer__body), .device-edit-drawer :deep(.el-drawer__body) {
  1041. overflow: auto;
  1042. padding: 20px 24px
  1043. }
  1044. .device-detail-drawer :deep(.el-descriptions__label) {
  1045. white-space: nowrap
  1046. }
  1047. .device-detail-drawer :deep(.el-descriptions__content) {
  1048. overflow-wrap: anywhere;
  1049. word-break: break-word
  1050. }
  1051. .detail-media {
  1052. margin-bottom: 14px;
  1053. padding: 12px;
  1054. background: #f7fbff;
  1055. border: 1px solid rgba(37, 99, 235, .1);
  1056. border-radius: 10px
  1057. }
  1058. .detail-media-label {
  1059. margin-bottom: 8px;
  1060. font-weight: 600;
  1061. color: #17324d
  1062. }
  1063. .detail-image {
  1064. width: 100%;
  1065. height: 180px;
  1066. border-radius: 8px;
  1067. overflow: hidden
  1068. }
  1069. .detail-image-placeholder {
  1070. display: grid;
  1071. place-items: center;
  1072. height: 120px;
  1073. background: #f1f5f9;
  1074. border-radius: 8px;
  1075. color: #909399
  1076. }
  1077. .full-input {
  1078. width: 100%
  1079. }
  1080. .import-update {
  1081. margin-top: 10px
  1082. }
  1083. .import-tip {
  1084. line-height: 1.6
  1085. }
  1086. .el-pagination {
  1087. justify-content: flex-end;
  1088. margin-top: 16px
  1089. }
  1090. .dispatch-layout {
  1091. display: flex;
  1092. gap: 20px;
  1093. min-height: 420px
  1094. }
  1095. .dispatch-form {
  1096. width: 300px;
  1097. min-width: 280px;
  1098. padding-left: 20px;
  1099. border-left: 1px solid #e4e7ed
  1100. }
  1101. .assignee-field {
  1102. display: flex;
  1103. align-items: center;
  1104. gap: 12px;
  1105. width: 100%;
  1106. min-height: 32px
  1107. }
  1108. .assignee-summary {
  1109. color: #17324d
  1110. }
  1111. .assignee-placeholder {
  1112. color: #909399
  1113. }
  1114. .assignee-picker {
  1115. display: flex;
  1116. gap: 16px;
  1117. flex: 1;
  1118. min-width: 0;
  1119. min-height: 390px
  1120. }
  1121. .assignee-depts {
  1122. width: 190px;
  1123. min-width: 190px;
  1124. padding-right: 16px;
  1125. border-right: 1px solid #e4e7ed
  1126. }
  1127. .assignee-tree {
  1128. margin-top: 10px;
  1129. max-height: 340px;
  1130. overflow: auto
  1131. }
  1132. .assignee-users {
  1133. flex: 1;
  1134. min-width: 0
  1135. }
  1136. .assignee-search {
  1137. display: flex;
  1138. gap: 8px;
  1139. margin-bottom: 12px
  1140. }
  1141. .assignee-search .el-input {
  1142. flex: 1
  1143. }
  1144. .assignee-users .el-pagination {
  1145. margin-top: 12px
  1146. }
  1147. .assignee-picked {
  1148. display: flex;
  1149. justify-content: space-between;
  1150. align-items: center;
  1151. gap: 8px;
  1152. margin-top: 14px;
  1153. padding: 10px 12px;
  1154. background: #f5f7fa;
  1155. border: 1px solid #ebeef5;
  1156. color: #606266
  1157. }
  1158. .assignee-picked strong {
  1159. color: #2563eb
  1160. }
  1161. .assignee-users .el-alert {
  1162. margin-bottom: 12px
  1163. }
  1164. @media (max-width: 900px) {
  1165. .metric-grid {
  1166. grid-template-columns:repeat(2, minmax(0, 1fr))
  1167. }
  1168. .filter-card :deep(.el-form-item) {
  1169. margin-bottom: 12px
  1170. }
  1171. }
  1172. @media (max-width: 768px) {
  1173. .device-detail-drawer, .device-edit-drawer {
  1174. width: 100% !important
  1175. }
  1176. .dispatch-dialog {
  1177. width: calc(100% - 24px) !important;
  1178. margin-left: auto;
  1179. margin-right: auto
  1180. }
  1181. .dispatch-layout {
  1182. flex-direction: column
  1183. }
  1184. .assignee-picker {
  1185. flex-direction: column
  1186. }
  1187. .assignee-depts {
  1188. width: auto;
  1189. min-width: 0;
  1190. padding-right: 0;
  1191. padding-bottom: 12px;
  1192. border-right: 0;
  1193. border-bottom: 1px solid #e4e7ed
  1194. }
  1195. .assignee-tree {
  1196. max-height: 180px
  1197. }
  1198. .dispatch-form {
  1199. width: auto;
  1200. min-width: 0;
  1201. padding-left: 0;
  1202. padding-top: 16px;
  1203. border-left: 0;
  1204. border-top: 1px solid #e4e7ed
  1205. }
  1206. }
  1207. </style>
  1208. <style lang="scss">
  1209. @use './waterSupplyPageTheme.scss';
  1210. @media (max-width: 768px) {
  1211. .device-detail-drawer {
  1212. width: 100% !important
  1213. }
  1214. .dispatch-dialog {
  1215. width: calc(100% - 24px) !important;
  1216. margin-left: auto;
  1217. margin-right: auto
  1218. }
  1219. .dispatch-layout {
  1220. flex-direction: column
  1221. }
  1222. .assignee-picker {
  1223. flex-direction: column
  1224. }
  1225. .assignee-depts {
  1226. width: auto;
  1227. min-width: 0;
  1228. padding-right: 0;
  1229. padding-bottom: 12px;
  1230. border-right: 0;
  1231. border-bottom: 1px solid #e4e7ed
  1232. }
  1233. .assignee-tree {
  1234. max-height: 180px
  1235. }
  1236. .dispatch-form {
  1237. width: auto;
  1238. min-width: 0;
  1239. padding-left: 0;
  1240. padding-top: 16px;
  1241. border-left: 0;
  1242. border-top: 1px solid #e4e7ed
  1243. }
  1244. }
  1245. </style>