RobotTaskHistoryPage.vue 9.79 KB
<!--
  任务历史页面
  @author zzy
-->
<script setup lang="ts">
import { computed, onBeforeMount, ref, shallowRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from 'vuestic-ui'
import { ColDef, FilterChangedEvent, GridReadyEvent, ITooltipParams } from 'ag-grid-community'
import robotTasksApi from '../../services/robotTasks'
import { useGridFilters } from '../../composables/useGridFilters'
import { useOptions } from '../../composables/useOptions'

const rowData = ref<any[] | null>(null)
const { t } = useI18n()
const { init: notify } = useToast()
const { containsFilterParams, dateFilterParams, createSelectFilterParams, gridLocaleText } = useGridFilters()
const { loadOptions, getTaskStatusOptions } = useOptions()

const taskStatusOptions = computed(() => getTaskStatusOptions())

const columnDefs = shallowRef<ColDef[]>([
  {
    headerName: t('robotTasks.table.serialNumber'),
    width: 80,
    filter: false,
    sortable: false,
    valueGetter: (params: any) => (params.node?.rowIndex ?? 0) + 1,
    cellStyle: { textAlign: 'center' },
  },
  { field: 'taskCode', headerName: t('robotTasks.table.taskCode'), width: 250 },
  { field: 'taskName', headerName: t('robotTasks.table.taskName'), width: 200 },
  { field: 'beginLocationCode', headerName: t('robotTasks.table.beginNodeCode'), width: 150 },
  { field: 'endLocationCode', headerName: t('robotTasks.table.endNodeCode'), width: 150 },
  { field: 'robotCode', headerName: t('robotTasks.table.robotCode'), width: 120 },
  { field: 'taskTemplateCode', headerName: t('robotTasks.table.taskTemplateName'), width: 150 },
  {
    field: 'pause',
    headerName: t('robotTasks.table.pause'),
    width: 100,
    filterParams: null,
    valueFormatter: (p: any) => (p.value ? t('grid.filter.true') : t('grid.filter.false')),
  },
  {
    field: 'status',
    headerName: t('robotTasks.table.status'),
    width: 110,
    filterParams: createSelectFilterParams(taskStatusOptions.value),
    valueGetter: (params: any) => {
      const value = params.data?.status
      if (value === undefined || value === null) return ''
      const option = taskStatusOptions.value.find((opt) => opt.value === value)
      return option?.text || params.data?.statusName || value
    },
  },
  { field: 'priority', headerName: t('robotTasks.table.priority'), width: 80, cellStyle: { textAlign: 'center' } },
  { field: 'shelfCode', headerName: t('robotTasks.table.shelfCode'), width: 120 },
  {
    field: 'containerCode',
    headerName: t('robots.cacheLocation.containerId'),
    width: 120,
    valueGetter: (params: any) =>
      params.data?.containerCode ?? params.data?.containerId ?? params.data?.containerID ?? '',
  },
  { field: 'source', headerName: t('robotTasks.table.source'), width: 120 },
  { field: 'relation', headerName: t('robotTasks.table.relation'), width: 120 },
  {
    field: 'createdAt',
    headerName: t('robotTasks.table.createdAt'),
    minWidth: 180,
    filter: 'agDateColumnFilter',
    filterParams: {
      ...dateFilterParams.value,
      comparator: (filterLocalDateAtMidnight: Date, cellValue: string) => {
        if (!cellValue) return -1
        const cellDate = new Date(cellValue)
        if (cellDate < filterLocalDateAtMidnight) return -1
        if (cellDate > filterLocalDateAtMidnight) return 1
        return 0
      },
    },
  },
  {
    field: 'archivedAt',
    headerName: t('robotTasks.table.archivedAt'),
    minWidth: 180,
    flex: 1,
    filter: 'agDateColumnFilter',
    filterParams: {
      ...dateFilterParams.value,
      comparator: (filterLocalDateAtMidnight: Date, cellValue: string) => {
        if (!cellValue) return -1
        const cellDate = new Date(cellValue)
        if (cellDate < filterLocalDateAtMidnight) return -1
        if (cellDate > filterLocalDateAtMidnight) return 1
        return 0
      },
    },
  },
])

const defaultColDef = ref<ColDef>({
  floatingFilter: true,
  filter: true,
  cellStyle: {
    whiteSpace: 'nowrap',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
  },
  filterParams: containsFilterParams.value,
  tooltipValueGetter: (p: ITooltipParams) => p.value,
})

const gridApi = ref<any>(null)
const isGridInitialized = ref(false)
const isFetchingData = ref(false)
const currentPage = ref(1)
const totalPages = ref(1)
const queryParams = ref({
  pageNumber: 1,
  pageSize: 20,
  filterModel: {} as any,
})

const tbLoading = ref(false)

const showLoading = () => {
  tbLoading.value = true
}

const hideLoading = () => {
  tbLoading.value = false
}

function onGridReady(params: GridReadyEvent) {
  gridApi.value = params.api
  queryParams.value.pageNumber = 1
  queryParams.value.pageSize = 20
  isGridInitialized.value = true
  fetchRowData()
}

const onFilterChanged = (params: FilterChangedEvent) => {
  if (!isGridInitialized.value || isFetchingData.value) return
  const filterModel = params.api.getFilterModel()
  if (filterModel.status) {
    const selectedValue = filterModel.status.type
    filterModel.status = {
      filterType: 'enum',
      type: 'equals',
      filter: parseInt(selectedValue, 10) || selectedValue,
    }
  }
  queryParams.value.filterModel = filterModel
  fetchRowData()
}

const onPaginationChange = (page: number) => {
  if (page !== queryParams.value.pageNumber) {
    queryParams.value.pageNumber = page
    fetchRowData()
  }
}

const refreshTasks = async () => {
  try {
    await fetchRowData()
    notify({ message: t('robotTasks.messages.refreshed'), color: 'success' })
  } catch (err: any) {
    notify({ message: err?.message || t('robotTasks.messages.refreshFailed'), color: 'danger' })
  }
}

const resetFilters = () => {
  try {
    if (gridApi.value) {
      queryParams.value.filterModel = {}
      gridApi.value.setFilterModel(null)
      fetchRowData()
      notify({ message: t('robotTasks.messages.resetFiltersDone'), color: 'success' })
    }
  } catch (err: any) {
    notify({ message: err?.message || t('robotTasks.messages.resetFiltersFailed'), color: 'danger' })
  }
}

const fetchRowData = async () => {
  if (isFetchingData.value) return
  try {
    isFetchingData.value = true
    showLoading()
    const params = {
      ...queryParams.value,
      filterModel: JSON.stringify(queryParams.value.filterModel || {}),
    }
    const res = await robotTasksApi.listHistory(params)
    hideLoading()
    if (!res.success) {
      notify({ message: res.message, color: 'danger' })
    }
    const data = (res && (res.Data ?? res.data)) || []
    rowData.value = Array.isArray(data) ? data : []
    if (res?.pageInfo) {
      const pageInfo = res.pageInfo
      totalPages.value = pageInfo.totalPages || 1
      currentPage.value = pageInfo.pageNumber || 1
    }
  } catch (err: any) {
    hideLoading()
    notify({ message: err?.message || t('robotTasks.messages.fetchFailed'), color: 'danger' })
  } finally {
    isFetchingData.value = false
  }
}

onBeforeMount(async () => {
  await loadOptions()
})

watch(
  taskStatusOptions,
  (newOptions) => {
    if (newOptions && newOptions.length > 0) {
      const cols = [...columnDefs.value]
      const statusCol = cols.find((c) => c.field === 'status')
      if (statusCol) {
        statusCol.filterParams = createSelectFilterParams(newOptions)
        columnDefs.value = cols
      }
    }
  },
  { immediate: true },
)
</script>

<template>
  <VaCard>
    <VaCardContent>
      <div class="flex flex-col md:flex-row gap-2 mb-2 justify-between">
        <div class="flex items-center gap-2">
          <div
            role="group"
            aria-label="task-history-actions"
            style="display: inline-flex; border-radius: 6px; overflow: hidden"
          >
            <VaButton
              size="small"
              :title="t('robotTasks.actions.refresh')"
              :aria-label="t('robotTasks.actions.refresh')"
              preset="primary"
              style="border-radius: 0; margin: 0; border-right: 1px solid rgba(255, 255, 255, 0.06)"
              icon="sync"
              @click="refreshTasks"
            />
            <VaButton
              size="small"
              :title="t('robotTasks.actions.resetFilters')"
              :aria-label="t('robotTasks.actions.resetFilters')"
              preset="primary"
              style="border-radius: 0; margin: 0"
              icon="clear_all"
              @click="resetFilters"
            />
          </div>
        </div>
        <div class="flex items-center">
          <VaPagination
            v-model="currentPage"
            class="justify-end task-pagination"
            style="height: 24px; min-height: 24px"
            :pages="totalPages"
            input
            @update:modelValue="onPaginationChange"
          />
        </div>
      </div>

      <div class="table-container">
        <AgGridVue
          style="width: 100%; height: 100%"
          :tooltip-show-delay="500"
          :column-defs="columnDefs"
          :loading="tbLoading"
          :locale-text="gridLocaleText"
          :default-col-def="defaultColDef"
          :row-selection="'single'"
          :row-data="rowData"
          @gridReady="onGridReady"
          @filterChanged="onFilterChanged"
        ></AgGridVue>
      </div>
    </VaCardContent>
  </VaCard>
</template>

<style lang="scss" scoped>
.table-container {
  width: 100%;
  height: calc(100vh - 195px);
  min-height: 400px;
}

:deep(.va-pagination) {
  height: 24px;
  min-height: 24px;
}

:deep(.va-pagination .va-button) {
  height: 24px;
  min-height: 24px;
  padding: 0 8px;
}

:deep(.task-pagination .va-input) {
  height: 24px;
  min-height: 24px;
  width: 80px !important;
  max-width: 80px !important;
}

:deep(.task-pagination .va-input__input) {
  height: 24px;
  min-height: 24px;
  padding: 0 4px;
  width: 80px !important;
  max-width: 80px !important;
}

:deep(.task-pagination .va-input-wrapper) {
  width: 80px !important;
  max-width: 80px !important;
}

:deep(.task-pagination input) {
  width: 80px !important;
  max-width: 80px !important;
}
</style>