| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- <template>
- <div class="main-wrap flex flex-col gap-y-2 p-4">
- <NCard>
- <div class="mb-2 flex gap-x-4">
- <NForm ref="formRef" :model="form" inline label-placement="left">
- <NFormItem label="站点" path="sn">
- <NInput v-model:value="form.sn" style="width: 250px" clearable />
- </NFormItem>
- <NFormItem label="进度" path="progress">
- <NSelect v-model:value="form.progress" :options="progressOptions" style="width: 120px" clearable />
- </NFormItem>
- </NForm>
- <div class="flex gap-2">
- <NButton type="success" @click="createBatchDownload">
- <template #icon>
- <span class="i-ph:plus-circle" />
- </template>
- 创建任务
- </NButton>
- <NButton type="info" @click="handleQueryClick" :loading="isRequestLoading" :disabled="isRequestLoading">
- <template #icon>
- <span class="i-ph:magnifying-glass" />
- </template>
- 查询
- </NButton>
- <NButton type="warning" @click="resetForm">
- <template #icon>
- <span class="i-ph:arrow-clockwise" />
- </template>
- 重置
- </NButton>
- </div>
- </div>
- <div>
- <NDataTable ref="dataTableRef" v-model:checked-row-keys="checkedRowKeys" :remote="true" :min-height="166.6"
- :max-height="maxHeight" size="small" :columns="tableColumns" :data="tableData" :row-key="tableRowKey"
- :loading="isRequestLoading" striped :single-line="false" :row-props="rowProps" />
- <div class="mt-3 flex items-end justify-between">
- <div class="flex items-center gap-x-3">
- <span>已选择 {{ checkedRowKeys.length }} 条</span>
- <NButtonGroup size="small" :ghost="true">
- <NButton :disabled="isSelectMultiple" @click="handleDownloadClick" secondary type="info">
- 下载
- </NButton>
- <NButton :disabled="isSelectMultiple" @click="handleDeleteClick" secondary type="info">
- 删除
- </NButton>
- </NButtonGroup>
- </div>
- <NPagination v-bind="tablePagination" :prefix="paginationPrefix" />
- </div>
- </div>
- <NDrawer v-model:show="drawerShow" :width="502" placement='right'>
- <NDrawerContent :title="drawerTitle">
- <pre class="whitespace-pre-wrap">{{ drawerContext }}</pre>
- </NDrawerContent>
- </NDrawer>
- </NCard>
- <NDropdown placement="bottom-start" trigger="manual" v-bind="dropdownOptions" :show="showDropdown" />
- </div>
- </template>
- <script setup lang="tsx">
- import { useRouter } from 'vue-router'
- import { NButton, NDataTable, NForm, NNumberAnimation, NInput, NEllipsis, NDrawer, NDrawerContent } from 'naive-ui'
- import type { DataTableColumns, PaginationProps, DropdownProps, EllipsisProps } from 'naive-ui'
- import { useDataTable } from '@/composable/comp/useDataTable'
- import { useResettableReactive } from '@/composable/useResettable'
- import { getBatchDownloadList, createBatchDownload as createBatchDownloadTask, getBatchDownloadPackage, getBatchDownloadPath, updateBatchDownloadOwner } from '@/service/batchDownload'
- import type { BatchDownloadFilter, BatchDownloadInfo, BatchDownloadConfig } from '@/service/batchDownload/interface'
- import ModalData from './ModalData.vue'
- import ViewPathDialog from './component/viewPathDialog.vue'
- defineOptions({
- name: 'BatchDownload',
- })
- const router = useRouter()
- const message = useMessage()
- const modal = useModal()
- const dialog = useDialog()
- const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
- const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
- const { maxHeight } = useDataTable(dataTableRef)
- const isRequestLoading = ref(false)
- const showDropdown = ref(false)
- const checkedRowKeys = ref<Array<number | string>>([])
- const contextmenu = ref<BatchDownloadInfo | undefined>(undefined)
- const isSelectMultiple = ref(false)
- const drawerShow = ref(false)
- const drawerTitle = ref<string>("")
- const drawerContext = ref<string>("")
- type FilterForm = BatchDownloadFilter
- const [form, , resetForm] = useResettableReactive<FilterForm>({
- sn: undefined,
- })
- const progressOptions = [
- { label: 'pending', value: 'pending' },
- { label: 'running', value: 'running' },
- { label: 'error', value: 'error' },
- { label: 'failed', value: 'failed' },
- { label: 'complete', value: 'complete' },
- ]
- const handleQueryClick = () => {
- // 请求查询数据
- handleListQuery()
- }
- const prevUserListTotal = ref(0)
- const paginationPrefix: PaginationProps['prefix'] = (info) => {
- const { itemCount } = info
- return (
- itemCount && (
- <div>
- <span>总 </span>
- < NNumberAnimation
- from={prevUserListTotal.value}
- to={itemCount}
- onFinish={() => {
- prevUserListTotal.value = itemCount
- }}
- />
- <span> 条 </span>
- </div>
- )
- )
- }
- const dropdownOptions = reactive<DropdownProps>({
- x: 0,
- y: 0,
- options: [
- {
- label: () => <span class='text-green-500'> 下载 </span>,
- key: 'download',
- disabled: true,
- },
- {
- label: () => <span class='text-red-500'> 删除 </span>,
- key: 'delete',
- disabled: true,
- },
- ],
- onClickoutside: () => {
- showDropdown.value = false
- },
- onSelect: (key: string) => {
- showDropdown.value = false
- if (contextmenu.value === undefined) return
- if (key === 'download') {
- itemDownload(contextmenu.value.id)
- } else if (key === 'delete') {
- itemDelete(contextmenu.value.id)
- }
- },
- })
- function createBatchDownload() {
- const title = '新增数据批量下载'
- const handleSubmitClick = async (data: BatchDownloadConfig) => {
- let promise: Promise<void> | undefined
- promise = createBatchDownloadTask(data)
- const result = await promise?.then(async () => {
- handleListQuery()
- message.success('创建成功')
- return true
- }).catch((err) => {
- message.error(`请求失败。\n${err}`)
- return false
- })
- if (!result) return
- m.destroy()
- }
- function handleCancelClick() {
- m.destroy()
- }
- const m = modal.create({
- title,
- preset: 'card',
- draggable: true,
- style: {
- width: '500px',
- },
- content: () => (
- <ModalData
- onSubmit={handleSubmitClick}
- onCancel={handleCancelClick}
- />
- ),
- })
- }
- const tableRowKey = (rowData: BatchDownloadInfo) => {
- return rowData.id
- }
- function createColumns(): DataTableColumns<BatchDownloadInfo> {
- return [
- {
- type: 'selection',
- fixed: 'left'
- },
- {
- title: 'SN',
- key: 'sn',
- width: 200,
- fixed: 'left',
- render(rowData, rowIndex) {
- return h(NEllipsis, {
- lineClamp: 3,
- tooltip: {
- placement: "right-start", // ⬅️ Tooltip 显示在右侧
- style: {
- maxHeight: "200px", // ⬅️ 多行时滚动
- overflowY: "auto",
- whiteSpace: "pre-line" // ⬅️ \n 正常换行
- }
- }
- } as EllipsisProps,
- {
- default: () =>
- h("span", { style: { whiteSpace: "pre-line" } }, rowData.sn.join("\n"))
- })
- },
- },
- {
- title: '进度',
- key: 'progress',
- width: 100,
- },
- {
- title: '所有者',
- key: 'owner',
- render(rowData, rowIndex) {
- if (rowData.owner === undefined) return 'admin'
- return ['admin', ...rowData.owner].join(', ')
- }
- },
- {
- title: '任务结果',
- key: 'failReasons',
- render(rowData, rowIndex) {
- return h(NEllipsis, {
- lineClamp: 3,
- tooltip: {
- placement: "left-start", // ⬅️ Tooltip 显示在右侧
- style: {
- maxHeight: "200px", // ⬅️ 多行时滚动
- overflowY: "auto",
- whiteSpace: "pre-line", // ⬅️ \n 正常换行
- maxWidth: "480px"
- }
- }
- } as EllipsisProps,
- {
- default: () =>
- h("span", { style: { whiteSpace: "pre-line" } }, rowData.failReasons?.join("\n"))
- })
- },
- },
- {
- title: '操作',
- key: 'operate',
- width: 280,
- fixed: 'right',
- render(row) {
- return h('div', { class: 'flex justify-center gap-4 p-x-4' }, [
- h(NButton, {
- type: "info",
- strong: true,
- size: 'small',
- onClick: () => itemViewInfo(row)
- }, {
- default: () => '详情'
- }),
- h(NButton, {
- type: "info",
- strong: true,
- size: 'small',
- disabled: row.progress !== "complete",
- onClick: () => itemViewPath(row.id)
- }, {
- default: () => '查看'
- }),
- h(NButton, {
- type: "success",
- strong: true,
- size: 'small',
- disabled: row.progress !== "complete",
- onClick: () => itemDownload(row.id)
- }, {
- default: () => '下载'
- }),
- h(NButton, {
- type: "error",
- strong: true,
- size: 'small',
- disabled: row.progress === "pending" || row.progress === "running",
- onClick: () => itemDelete(row.id)
- }, {
- default: () => '删除'
- })
- ])
- }
- }
- ]
- }
- // 表格右键菜单
- function rowProps(row: BatchDownloadInfo) {
- return {
- onContextmenu: (e: MouseEvent) => {
- e.preventDefault()
- showDropdown.value = false
- nextTick().then(() => {
- contextmenu.value = row
- showDropdown.value = true
- dropdownOptions.x = e.clientX
- dropdownOptions.y = e.clientY
- })
- },
- }
- }
- const tableData = ref<BatchDownloadInfo[]>([])
- const tableColumns = createColumns()
- const tablePagination = reactive<PaginationProps>({
- page: 1,
- pageSize: 10,
- showSizePicker: true,
- pageSizes: [10, 20, 50, 100],
- itemCount: 0,
- showQuickJumper: true,
- showQuickJumpDropdown: true,
- onUpdatePage: (page: number) => {
- tablePagination.page = page
- handleListQuery()
- },
- onUpdatePageSize: (pageSize: number) => {
- tablePagination.pageSize = pageSize
- tablePagination.page = 1
- handleListQuery()
- },
- })
- const itemViewInfo = (info: BatchDownloadInfo) => {
- const idStr = `ID: ${info.id}`
- const snStr = "SN:\n" + info.sn.map(sn => `\t${sn}`).join("\n")
- const descStr = `Description: ${info.description}`
- const periodStr = `Period: ${info.timePeriod[0]} - ${info.timePeriod[1]}`
- const powerFlowStr = `\tPower Flow: ${info.options.powerFlow ? '✔️' : '❌'}`
- const mqttLogsStr = `\tMQTT Logs: [${info.options.mqttLogs.join(', ')}]`
- const eventAlarmStr = `\tEvent & Alarm: ${info.options.eventAlarm ? '✔️' : '❌'}`
- const alarmSnapshotStr = `\tAlarm Snapshot: ${info.options.alarmSnapshot ? '✔️' : '❌'}`
- const emsLogsStr = `\tEMS Logs: [${info.options.emsLogs.join(', ')}]`
- const faultRecordStr = `\tFault Record: ${info.options.faultRecord ? '✔️' : '❌'}`
- const updateLogStr = `\tUpdate Log: ${info.options.updateLog ? '✔️' : '❌'}`
- const settingsStr = `\tSettings: ${info.options.settings ? '✔️' : '❌'}`
- const optionStr = "Options:\n" + powerFlowStr + '\n' + mqttLogsStr + '\n' + eventAlarmStr + '\n' + alarmSnapshotStr + '\n' + emsLogsStr + '\n' + faultRecordStr + '\n' + updateLogStr + '\n' + settingsStr
- const progressStr = `Progress: ${info.progress}`
- const failReasonsStr = "Fail Reasons:\n" + info.failReasons.map(fail => `\t${fail}`).join("\n")
- let contextStr = idStr + "\n" + snStr + "\n" + descStr + "\n" + periodStr + "\n" + optionStr + "\n" + progressStr + "\n" + failReasonsStr
- if (info.owner !== undefined) {
- contextStr = contextStr + "\n" + `[${['admin', ...info.owner].join(', ')}]`
- }
- drawerTitle.value = "任务详情"
- drawerContext.value = contextStr
- drawerShow.value = true
- }
- const itemViewPath = async (id: string) => {
- const pathList = await getBatchDownloadPath(id)
- dialog.info({
- title: "数据目录",
- content: () => h(ViewPathDialog, { data: pathList })
- })
- }
- const itemDownload = (id: string) => {
- getBatchDownloadPackage(id)
- }
- const itemDelete = (id: string) => {
- // TODO
- }
- // 批量下载
- const handleDownloadClick = () => {
- return
- }
- // 批量删除
- const handleDeleteClick = () => {
- }
- const queryBatchDownloadList = async (pageSize: number, pageNum: number, filter?: BatchDownloadFilter) => {
- const responseList = await getBatchDownloadList(pageSize, pageNum, filter)
- if (responseList.data != null) {
- tableData.value = responseList.data
- } else {
- tableData.value = []
- }
- tablePagination.itemCount = responseList.total
- }
- const handleListQuery = async () => {
- const pageSize = tablePagination.pageSize ?? 10
- const page = tablePagination.page ?? 1
- // 处理过滤条件
- const filter: FilterForm = {
- sn: form.sn,
- progress: form.progress,
- }
- await queryBatchDownloadList(pageSize, page, filter)
- }
- onMounted(async () => {
- // 请求查询数据
- await handleListQuery()
- })
- </script>
|