index.vue 13 KB

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