index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. <template>
  2. <div class="main-wrap flex flex-col gap-y-2 p-4">
  3. <NCard>
  4. <div class="mb-2 flex gap-x-4">
  5. <NForm ref="formRef" :model="form" inline label-placement="left">
  6. <NFormItem label="站点" path="sn">
  7. <NInput v-model:value="form.sn" style="width: 250px" clearable />
  8. </NFormItem>
  9. <NFormItem label="进度" path="progress">
  10. <NSelect v-model:value="form.progress" :options="progressOptions" style="width: 120px" clearable />
  11. </NFormItem>
  12. </NForm>
  13. <div class="flex gap-2">
  14. <NButton type="success" @click="createBatchDownload">
  15. <template #icon>
  16. <span class="i-ph:plus-circle" />
  17. </template>
  18. 创建任务
  19. </NButton>
  20. <NButton type="info" @click="handleQueryClick" :loading="isRequestLoading" :disabled="isRequestLoading">
  21. <template #icon>
  22. <span class="i-ph:magnifying-glass" />
  23. </template>
  24. 查询
  25. </NButton>
  26. <NButton type="warning" @click="resetForm">
  27. <template #icon>
  28. <span class="i-ph:arrow-clockwise" />
  29. </template>
  30. 重置
  31. </NButton>
  32. </div>
  33. </div>
  34. <div>
  35. <NDataTable ref="dataTableRef" v-model:checked-row-keys="checkedRowKeys" :remote="true" :min-height="166.6"
  36. :max-height="maxHeight" size="small" :columns="tableColumns" :data="tableData" :row-key="tableRowKey"
  37. :loading="isRequestLoading" striped :single-line="false" :row-props="rowProps" />
  38. <div class="mt-3 flex items-end justify-between">
  39. <div class="flex items-center gap-x-3">
  40. <span>已选择&nbsp;{{ checkedRowKeys.length }}&nbsp; 条</span>
  41. <NButtonGroup size="small" :ghost="true">
  42. <NButton :disabled="isSelectMultiple" @click="handleDownloadClick" secondary type="info">
  43. 下载
  44. </NButton>
  45. <NButton :disabled="isSelectMultiple" @click="handleDeleteClick" secondary type="info">
  46. 删除
  47. </NButton>
  48. </NButtonGroup>
  49. </div>
  50. <NPagination v-bind="tablePagination" :prefix="paginationPrefix" />
  51. </div>
  52. </div>
  53. <NDrawer v-model:show="drawerShow" :width="502" placement='right'>
  54. <NDrawerContent :title="drawerTitle">
  55. <pre class="whitespace-pre-wrap">{{ drawerContext }}</pre>
  56. </NDrawerContent>
  57. </NDrawer>
  58. </NCard>
  59. <NDropdown placement="bottom-start" trigger="manual" v-bind="dropdownOptions" :show="showDropdown" />
  60. </div>
  61. </template>
  62. <script setup lang="tsx">
  63. import { useRouter } from 'vue-router'
  64. import { NButton, NDataTable, NForm, NNumberAnimation, NInput, NEllipsis, NDrawer, NDrawerContent } from 'naive-ui'
  65. import type { DataTableColumns, PaginationProps, DropdownProps, EllipsisProps } from 'naive-ui'
  66. import { useDataTable } from '@/composable/comp/useDataTable'
  67. import { useResettableReactive } from '@/composable/useResettable'
  68. import { getBatchDownloadList, createBatchDownload as createBatchDownloadTask, getBatchDownloadPackage, getBatchDownloadPath, updateBatchDownloadOwner } from '@/service/batchDownload'
  69. import type { BatchDownloadFilter, BatchDownloadInfo, BatchDownloadConfig } from '@/service/batchDownload/interface'
  70. import ModalData from './ModalData.vue'
  71. import ViewPathDialog from './component/viewPathDialog.vue'
  72. defineOptions({
  73. name: 'BatchDownload',
  74. })
  75. const router = useRouter()
  76. const message = useMessage()
  77. const modal = useModal()
  78. const dialog = useDialog()
  79. const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
  80. const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
  81. const { maxHeight } = useDataTable(dataTableRef)
  82. const isRequestLoading = ref(false)
  83. const showDropdown = ref(false)
  84. const checkedRowKeys = ref<Array<number | string>>([])
  85. const contextmenu = ref<BatchDownloadInfo | undefined>(undefined)
  86. const isSelectMultiple = ref(false)
  87. const drawerShow = ref(false)
  88. const drawerTitle = ref<string>("")
  89. const drawerContext = ref<string>("")
  90. type FilterForm = BatchDownloadFilter
  91. const [form, , resetForm] = useResettableReactive<FilterForm>({
  92. sn: undefined,
  93. })
  94. const progressOptions = [
  95. { label: 'pending', value: 'pending' },
  96. { label: 'running', value: 'running' },
  97. { label: 'error', value: 'error' },
  98. { label: 'failed', value: 'failed' },
  99. { label: 'complete', value: 'complete' },
  100. ]
  101. const handleQueryClick = () => {
  102. // 请求查询数据
  103. handleListQuery()
  104. }
  105. const prevUserListTotal = ref(0)
  106. const paginationPrefix: PaginationProps['prefix'] = (info) => {
  107. const { itemCount } = info
  108. return (
  109. itemCount && (
  110. <div>
  111. <span>总 &nbsp; </span>
  112. < NNumberAnimation
  113. from={prevUserListTotal.value}
  114. to={itemCount}
  115. onFinish={() => {
  116. prevUserListTotal.value = itemCount
  117. }}
  118. />
  119. <span> &nbsp; 条 </span>
  120. </div>
  121. )
  122. )
  123. }
  124. const dropdownOptions = reactive<DropdownProps>({
  125. x: 0,
  126. y: 0,
  127. options: [
  128. {
  129. label: () => <span class='text-green-500'> 下载 </span>,
  130. key: 'download',
  131. disabled: true,
  132. },
  133. {
  134. label: () => <span class='text-red-500'> 删除 </span>,
  135. key: 'delete',
  136. disabled: true,
  137. },
  138. ],
  139. onClickoutside: () => {
  140. showDropdown.value = false
  141. },
  142. onSelect: (key: string) => {
  143. showDropdown.value = false
  144. if (contextmenu.value === undefined) return
  145. if (key === 'download') {
  146. itemDownload(contextmenu.value.id)
  147. } else if (key === 'delete') {
  148. itemDelete(contextmenu.value.id)
  149. }
  150. },
  151. })
  152. function createBatchDownload() {
  153. const title = '新增数据批量下载'
  154. const handleSubmitClick = async (data: BatchDownloadConfig) => {
  155. let promise: Promise<void> | undefined
  156. promise = createBatchDownloadTask(data)
  157. const result = await promise?.then(async () => {
  158. handleListQuery()
  159. message.success('创建成功')
  160. return true
  161. }).catch((err) => {
  162. message.error(`请求失败。\n${err}`)
  163. return false
  164. })
  165. if (!result) return
  166. m.destroy()
  167. }
  168. function handleCancelClick() {
  169. m.destroy()
  170. }
  171. const m = modal.create({
  172. title,
  173. preset: 'card',
  174. draggable: true,
  175. style: {
  176. width: '500px',
  177. },
  178. content: () => (
  179. <ModalData
  180. onSubmit={handleSubmitClick}
  181. onCancel={handleCancelClick}
  182. />
  183. ),
  184. })
  185. }
  186. const tableRowKey = (rowData: BatchDownloadInfo) => {
  187. return rowData.id
  188. }
  189. function createColumns(): DataTableColumns<BatchDownloadInfo> {
  190. return [
  191. {
  192. type: 'selection',
  193. fixed: 'left'
  194. },
  195. {
  196. title: 'SN',
  197. key: 'sn',
  198. width: 200,
  199. fixed: 'left',
  200. render(rowData, rowIndex) {
  201. return h(NEllipsis, {
  202. lineClamp: 3,
  203. tooltip: {
  204. placement: "right-start", // ⬅️ Tooltip 显示在右侧
  205. style: {
  206. maxHeight: "200px", // ⬅️ 多行时滚动
  207. overflowY: "auto",
  208. whiteSpace: "pre-line" // ⬅️ \n 正常换行
  209. }
  210. }
  211. } as EllipsisProps,
  212. {
  213. default: () =>
  214. h("span", { style: { whiteSpace: "pre-line" } }, rowData.sn.join("\n"))
  215. })
  216. },
  217. },
  218. {
  219. title: '进度',
  220. key: 'progress',
  221. width: 100,
  222. },
  223. {
  224. title: '所有者',
  225. key: 'owner',
  226. render(rowData, rowIndex) {
  227. if (rowData.owner === undefined) return 'admin'
  228. return ['admin', ...rowData.owner].join(', ')
  229. }
  230. },
  231. {
  232. title: '任务结果',
  233. key: 'failReasons',
  234. render(rowData, rowIndex) {
  235. return h(NEllipsis, {
  236. lineClamp: 3,
  237. tooltip: {
  238. placement: "left-start", // ⬅️ Tooltip 显示在右侧
  239. style: {
  240. maxHeight: "200px", // ⬅️ 多行时滚动
  241. overflowY: "auto",
  242. whiteSpace: "pre-line", // ⬅️ \n 正常换行
  243. maxWidth: "480px"
  244. }
  245. }
  246. } as EllipsisProps,
  247. {
  248. default: () =>
  249. h("span", { style: { whiteSpace: "pre-line" } }, rowData.failReasons?.join("\n"))
  250. })
  251. },
  252. },
  253. {
  254. title: '操作',
  255. key: 'operate',
  256. width: 280,
  257. fixed: 'right',
  258. render(row) {
  259. return h('div', { class: 'flex justify-center gap-4 p-x-4' }, [
  260. h(NButton, {
  261. type: "info",
  262. strong: true,
  263. size: 'small',
  264. onClick: () => itemViewInfo(row)
  265. }, {
  266. default: () => '详情'
  267. }),
  268. h(NButton, {
  269. type: "info",
  270. strong: true,
  271. size: 'small',
  272. disabled: row.progress !== "complete",
  273. onClick: () => itemViewPath(row.id)
  274. }, {
  275. default: () => '查看'
  276. }),
  277. h(NButton, {
  278. type: "success",
  279. strong: true,
  280. size: 'small',
  281. disabled: row.progress !== "complete",
  282. onClick: () => itemDownload(row.id)
  283. }, {
  284. default: () => '下载'
  285. }),
  286. h(NButton, {
  287. type: "error",
  288. strong: true,
  289. size: 'small',
  290. disabled: row.progress === "pending" || row.progress === "running",
  291. onClick: () => itemDelete(row.id)
  292. }, {
  293. default: () => '删除'
  294. })
  295. ])
  296. }
  297. }
  298. ]
  299. }
  300. // 表格右键菜单
  301. function rowProps(row: BatchDownloadInfo) {
  302. return {
  303. onContextmenu: (e: MouseEvent) => {
  304. e.preventDefault()
  305. showDropdown.value = false
  306. nextTick().then(() => {
  307. contextmenu.value = row
  308. showDropdown.value = true
  309. dropdownOptions.x = e.clientX
  310. dropdownOptions.y = e.clientY
  311. })
  312. },
  313. }
  314. }
  315. const tableData = ref<BatchDownloadInfo[]>([])
  316. const tableColumns = createColumns()
  317. const tablePagination = reactive<PaginationProps>({
  318. page: 1,
  319. pageSize: 10,
  320. showSizePicker: true,
  321. pageSizes: [10, 20, 50, 100],
  322. itemCount: 0,
  323. showQuickJumper: true,
  324. showQuickJumpDropdown: true,
  325. onUpdatePage: (page: number) => {
  326. tablePagination.page = page
  327. handleListQuery()
  328. },
  329. onUpdatePageSize: (pageSize: number) => {
  330. tablePagination.pageSize = pageSize
  331. tablePagination.page = 1
  332. handleListQuery()
  333. },
  334. })
  335. const itemViewInfo = (info: BatchDownloadInfo) => {
  336. const idStr = `ID: ${info.id}`
  337. const snStr = "SN:\n" + info.sn.map(sn => `\t${sn}`).join("\n")
  338. const descStr = `Description: ${info.description}`
  339. const periodStr = `Period: ${info.timePeriod[0]} - ${info.timePeriod[1]}`
  340. const powerFlowStr = `\tPower Flow: ${info.options.powerFlow ? '✔️' : '❌'}`
  341. const mqttLogsStr = `\tMQTT Logs: [${info.options.mqttLogs.join(', ')}]`
  342. const eventAlarmStr = `\tEvent & Alarm: ${info.options.eventAlarm ? '✔️' : '❌'}`
  343. const alarmSnapshotStr = `\tAlarm Snapshot: ${info.options.alarmSnapshot ? '✔️' : '❌'}`
  344. const emsLogsStr = `\tEMS Logs: [${info.options.emsLogs.join(', ')}]`
  345. const faultRecordStr = `\tFault Record: ${info.options.faultRecord ? '✔️' : '❌'}`
  346. const updateLogStr = `\tUpdate Log: ${info.options.updateLog ? '✔️' : '❌'}`
  347. const settingsStr = `\tSettings: ${info.options.settings ? '✔️' : '❌'}`
  348. const optionStr = "Options:\n" + powerFlowStr + '\n' + mqttLogsStr + '\n' + eventAlarmStr + '\n' + alarmSnapshotStr + '\n' + emsLogsStr + '\n' + faultRecordStr + '\n' + updateLogStr + '\n' + settingsStr
  349. const progressStr = `Progress: ${info.progress}`
  350. const failReasonsStr = "Fail Reasons:\n" + info.failReasons.map(fail => `\t${fail}`).join("\n")
  351. let contextStr = idStr + "\n" + snStr + "\n" + descStr + "\n" + periodStr + "\n" + optionStr + "\n" + progressStr + "\n" + failReasonsStr
  352. if (info.owner !== undefined) {
  353. contextStr = contextStr + "\n" + `[${['admin', ...info.owner].join(', ')}]`
  354. }
  355. drawerTitle.value = "任务详情"
  356. drawerContext.value = contextStr
  357. drawerShow.value = true
  358. }
  359. const itemViewPath = async (id: string) => {
  360. const pathList = await getBatchDownloadPath(id)
  361. dialog.info({
  362. title: "数据目录",
  363. content: () => h(ViewPathDialog, { data: pathList }),
  364. style: {
  365. minWidth: '600px',
  366. width: 'fit-content',
  367. maxWidth: '90vw'
  368. }
  369. })
  370. }
  371. const itemDownload = (id: string) => {
  372. getBatchDownloadPackage(id)
  373. }
  374. const itemDelete = (id: string) => {
  375. // TODO
  376. }
  377. // 批量下载
  378. const handleDownloadClick = () => {
  379. return
  380. }
  381. // 批量删除
  382. const handleDeleteClick = () => {
  383. }
  384. const queryBatchDownloadList = async (pageSize: number, pageNum: number, filter?: BatchDownloadFilter) => {
  385. const responseList = await getBatchDownloadList(pageSize, pageNum, filter)
  386. if (responseList.data != null) {
  387. tableData.value = responseList.data
  388. } else {
  389. tableData.value = []
  390. }
  391. tablePagination.itemCount = responseList.total
  392. }
  393. const handleListQuery = async () => {
  394. const pageSize = tablePagination.pageSize ?? 10
  395. const page = tablePagination.page ?? 1
  396. // 处理过滤条件
  397. const filter: FilterForm = {
  398. sn: form.sn,
  399. progress: form.progress,
  400. }
  401. await queryBatchDownloadList(pageSize, page, filter)
  402. }
  403. onMounted(async () => {
  404. // 请求查询数据
  405. await handleListQuery()
  406. })
  407. </script>