index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <template>
  2. <div class="taskList">
  3. <a-spin :spinning="state.onlineDockListLoading" v-if="state.collapsed">
  4. <div class="taskList-left">
  5. <div class="taskList-left-title">
  6. <div>
  7. 当前机场
  8. </div>
  9. <a-checkbox v-model:checked="checkState.checkAll" :indeterminate="checkState.indeterminate"
  10. @change="onCheckAllChange">
  11. 全选
  12. </a-checkbox>
  13. </div>
  14. <div v-for="(dock, index) in state.onlineDockList" :key="dock.sn">
  15. <div :class="[
  16. 'taskList-left-item',
  17. checkState.checkSnList.includes(dock.sn) ? 'taskList-left-item-selected' : ''
  18. ]" @click="onClickCheckItem(dock.sn)">
  19. <Airport :dock="dock" :look-info="false" />
  20. </div>
  21. </div>
  22. <div class="taskList-left-fill"></div>
  23. </div>
  24. </a-spin>
  25. <div :style="{ width: state.collapsed ? 'calc(100% - 250px)' : '100%' }">
  26. <Search :onClickCollapsed="() => { state.collapsed = !state.collapsed }"
  27. :onClickCreateTask="() => { state.visible = true }" :onClickSearch="onClickSearch"
  28. :onClickReset="onClickReset" />
  29. <a-table :scroll="{ x: '100%', y: 500 }" rowKey="job_id" :loading="state.listLoading" :columns="columns"
  30. :dataSource="state.list" @change="refreshData" :rowClassName="rowClassName" :pagination="paginationConfig">
  31. <!-- 计划|实际时间 -->
  32. <template #duration="{ record }">
  33. <div class="flex-row" style="white-space: pre-wrap">
  34. <div>
  35. <div>
  36. {{ record.begin_time }}
  37. </div>
  38. <div>
  39. {{ record.end_time }}
  40. </div>
  41. </div>
  42. <div class="ml10">
  43. <div>
  44. {{ record.execute_time }}
  45. </div>
  46. <div>
  47. {{ record.completed_time }}
  48. </div>
  49. </div>
  50. </div>
  51. </template>
  52. <!-- 执行状态 -->
  53. <template #status="{ record }">
  54. <div style="color: #2B85E4;" v-if="record.status === 1">
  55. 待执行
  56. </div>
  57. <div style="color: #2B85E4;" v-else-if="record.status === 2">
  58. 执行中
  59. </div>
  60. <div style="color: #19BE6B;" v-else-if="record.status === 3">
  61. 完成
  62. </div>
  63. <div style="color: #E02020;" v-else-if="record.status === 4">
  64. 取消
  65. </div>
  66. <div style="color: #E02020;" v-else-if="record.status === 5">
  67. 失败
  68. </div>
  69. <div style="color: #2B85E4;" v-else-if="record.status === 6">
  70. 暂停
  71. </div>
  72. </template>
  73. <!-- 媒体上传 -->
  74. <template #media_upload="{ record }">
  75. <div class="flex-display flex-align-center">
  76. <span class="circle-icon" :style="{ backgroundColor: formatMediaTaskStatus(record).color }"></span>
  77. {{ formatMediaTaskStatus(record).text }}
  78. </div>
  79. <div class="pl15">
  80. {{ formatMediaTaskStatus(record).number }}
  81. </div>
  82. </template>
  83. <!-- 操作 -->
  84. <template #action="{ record }">
  85. <div class="flex-align-center flex-row" style="color: #2d8cf0">
  86. <a-tooltip title="复制任务">
  87. <CopyOutlined style="margin-right: 10px;" />
  88. </a-tooltip>
  89. <a-tooltip title="查看轨迹" v-if="false">
  90. <GatewayOutlined style="margin-right: 10px;" />
  91. </a-tooltip>
  92. <a-tooltip title="删除">
  93. <DeleteOutlined @click="onClickDelete(record.job_id, record.job_name)" />
  94. </a-tooltip>
  95. </div>
  96. </template>
  97. </a-table>
  98. </div>
  99. <CreateTaskModal :jobId="''" :visible="state.visible" :onClickConfirm="createTaskModalOnClickConfirm"
  100. :onClickCancel="createTaskModalOnClickCancel" v-if="state.visible" />
  101. </div>
  102. </template>
  103. <script lang="ts" setup>
  104. import { reactive, onMounted, watch, computed } from 'vue';
  105. import { Modal, message } from 'ant-design-vue';
  106. import { CopyOutlined, GatewayOutlined, DeleteOutlined } from '@ant-design/icons-vue';
  107. import Search from './components/Search.vue';
  108. import Airport from '/@/components/airport/index.vue';
  109. import CreateTaskModal from './components/CreateTaskModal.vue';
  110. import { useMyStore } from '/@/store';
  111. import { useFormatTask } from '/@/components/task/use-format-task';
  112. import { apis } from '/@/api/custom';
  113. import { getDeviceTopo, getUnreadDeviceHms } from '/@/api/manage';
  114. import { getWorkspaceId } from '/@/utils';
  115. import { OnlineDevice, EModeCode } from '/@/types/device';
  116. import { EDeviceTypeName } from '/@/types';
  117. interface State {
  118. visible: boolean,
  119. collapsed: boolean,
  120. onlineDockListLoading: boolean,
  121. onlineDockList: OnlineDevice[],
  122. query: any,
  123. listLoading: boolean,
  124. list: any[],
  125. };
  126. const state: State = reactive({
  127. visible: false,
  128. collapsed: true,
  129. onlineDockListLoading: false,
  130. onlineDockList: [],
  131. query: undefined,
  132. listLoading: false,
  133. list: [],
  134. });
  135. const checkState = reactive({
  136. checkAll: false as boolean,
  137. indeterminate: false as boolean,
  138. checkSnList: [] as string[],
  139. });
  140. watch(() => checkState.checkSnList, val => {
  141. checkState.indeterminate = !!val.length && val.length < state.onlineDockList.length;
  142. checkState.checkAll = val.length === state.onlineDockList.length;
  143. }, { deep: true });
  144. const store = useMyStore();
  145. const { formatMediaTaskStatus } = useFormatTask();
  146. const deviceInfo = computed(() => store.state.deviceState.deviceInfo)
  147. const dockInfo = computed(() => store.state.deviceState.dockInfo)
  148. const hmsInfo = computed({
  149. get: () => store.state.hmsInfo,
  150. set: (val) => {
  151. return val
  152. }
  153. })
  154. const fetchOnlineDock = async () => {
  155. state.onlineDockListLoading = true;
  156. try {
  157. const res = await getDeviceTopo(getWorkspaceId());
  158. if (res.code !== 0) {
  159. return;
  160. }
  161. const list = state.onlineDockList;
  162. res.data.forEach((gateway: any) => {
  163. const child = gateway.children
  164. const device: OnlineDevice = {
  165. model: child?.device_name,
  166. callsign: child?.nickname,
  167. sn: child?.device_sn,
  168. mode: EModeCode.Disconnected,
  169. gateway: {
  170. model: gateway?.device_name,
  171. callsign: gateway?.nickname,
  172. sn: gateway?.device_sn,
  173. domain: gateway?.domain
  174. },
  175. payload: []
  176. }
  177. child?.payloads_list.forEach((payload: any) => {
  178. device.payload.push({
  179. index: payload.index,
  180. model: payload.model,
  181. payload_name: payload.payload_name,
  182. payload_sn: payload.payload_sn,
  183. control_source: payload.control_source,
  184. payload_index: payload.payload_index
  185. })
  186. })
  187. if (EDeviceTypeName.Dock === gateway.domain) {
  188. list.push(device)
  189. }
  190. })
  191. state.onlineDockList = list;
  192. checkState.checkAll = true;
  193. checkState.checkSnList = list.map(item => item.sn);
  194. } catch (error) {
  195. console.error(error);
  196. } finally {
  197. state.onlineDockListLoading = false;
  198. }
  199. }
  200. const fetchList = async () => {
  201. state.listLoading = true;
  202. try {
  203. const res = await apis.fetchJobList({
  204. ...state.query,
  205. snList: checkState.checkSnList.join(','),
  206. page: paginationConfig.current,
  207. page_size: paginationConfig.pageSize
  208. });
  209. if (res.code === 0) {
  210. state.list = res.data.list;
  211. paginationConfig.total = res.data.pagination.total
  212. paginationConfig.current = res.data.pagination.page
  213. paginationConfig.pageSize = res.data.pagination.page_size
  214. }
  215. } catch (e) {
  216. console.error(e);
  217. } finally {
  218. state.listLoading = false;
  219. }
  220. }
  221. function getUnreadHms(sn: string) {
  222. getUnreadDeviceHms(getWorkspaceId(), sn).then(res => {
  223. if (res.data.length !== 0) {
  224. hmsInfo.value[sn] = res.data
  225. }
  226. })
  227. }
  228. function getOnlineDeviceHms() {
  229. const snList = Object.keys(dockInfo.value)
  230. if (snList.length === 0) {
  231. return
  232. }
  233. snList.forEach(sn => {
  234. getUnreadHms(sn)
  235. })
  236. const deviceSnList = Object.keys(deviceInfo.value)
  237. if (deviceSnList.length === 0) {
  238. return
  239. }
  240. deviceSnList.forEach(sn => {
  241. getUnreadHms(sn)
  242. })
  243. }
  244. onMounted(async () => {
  245. await fetchOnlineDock();
  246. setTimeout(() => {
  247. watch(() => store.state.deviceStatusEvent, async data => {
  248. await fetchOnlineDock()
  249. if (data.deviceOnline.sn) {
  250. getUnreadHms(data.deviceOnline.sn)
  251. }
  252. }, { deep: true })
  253. getOnlineDeviceHms()
  254. }, 1000)// 默认3秒,此时改成1秒
  255. await fetchList();
  256. });
  257. // 全选
  258. const onCheckAllChange = async (e: any) => {
  259. Object.assign(checkState, {
  260. checkSnList: e.target.checked ? state.onlineDockList.map(item => item.sn) : [],
  261. indeterminate: false,
  262. });
  263. await fetchList();
  264. }
  265. // 点击勾选条
  266. const onClickCheckItem = async (sn: string) => {
  267. const list = checkState.checkSnList;
  268. if (list.includes(sn)) {
  269. checkState.checkSnList = list.filter(item => item !== sn);
  270. } else {
  271. list.push(sn);
  272. checkState.checkSnList = list;
  273. }
  274. await fetchList();
  275. }
  276. // 新建机场任务弹出层-点击确定
  277. const createTaskModalOnClickConfirm = async () => {
  278. state.visible = false;
  279. }
  280. // 新建机场任务弹出层-点击取消
  281. const createTaskModalOnClickCancel = () => {
  282. state.visible = false;
  283. }
  284. const paginationConfig = reactive({
  285. pageSizeOptions: ['20', '50', '100'],
  286. showQuickJumper: true,
  287. showSizeChanger: true,
  288. pageSize: 20,
  289. current: 1,
  290. total: 0
  291. });
  292. const columns = [
  293. {
  294. title: '计划|实际时间',
  295. dataIndex: 'duration',
  296. width: 200,
  297. slots: { customRender: 'duration' },
  298. },
  299. {
  300. title: '执行状态',
  301. dataIndex: 'status',
  302. width: 100,
  303. slots: { customRender: 'status' },
  304. },
  305. {
  306. title: '任务名称',
  307. dataIndex: 'job_name',
  308. width: 150,
  309. ellipsis: true,
  310. },
  311. {
  312. title: '任务策略',
  313. dataIndex: 'task_type',
  314. width: 120,
  315. customRender: ({ text }: any) => {
  316. let content = '';
  317. switch (text) {
  318. case 0:
  319. content = '立即';
  320. break;
  321. case 1:
  322. content = '定时';
  323. break;
  324. case 2:
  325. content = '循环';
  326. break;
  327. default:
  328. break;
  329. }
  330. return content;
  331. }
  332. },
  333. {
  334. title: '航线名称',
  335. dataIndex: 'file_name',
  336. width: 150,
  337. ellipsis: true,
  338. },
  339. {
  340. title: '设备名称',
  341. dataIndex: 'dock_name',
  342. width: 200,
  343. ellipsis: true,
  344. },
  345. {
  346. title: '创建人',
  347. dataIndex: 'username',
  348. width: 150,
  349. },
  350. {
  351. title: '媒体上传',
  352. dataIndex: 'media_upload',
  353. width: 200,
  354. slots: { customRender: 'media_upload' },
  355. },
  356. {
  357. title: '操作',
  358. dataIndex: 'actions',
  359. fixed: 'right',
  360. width: 100,
  361. slots: { customRender: 'action' },
  362. },
  363. ];
  364. const rowClassName = (record: any, index: number) => {
  365. const className = []
  366. if ((index & 1) === 0) {
  367. className.push('table-striped')
  368. }
  369. return className.toString().replaceAll(',', ' ')
  370. }
  371. const refreshData = async (page: any) => {
  372. paginationConfig.current = page?.current!
  373. paginationConfig.pageSize = page?.pageSize!
  374. await fetchList();
  375. }
  376. // 点击搜索
  377. const onClickSearch = async (query: any) => {
  378. state.query = query;
  379. await fetchList();
  380. }
  381. // 点击重置
  382. const onClickReset = async (query: any) => {
  383. state.query = query;
  384. await fetchList();
  385. }
  386. // 点击删除
  387. const onClickDelete = (id: string, name: string) => {
  388. Modal.confirm({
  389. title: '删除任务',
  390. content: `确定删除${name}吗?`,
  391. okType: 'danger',
  392. onOk: async () => {
  393. try {
  394. await apis.deleteJob({ job_id: id });
  395. await fetchList();
  396. message.success('删除成功');
  397. } catch (error) {
  398. message.error('删除失败: ' + error);
  399. }
  400. },
  401. })
  402. }
  403. </script>
  404. <style lang="scss">
  405. .taskList {
  406. padding: 20px;
  407. display: flex;
  408. &-left {
  409. width: 230px;
  410. height: calc(100vh - 146px);
  411. padding: 10px 8px 0;
  412. background-color: #232323;
  413. overflow-y: auto;
  414. margin-right: 20px;
  415. &-title {
  416. display: flex;
  417. justify-content: space-between;
  418. align-items: center;
  419. color: #FFFFFF;
  420. .ant-checkbox-wrapper {
  421. color: #FFFFFF !important;
  422. }
  423. }
  424. &-item {
  425. border-radius: 4px;
  426. border: 2px solid transparent;
  427. overflow: hidden;
  428. margin-top: 10px;
  429. &-selected {
  430. border-color: #1fa3f6;
  431. }
  432. }
  433. &-fill {
  434. width: 100%;
  435. height: 20px;
  436. }
  437. }
  438. }
  439. .ant-table {
  440. border-top: 1px solid rgb(0, 0, 0, 0.06);
  441. border-bottom: 1px solid rgb(0, 0, 0, 0.06);
  442. }
  443. .ant-table-tbody tr td {
  444. border: 0;
  445. }
  446. .table-striped {
  447. background-color: #f7f9fa;
  448. }
  449. </style>