浏览代码

feat: 重构首页 Booking筛选项 解决网络竞态问题

Jack Zhou 1 周之前
父节点
当前提交
0851c116c3

+ 47 - 15
src/components/AutoSelect/src/AutoSelect.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { useFiltersStore } from '@/stores/modules/filtersList'
-import { cloneDeep } from 'lodash'
+import { cloneDeep, debounce } from 'lodash'
+import axios from 'axios'
 
 const filtersStore = useFiltersStore()
 
@@ -33,6 +34,7 @@ interface ListItem {
 const options = ref<ListItem[]>([])
 const pageData = ref(cloneDeep(props.data))
 const loading = ref(false)
+const currentController = ref<AbortController | null>(null)
 watch(
   () => props.data,
   (current) => {
@@ -42,27 +44,43 @@ watch(
 
 const remoteMethod = (query: string) => {
   if (query) {
+    currentController.value?.abort()
+
+    const newController = new AbortController()
+    currentController.value = newController
     loading.value = true
 
     const queryData = filtersStore.getQueryData
-    setTimeout(() => {
-      $api
-        .getMoreFiltersData({
+    $api
+      .getMoreFiltersData(
+        {
           term: query,
           type: props.type,
           search_field: props.title,
           search_mode: 'booking',
           ...queryData
-        })
-        .then((res: any) => {
+        },
+        { signal: newController.signal }
+      )
+      .then((res: any) => {
+        if (!newController.signal.aborted && res.code == 200) {
+          options.value = res.data.map((item: any) => {
+            return { value: item, label: item, checked: pageData.value?.includes(item) }
+          })
+        }
+      })
+      .catch((err) => {
+        options.value = []
+        if (!axios.isCancel(err) && !newController.signal.aborted) {
+          ElMessage.error('Failed to load options')
+        }
+      })
+      .finally(() => {
+        // 仅当这是最新请求时,才关闭 loading
+        if (currentController.value === newController) {
           loading.value = false
-          if (res.code == 200) {
-            options.value = res.data.map((item: any) => {
-              return { value: item, label: item, checked: pageData.value?.includes(item) }
-            })
-          }
-        })
-    }, 200)
+        }
+      })
   } else {
     options.value = []
   }
@@ -82,6 +100,20 @@ const removeClass = () => {
     }
   }
 }
+
+const handleBlur = () => {
+  emit('changeFocus', false)
+  nextTick(() => {
+    options.value = []
+  })
+}
+
+// 防抖版本(可选)
+const debouncedRemoteMethod = debounce(remoteMethod, 200)
+
+onUnmounted(() => {
+  currentController.value?.abort()
+})
 </script>
 <template>
   <div>
@@ -95,11 +127,11 @@ const removeClass = () => {
       :placeholder="props.placeholder"
       collapse-tags
       @focus="removeClass"
-      @blur="emit('changeFocus', false)"
+      @blur="handleBlur"
       :disabled="props.isDisabled"
       collapse-tags-tooltip
       :max-collapse-tags="3"
-      :remote-method="remoteMethod"
+      :remote-method="debouncedRemoteMethod"
       :loading="loading"
     >
       <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">

+ 147 - 149
src/components/SelectTable/src/SelectTable.vue

@@ -1,194 +1,192 @@
 <script setup lang="ts" name="SelTable">
+import { ref, reactive, watch, onUnmounted, nextTick } from 'vue'
 import { ElMessage } from 'element-plus'
 import { formatNumber } from '@/utils/tools'
 import { useFiltersStore } from '@/stores/modules/filtersList'
-import { cloneDeep } from 'lodash'
+import { cloneDeep, debounce } from 'lodash'
 import { useRoute } from 'vue-router'
+import axios from 'axios'
 
 const filtersStore = useFiltersStore()
 const route = useRoute()
 const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
 
-const emit = defineEmits(['check', 'input'])
+const emit = defineEmits(['input'])
 const props = defineProps({
-  poverWidth: {
-    type: Number,
-    default: 380
-  },
-
-  data: {
-    type: Array,
-    default: () => []
-  },
-  needKey: {
-    type: String,
-    default: 'city'
-  },
-  isError: {
-    type: Boolean,
-    default: false
-  },
-  title: {
-    type: String,
-    default: ''
-  },
-  disabled: {
-    type: Boolean,
-    default: false
-  }
+  poverWidth: { type: Number, default: 380 },
+  data: { type: Array, default: () => [] },
+  needKey: { type: String, default: 'city' },
+  isError: { type: Boolean, default: false },
+  title: { type: String, default: '' },
+  disabled: { type: Boolean, default: false }
 })
 
-const searchVal = ref(null)
-const innerTags: any = ref([])
-const tagInputRef: any = ref(null)
+const searchVal = ref('')
+const innerTags = ref<string[]>([])
+const tagInputRef = ref<HTMLInputElement | null>(null)
+
+// 用于取消上一次搜索请求
+let searchAbortController: AbortController | null = null
 
 watch(
   () => props.data,
   (current) => {
-    innerTags.value = cloneDeep(current)
+    innerTags.value = cloneDeep(current as string[])
   },
-  {
-    deep: true,
-    immediate: true
-  }
+  { deep: true, immediate: true }
 )
-// 响应数据
-const state: any = reactive({
+
+const state = reactive({
   poverShow: false,
   currentPage: 1,
   pageSize: 10,
   total: 0,
-  activeRowIndex: '',
   tableData: [],
   loading: false
 })
 
-// 点击行
-const handleRowClick = (row: any) => {
-  state.poverShow = false
-  if (!innerTags.value.includes(row[props.needKey])) {
-    innerTags.value.push(row[props.needKey])
-    state.activeRowIndex = row.id
-    emit('input', innerTags.value, props.title)
-  } else {
-    ElMessage({
-      message: 'Cannot add duplicate cities.',
-      type: 'success'
-    })
+// —————— 获取数据 ——————
+const fetchData = async (query: string, page: number, isPageChange = false) => {
+  if (!isPageChange) {
+    if (searchAbortController) {
+      searchAbortController.abort()
+    }
+    searchAbortController = new AbortController()
   }
-  searchVal.value = null
-}
-const handleSearch = (val?) => {
+
   state.loading = true
-  let curTableData: any = []
-  state.tableData = []
-  let rc = '-1'
-  if (val === 'pageChange') {
-    rc = state.total
-  } else {
-    searchVal.value = val ? val.target.value : ''
-    state.total = 0
-  }
-  const queryData = filtersStore.getQueryData
-  setTimeout(() => {
-    $api
-      .getMoreFiltersTableData({
-        term: searchVal.value ? searchVal.value : '',
-        cp: state.currentPage,
+
+  try {
+    const rc = isPageChange ? String(state.total) : '-1'
+    const queryData = filtersStore.getQueryData
+
+    const res = await $api.getMoreFiltersTableData(
+      {
+        term: query?.trim(),
+        cp: page,
         ps: state.pageSize,
         rc,
         search_field: props.title,
         search_mode: searchMode,
         ...queryData
-      })
-      .then((res: any) => {
-        if (res.code == 200) {
-          curTableData = res.data.searchData
-          curTableData = res.data.searchData.filter((p: any) => {
-            return (
-              p.country.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.city.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.uncode.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1
-            )
-          })
-          state.tableData = curTableData
-          state.total = Number(res.data.rc)
-          state.loading = false
-        }
-      })
-  }, 800)
+      },
+      { signal: searchAbortController?.signal }
+    )
+
+    if (searchAbortController?.signal.aborted) return
+
+    if (res.code === 200) {
+      state.tableData = res.data.searchData || []
+      state.total = Number(res.data.rc || 0)
+    } else {
+      state.tableData = []
+      state.total = 0
+    }
+  } catch (err) {
+    if (!axios.isCancel(err) && !searchAbortController?.signal.aborted) {
+      ElMessage.error('Failed to load data')
+      state.tableData = []
+      state.total = 0
+    }
+  } finally {
+    if (!searchAbortController?.signal.aborted) {
+      state.loading = false
+    }
+  }
 }
-// 分页 请求接口
-const handleCurrentChange = () => {
-  state.loading = true
-  let tableData: any = []
-  const queryData = filtersStore.getQueryData
-  setTimeout(() => {
-    $api
-      .getMoreFiltersTableData({
-        term: searchVal.value ? searchVal.value : '',
-        cp: state.currentPage,
-        ps: state.pageSize,
-        rc: state.total,
-        search_field: props.title,
-        search_mode: 'booking',
-        ...queryData
-      })
-      .then((res: any) => {
-        if (res.code == 200) {
-          tableData = res.data.searchData
-          state.loading = false
-          tableData = res.data.searchData.filter((p: any) => {
-            return (
-              p.country.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.city.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.uncode.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1
-            )
-          })
-          state.tableData = tableData
-          state.total = Number(res.data.rc)
-          state.loading = false
-        }
-      })
-  }, 800)
+
+// —————— 防抖搜索 ——————
+const debouncedSearch = debounce((query: string) => {
+  state.currentPage = 1
+  fetchData(query, 1, false)
+}, 300)
+
+// —————— 事件处理 ——————
+const handleSearchInput = () => {
+  const val = searchVal.value
+  if (val.trim() === '') {
+    state.tableData = []
+    state.total = 0
+    state.currentPage = 1
+  } else {
+    debouncedSearch(val)
+  }
+}
+
+const handlePageChange = () => {
+  fetchData(searchVal.value, state.currentPage, true)
+}
+
+const handleRowClick = (row: any) => {
+  const keyVal = row[props.needKey]
+  if (innerTags.value.includes(keyVal)) {
+    ElMessage.success('Cannot add duplicate cities.')
+    return
+  }
+
+  innerTags.value.push(keyVal)
+  emit('input', innerTags.value, props.title)
+
+  // 关闭弹窗并重置搜索
+  state.poverShow = false
+  searchVal.value = ''
+  state.tableData = []
+  state.currentPage = 1
+  state.total = 0
 }
-// 删除
+
 const remove = (idx: number) => {
   innerTags.value.splice(idx, 1)
   emit('input', innerTags.value, props.title)
 }
 
-const onInput = () => {
-  tagInputRef.value.focus()
+// 👇 仅在点击 reference 区域时打开弹窗
+const openPopover = () => {
+  if (props.disabled) return
+  state.poverShow = true
+  nextTick(() => {
+    tagInputRef.value?.focus()
+  })
 }
 
-// 在切换title时,可以用来清空搜索框
 const clearSearchInput = () => {
   searchVal.value = ''
   state.tableData = []
   state.currentPage = 1
   state.total = 0
+  if (searchAbortController) {
+    searchAbortController.abort()
+    searchAbortController = null
+  }
 }
-defineExpose({
-  clearSearchInput
+
+defineExpose({ clearSearchInput })
+
+onUnmounted(() => {
+  if (searchAbortController) {
+    searchAbortController.abort()
+  }
 })
 </script>
+
 <template>
   <div>
+    <!-- 手动控制弹窗 -->
     <el-popover
       v-model:visible="state.poverShow"
       placement="bottom-start"
       :teleported="false"
       :width="props.poverWidth"
-      trigger="click"
+      trigger="manual"
     >
       <template #reference>
+        <!-- 点击此区域才打开弹窗 -->
         <div
           class="el-input el-input--suffix el-tooltip__trigger"
-          @click="disabled ? null : onInput"
+          @click="openPopover"
           :class="{ is_error: props.isError }"
         >
-          <div class="el-input__wrapper" :class="{ 'is-disabled': disabled }">
+          <div class="el-input__wrapper" :class="{ 'is-disabled': props.disabled }">
             <el-space :class="[innerTags.length ? 'custom-el-spaceno' : 'custom-el-space']">
               <template v-for="(item, idx) in innerTags" :key="item">
                 <template v-if="idx <= 2">
@@ -197,7 +195,7 @@ defineExpose({
                   </el-tag>
                 </template>
               </template>
-              <template v-if="innerTags.length && innerTags.length > 3">
+              <template v-if="innerTags.length > 3">
                 <el-popover placement="bottom" trigger="hover">
                   <template #reference>
                     <el-tag type="info" size="small" round :disable-transitions="false">
@@ -228,19 +226,20 @@ defineExpose({
                 </el-popover>
               </template>
               <input
-                :value="searchVal"
+                v-model="searchVal"
                 ref="tagInputRef"
                 :placeholder="innerTags.length ? '' : 'Please input country/city/uncode'"
                 class="el-input__inner"
-                :disabled="disabled"
+                :disabled="props.disabled"
                 type="text"
-                @input="handleSearch"
-                @click="handleSearch"
+                @input="handleSearchInput"
+                @keydown.enter.stop.prevent
+                @focus="debouncedSearch('')"
               />
             </el-space>
-            <div class="el-input__suffix">
-              <div class="el-input__suffix-inner" v-if="!disabled">
-                <el-icon :class="state.poverShow ? 'reverse' : ''">
+            <div class="el-input__suffix" v-if="!props.disabled">
+              <div class="el-input__suffix-inner">
+                <el-icon :class="{ reverse: state.poverShow }">
                   <CaretBottom />
                 </el-icon>
               </div>
@@ -248,6 +247,8 @@ defineExpose({
           </div>
         </div>
       </template>
+
+      <!-- 数据表格 -->
       <el-table
         :data="state.tableData"
         border
@@ -255,11 +256,14 @@ defineExpose({
         v-loading="state.loading"
         @row-click="handleRowClick"
         header-row-class-name="cus-header"
+        style="width: 100%"
       >
         <el-table-column property="country" label="Country" width="75" />
         <el-table-column property="city" label="City" />
         <el-table-column property="uncode" label="Uncode" width="80" />
       </el-table>
+
+      <!-- 分页 -->
       <div class="pagination">
         <span>Total {{ formatNumber(state.total) }}</span>
         <el-pagination
@@ -269,8 +273,8 @@ defineExpose({
           layout="prev, pager, next"
           :pager-count="5"
           :total="state.total"
-          @current-change="handleSearch('pageChange')"
-          @size-change="handleSearch('pageChange')"
+          @current-change="handlePageChange"
+          @size-change="handlePageChange"
         />
       </div>
     </el-popover>
@@ -306,22 +310,16 @@ defineExpose({
   background-color: var(--color-table-header-bg) !important;
 }
 
-:deep(.el-table__row) {
-  td {
-    cursor: pointer;
-  }
+:deep(.el-table__row) td {
+  cursor: pointer;
 }
 
-:deep(.el-table__row:not(.current-row):hover) {
-  td {
-    background-color: var(--color-btn-default-bg-hover) !important;
-  }
+:deep(.el-table__row:not(.current-row):hover) td {
+  background-color: var(--color-btn-default-bg-hover) !important;
 }
 
-:deep(.current-row) {
-  td {
-    background-color: #ffe3cd !important;
-  }
+:deep(.current-row) td {
+  background-color: #ffe3cd !important;
 }
 
 .pagination {

+ 127 - 57
src/components/selectAutoSelect/src/selectAutoSelect.vue

@@ -1,9 +1,10 @@
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { ref, watch, onUnmounted } from 'vue'
 import IconDropDown from '@/components/IconDropDown'
-import { cloneDeep } from 'lodash'
+import { cloneDeep, debounce } from 'lodash'
 import { useFiltersStore } from '@/stores/modules/filtersList'
 import { useRoute } from 'vue-router'
+import axios from 'axios'
 
 const route = useRoute()
 const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
@@ -15,87 +16,159 @@ interface Props {
   data: Array<any>
 }
 
-const loading = ref(false)
 const props = withDefaults(defineProps<Props>(), {})
-// 搜索得到的值列表
-const detailsData = ref([])
+
+// 每个 item 的独立状态:loading、options、abortController
+const itemStates = ref(
+  new Map<
+    string,
+    {
+      loading: boolean
+      options: Array<{ value: string; label: string; checked: boolean }>
+      controller: AbortController | null
+    }
+  >()
+)
+
 const pageData = ref(cloneDeep(props.data))
+
 watch(
   () => props.data,
   (newVal) => {
     pageData.value = cloneDeep(newVal)
+    // 初始化每个 item 的状态(如果不存在)
+    newVal.forEach((item) => {
+      if (!itemStates.value.has(item.title)) {
+        itemStates.value.set(item.title, {
+          loading: false,
+          options: [],
+          controller: null
+        })
+      }
+    })
   },
-  {
-    deep: true
-  }
+  { deep: true, immediate: true }
 )
 
 const originnalTypeOptions = ref(props.typeOptions)
 
 // 计算排除其他已选项后的可用选项
 const getAvailableOptions = (curTitle: string) => {
-  // 获取其他下拉已选的值(排除自己)
   const otherTypeOptions = pageData.value
-    .filter((type) => {
-      return type.title !== curTitle
-    })
-    .map((type) => {
-      return type.title
-    })
-  return originnalTypeOptions.value.filter((type) => {
-    return !otherTypeOptions.includes(type.title)
-  })
+    .filter((type) => type.title !== curTitle)
+    .map((type) => type.title)
+  return originnalTypeOptions.value.filter((type) => !otherTypeOptions.includes(type.title))
 }
+
 const emit = defineEmits(['deleteItem', 'changeTitle', 'changeValue'])
 
 const changeTitle = (title: string, item: any) => {
-  // item.value = []
   emit('changeTitle', item, title)
 }
+
 const changeValue = (value: string[], item: any) => {
-  // item.value = value
   emit('changeValue', item, value)
 }
 
 const deleteItem = (title: string) => {
+  // 清理该 item 的状态和 abort pending request
+  const state = itemStates.value.get(title)
+  if (state?.controller) {
+    state.controller.abort()
+  }
+  itemStates.value.delete(title)
   emit('deleteItem', title)
 }
 
-const visibleChange = (val: boolean) => {
+const visibleChange = (val: boolean, title: string) => {
   if (!val) {
-    detailsData.value = []
+    const state = itemStates.value.get(title)
+    if (state) {
+      state.options = []
+    }
   }
 }
+
+// 创建带防抖 + 竞态处理的远程搜索函数
 const createQueryHandler = (title: string, curValue: string[]) => {
-  return (query: string) => {
-    const curType = originnalTypeOptions.value.find((type) => {
-      return type.title === title
-    })
-    if (query) {
-      loading.value = true
-      const queryData = filtersStore.getQueryData
-      setTimeout(() => {
-        $api
-          .getMoreFiltersData({
-            term: query,
-            type: curType.type,
-            search_field: title,
-            search_mode: searchMode,
-            ...queryData
-          })
-          .then((res: any) => {
-            if (res.code == 200) {
-              loading.value = false
-              detailsData.value = res.data.map((item: any) => {
-                return { value: item, label: item, checked: curValue?.includes(item) }
-              })
-            }
-          })
-      }, 200)
+  const debouncedSearch = debounce((query: string) => {
+    const state = itemStates.value.get(title)
+    if (!state) return
+
+    // 取消上一次请求
+    if (state.controller) {
+      state.controller.abort()
     }
-  }
+
+    if (!query.trim()) {
+      state.options = []
+      state.loading = false
+      return
+    }
+
+    const newController = new AbortController()
+    state.controller = newController
+    state.loading = true
+
+    const curType = originnalTypeOptions.value.find((type) => type.title === title)
+    if (!curType) {
+      state.loading = false
+      return
+    }
+
+    const queryData = filtersStore.getQueryData
+    $api
+      .getMoreFiltersData(
+        {
+          term: query,
+          type: curType.type,
+          search_field: title,
+          search_mode: searchMode,
+          ...queryData
+        },
+        { signal: newController.signal }
+      )
+      .then((res: any) => {
+        if (!newController.signal.aborted && res?.code === 200) {
+          state.options = (res.data || []).map((item: string) => ({
+            value: item,
+            label: item,
+            checked: curValue?.includes(item)
+          }))
+        }
+      })
+      .catch((err) => {
+        if (!axios.isCancel(err) && !newController.signal.aborted) {
+          // ElMessage.error('Failed to load options')
+        }
+        if (itemStates.value.has(title)) {
+          itemStates.value.get(title)!.options = []
+        }
+      })
+      .finally(() => {
+        // 仅当这是最新 controller 时才关闭 loading
+        if (
+          itemStates.value.has(title) &&
+          itemStates.value.get(title)?.controller === newController
+        ) {
+          state.loading = false
+        }
+      })
+  }, 200)
+
+  return debouncedSearch
 }
+
+// 组件卸载时取消所有 pending 请求
+onUnmounted(() => {
+  itemStates.value.forEach((state) => {
+    if (state.controller) {
+      state.controller.abort()
+    }
+  })
+})
 </script>
+
 <template>
   <div class="addType" v-for="item in pageData" :key="item.id">
     <div>
@@ -118,8 +191,7 @@ const createQueryHandler = (title: string, curValue: string[]) => {
           :key="type.title"
           :label="type.title"
           :value="type.title"
-        >
-        </el-option>
+        />
       </el-select>
     </div>
     <div style="margin-top: 16px">
@@ -129,22 +201,20 @@ const createQueryHandler = (title: string, curValue: string[]) => {
         multiple
         filterable
         remote
-        :class="{
-          is_error: item.value != '' && item.title
-        }"
+        :class="{ is_error: item.value.length > 0 && item.title }"
         reserve-keyword
         :placeholder="props.placeholder"
         collapse-tags
-        :disabled="item.title ? false : true"
+        :disabled="!item.title"
         collapse-tags-tooltip
         :max-collapse-tags="3"
         :remote-method="createQueryHandler(item.title, item.value)"
-        :loading="loading"
+        :loading="itemStates.get(item.title)?.loading ?? false"
         @change="changeValue($event, item)"
-        @visible-change="visibleChange"
+        @visible-change="(val) => visibleChange(val, item.title)"
       >
         <el-option
-          v-for="infoItem in detailsData"
+          v-for="infoItem in itemStates.get(item.title)?.options || []"
           :key="infoItem.label"
           :label="infoItem.label"
           :value="infoItem.label"

+ 4 - 4
src/utils/tools.ts

@@ -8,16 +8,16 @@ const formatString = computed(() => {
   return userStore.dateFormat || 'MM/DD/YYYY'
 })
 
-export const formatTimezone = (time: string, timezone?: string, is12HourClock?: boolean) => {
+export const formatTimezone = (time: string, timezone?: string, is12HourClock?: boolean, originFormat?: string) => {
   if (!time) return '--'
   let formattedTime = ''
   if (time.length > 12) {
     if (is12HourClock) {
       // 如果是12小时制,使用12小时制格式化
-      formattedTime = moment(time).format(`${formatString.value} hh:mm A`)
+      formattedTime = moment(time, originFormat).format(`${formatString.value} hh:mm A`)
     } else {
       // 如果是24小时制,使用24小时制格式化
-      formattedTime = moment(time).format(`${formatString.value} HH:mm`)
+      formattedTime = moment(time, originFormat).format(`${formatString.value} HH:mm`)
     }
     if (!timezone) {
       return formattedTime
@@ -29,7 +29,7 @@ export const formatTimezone = (time: string, timezone?: string, is12HourClock?:
     utcOffset = `(UTC${timeZoneOffset.slice(0, 3)})`
     return `${formattedTime} ${utcOffset}`
   } else {
-    formattedTime = moment(time).format(formatString.value)
+    formattedTime = moment(time, originFormat).format(formatString.value)
     return formattedTime
   }
 }

+ 0 - 1
src/views/Dashboard/src/DashboardView.new.vue

@@ -45,7 +45,6 @@ const customerInfo = ref({
   customerType: []
 })
 const changeCustomerData = (val: string[], type: string) => {
-  console.log(val, type)
   customerInfo.value[type] = val
 }
 

+ 15 - 6
src/views/Dashboard/src/components/CustomerFilter.vue

@@ -21,14 +21,10 @@ const customerInfo = ref({
   customerCode: [] as string[],
   customerType: [] as string[]
 })
-// const customerCode = ref<string[]>([])
-// const customerType = ref<string[]>([])
+
 watch(
   () => props.data,
   (newValue) => {
-    // customerCode.value = cloneDeep(newValue.customerCode) || []
-    // customerType.value = cloneDeep(newValue.customerType) || []
-    console.log(newValue, 'newValue')
     customerInfo.value = cloneDeep(newValue) || {
       customerCode: [],
       customerType: []
@@ -156,6 +152,10 @@ const typeOptions = ref([
     </el-select>
     <el-select
       placeholder="Customer Type"
+      multiple
+      :max-collapse-tags="1"
+      collapse-tags
+      collapse-tags-tooltip
       :model-value="customerInfo.customerType"
       @change="changeData($event, 'customerType')"
       style="width: 240px; margin-left: 8px"
@@ -165,7 +165,16 @@ const typeOptions = ref([
         :key="item.value"
         :label="item.label"
         :value="item.value"
-      ></el-option>
+      >
+        <div class="select-option">
+          <el-checkbox
+            :model-value="customerInfo.customerType.includes(item.value)"
+            style="flex: 1"
+          >
+            <span style="display: inline-block; width: 220px">{{ item.label }}</span>
+          </el-checkbox>
+        </div>
+      </el-option>
     </el-select>
     <el-button class="el-button--default">Search</el-button>
   </div>

+ 13 - 1
src/views/Report/src/components/ReportDetail/src/components/FieldsTable.vue

@@ -2,7 +2,7 @@
 import { ref, onMounted } from 'vue'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
-import { formatNumber } from '@/utils/tools'
+import { formatNumber, formatTimezone } from '@/utils/tools'
 import dayjs from 'dayjs'
 import { autoWidth } from '@/utils/table'
 import { useRoute } from 'vue-router'
@@ -85,6 +85,18 @@ const handleColumns = (columns: any) => {
         slots: { default: 'status' }
       }
     }
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue, '', '', 'MM/DD/YYYY HH:mm')
+      }
+    } else if (item.formatter === 'number') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
+      }
+    }
     return curColumn
   })
   return newColumns