index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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 } 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: 'failReasons',
  226. render(rowData, rowIndex) {
  227. return h(NEllipsis, {
  228. lineClamp: 3,
  229. tooltip: {
  230. placement: "left-start", // ⬅️ Tooltip 显示在右侧
  231. style: {
  232. maxHeight: "200px", // ⬅️ 多行时滚动
  233. overflowY: "auto",
  234. whiteSpace: "pre-line", // ⬅️ \n 正常换行
  235. maxWidth: "480px"
  236. }
  237. }
  238. } as EllipsisProps,
  239. {
  240. default: () =>
  241. h("span", { style: { whiteSpace: "pre-line" } }, rowData.failReasons?.join("\n"))
  242. })
  243. },
  244. },
  245. {
  246. title: '操作',
  247. key: 'operate',
  248. width: 280,
  249. fixed: 'right',
  250. render(row) {
  251. return h('div', { class: 'flex justify-center gap-4 p-x-4' }, [
  252. h(NButton, {
  253. type: "info",
  254. strong: true,
  255. size: 'small',
  256. onClick: () => itemViewInfo(row)
  257. }, {
  258. default: () => '详情'
  259. }),
  260. h(NButton, {
  261. type: "info",
  262. strong: true,
  263. size: 'small',
  264. disabled: row.progress !== "complete",
  265. onClick: () => itemViewPath(row.id)
  266. }, {
  267. default: () => '查看'
  268. }),
  269. h(NButton, {
  270. type: "success",
  271. strong: true,
  272. size: 'small',
  273. disabled: row.progress !== "complete",
  274. onClick: () => itemDownload(row.id)
  275. }, {
  276. default: () => '下载'
  277. }),
  278. h(NButton, {
  279. type: "error",
  280. strong: true,
  281. size: 'small',
  282. disabled: row.progress === "pending" || row.progress === "running",
  283. onClick: () => itemDelete(row.id)
  284. }, {
  285. default: () => '删除'
  286. })
  287. ])
  288. }
  289. }
  290. ]
  291. }
  292. // 表格右键菜单
  293. function rowProps(row: BatchDownloadInfo) {
  294. return {
  295. onContextmenu: (e: MouseEvent) => {
  296. e.preventDefault()
  297. showDropdown.value = false
  298. nextTick().then(() => {
  299. contextmenu.value = row
  300. showDropdown.value = true
  301. dropdownOptions.x = e.clientX
  302. dropdownOptions.y = e.clientY
  303. })
  304. },
  305. }
  306. }
  307. const tableData = ref<BatchDownloadInfo[]>([])
  308. const tableColumns = createColumns()
  309. const tablePagination = reactive<PaginationProps>({
  310. page: 1,
  311. pageSize: 10,
  312. showSizePicker: true,
  313. pageSizes: [10, 20, 50, 100],
  314. itemCount: 0,
  315. showQuickJumper: true,
  316. showQuickJumpDropdown: true,
  317. onUpdatePage: (page: number) => {
  318. tablePagination.page = page
  319. handleListQuery()
  320. },
  321. onUpdatePageSize: (pageSize: number) => {
  322. tablePagination.pageSize = pageSize
  323. tablePagination.page = 1
  324. handleListQuery()
  325. },
  326. })
  327. const itemViewInfo = (info: BatchDownloadInfo) => {
  328. const idStr = `ID: ${info.id}`
  329. const snStr = "SN:\n" + info.sn.map(sn => `\t${sn}`).join("\n")
  330. const descStr = `Description: ${info.description}`
  331. const periodStr = `Period: ${info.timePeriod[0]} - ${info.timePeriod[1]}`
  332. const powerFlowStr = `\tPower Flow: ${info.options.powerFlow ? '✔️' : '❌'}`
  333. const mqttLogsStr = `\tMQTT Logs: [${info.options.mqttLogs.join(', ')}]`
  334. const eventAlarmStr = `\tEvent & Alarm: ${info.options.eventAlarm ? '✔️' : '❌'}`
  335. const alarmSnapshotStr = `\tAlarm Snapshot: ${info.options.alarmSnapshot ? '✔️' : '❌'}`
  336. const emsLogsStr = `\tEMS Logs: [${info.options.emsLogs.join(', ')}]`
  337. const faultRecordStr = `\tFault Record: ${info.options.faultRecord ? '✔️' : '❌'}`
  338. const updateLogStr = `\tUpdate Log: ${info.options.updateLog ? '✔️' : '❌'}`
  339. const settingsStr = `\tSettings: ${info.options.settings ? '✔️' : '❌'}`
  340. const optionStr = "Options:\n" + powerFlowStr + '\n' + mqttLogsStr + '\n' + eventAlarmStr + '\n' + alarmSnapshotStr + '\n' + emsLogsStr + '\n' + faultRecordStr + '\n' + updateLogStr + '\n' + settingsStr
  341. const progressStr = `Progress: ${info.progress}`
  342. const failReasonsStr = "Fail Reasons:\n" + info.failReasons.map(fail => `\t${fail}`).join("\n")
  343. const contextStr = idStr + "\n" + snStr + "\n" + descStr + "\n" + periodStr + "\n" + optionStr + "\n" + progressStr + "\n" + failReasonsStr
  344. drawerTitle.value = "任务详情"
  345. drawerContext.value = contextStr
  346. drawerShow.value = true
  347. }
  348. const itemViewPath = async (id: string) => {
  349. const pathList = await getBatchDownloadPath(id)
  350. dialog.info({
  351. title: "数据目录",
  352. content: () => h(ViewPathDialog, { data: pathList })
  353. })
  354. }
  355. const itemDownload = (id: string) => {
  356. getBatchDownloadPackage(id)
  357. }
  358. const itemDelete = (id: string) => {
  359. // TODO
  360. }
  361. // 批量下载
  362. const handleDownloadClick = () => {
  363. return
  364. }
  365. // 批量删除
  366. const handleDeleteClick = () => {
  367. }
  368. const queryBatchDownloadList = async (pageSize: number, pageNum: number, filter?: BatchDownloadFilter) => {
  369. const responseList = await getBatchDownloadList(pageSize, pageNum, filter)
  370. if (responseList.data != null) {
  371. tableData.value = responseList.data
  372. } else {
  373. tableData.value = []
  374. }
  375. tablePagination.itemCount = responseList.total
  376. }
  377. const handleListQuery = async () => {
  378. const pageSize = tablePagination.pageSize ?? 10
  379. const page = tablePagination.page ?? 1
  380. // 处理过滤条件
  381. const filter: FilterForm = {
  382. sn: form.sn,
  383. progress: form.progress,
  384. }
  385. await queryBatchDownloadList(pageSize, page, filter)
  386. }
  387. onMounted(async () => {
  388. // 请求查询数据
  389. await handleListQuery()
  390. })
  391. </script>