Преглед изворни кода

fix(TableFilterForm): 补充提交TableFilterForm及其相关组件

fxs пре 6 месеци
родитељ
комит
ced0d6cf0e

+ 7 - 0
src/assets/form.css

@@ -0,0 +1,7 @@
+.el-form-item .inputW120 {
+  width: 120px;
+}
+
+.el-form-item .inputW180 {
+  width: 180px;
+}

+ 43 - 0
src/components/form/TableFilterForm/FilterInput.vue

@@ -0,0 +1,43 @@
+<script setup lang="ts">
+import type { QueryInfo } from '@/types/table'
+
+interface FilterInput {
+  itemInfo: QueryInfo
+}
+
+defineProps<FilterInput>()
+
+const model = defineModel<any>()
+
+const emits = defineEmits(['inputEnterQuery'])
+
+/**
+ * 按回车查询
+ */
+const enterQuery = () => {
+  emits('inputEnterQuery', model.value)
+}
+</script>
+
+<template>
+  <!-- 所有的input查询框 -->
+  <el-form-item @keyup.enter="enterQuery" v-bind="$attrs" class="filterItem">
+    <el-input
+      v-if="itemInfo.valueType !== 'int'"
+      clearable
+      :placeholder="itemInfo.placeholder"
+      v-model="model"
+      class="inputW180"
+    />
+    <el-input
+      v-else
+      clearable
+      :placeholder="itemInfo.placeholder"
+      type="number"
+      v-model.number="model"
+      class="inputW180"
+    />
+  </el-form-item>
+</template>
+
+<style scoped></style>

+ 150 - 0
src/components/form/TableFilterForm/FilterSelect.vue

@@ -0,0 +1,150 @@
+<script setup lang="ts">
+import type { QueryInfo, SelectInfo } from '@/types/table'
+import { FilterType } from '@/types/table'
+import { ref } from 'vue'
+
+interface FilterSelectProps {
+  item: QueryInfo
+}
+
+interface SelectAllItem {
+  isCheckAll: boolean // 是否选中了所有
+  isIndeterminate: boolean // 是否是中间状态
+}
+
+const props = defineProps<FilterSelectProps>()
+
+const model = defineModel<any>()
+
+// 多选状态
+const selectState = ref<SelectAllItem>({
+  isCheckAll: true,
+  isIndeterminate: false
+})
+
+/**
+ * 判断是否符合多选的条件
+ *
+ * -必须为多选类型
+ * -传入的otherOption必须伪数组
+ * -绑定值必须为数组
+ *
+ * 以上条件必须同时满足,否则返回false
+ *
+ * @param item 查询信息
+ */
+const isMultipleSelect = (): boolean => {
+  const item = props.item
+  const options = item.otherOption
+  let result = true
+  if (item.type !== FilterType.MULTI_SELECT) {
+    result = false
+    console.error('非多选类型')
+  }
+  if (!(options && Array.isArray(options))) {
+    result = false
+    console.error('选项为空或非数组')
+  }
+  if (!(model.value && Array.isArray(model.value))) {
+    result = false
+    console.error('表单数据为空或非数组')
+  }
+  return result
+}
+
+/**
+ * 处理选项改变
+ * 单选情况:不处理
+ * 多选情况:改变上方多选框状态
+ *
+ */
+const selectChange = () => {
+  const item = props.item
+  // 不符合多选信息的不处理
+  if (!isMultipleSelect()) return
+  const options = item.otherOption as Array<SelectInfo>
+  const val = model.value
+  // 目前的选中值如果跟选项个数一样就视为全选
+  //    更精准就需要去判断选中值是否跟选项的值全相同
+  //    可使用set去重后比较实现,暂时不需要
+  const isSelectedAll = val.length === options.length
+
+  selectState.value.isCheckAll = isSelectedAll
+
+  // 对于全不选的情况需要特殊处理一下
+  // 全不选需要中间态取消,否则显示中间态
+  if (val.length === 0) {
+    selectState.value.isIndeterminate = false
+  } else {
+    selectState.value.isIndeterminate = !isSelectedAll
+  }
+}
+
+/**
+ * 根据全选情况改变选择情况
+ *
+ */
+const changeCheckAll = () => {
+  const item = props.item
+  if (!isMultipleSelect()) return
+  const options = item.otherOption as Array<SelectInfo>
+  const activeSelectInfo = selectState.value
+  const isCheckAll = activeSelectInfo.isCheckAll
+  const formList = model.value
+
+  // 不论是全选还是全不选,都不会是中间状态
+  activeSelectInfo.isIndeterminate = false
+  if (isCheckAll) {
+    // 是全选就需要将所有选项都选中
+    const valList = options.map((option) => {
+      return option.value
+    })
+    formList.splice(0, formList.length, ...valList)
+  } else {
+    // 否则清空所有选项
+    formList.splice(0, formList.length)
+  }
+}
+
+/**
+ * 处理点击多选按钮事件。
+ *
+ * @param item 查询条件信息
+ */
+const handleCheckAll = () => {
+  changeCheckAll()
+}
+</script>
+
+<template>
+  <div class="filterSelectContainer">
+    <el-select
+      :empty-values="[undefined, null]"
+      v-model="model"
+      v-bind="$attrs"
+      collapse-tags
+      collapse-tags-tooltip
+      style="width: 120px"
+      :multiple="item.type === FilterType.MULTI_SELECT"
+      @change="selectChange"
+    >
+      <template #header v-if="item.type === FilterType.MULTI_SELECT">
+        <el-checkbox
+          v-model="selectState.isCheckAll"
+          :indeterminate="selectState.isIndeterminate"
+          @change="handleCheckAll"
+        >
+          全部
+        </el-checkbox>
+      </template>
+      <el-option
+        v-for="val in item.otherOption"
+        :key="val.name"
+        :label="val.cnName"
+        :value="val.value"
+      />
+    </el-select>
+  </div>
+</template>
+
+<style scoped></style>

+ 51 - 0
src/components/form/TableFilterForm/FilterSelectLabel.vue

@@ -0,0 +1,51 @@
+<script setup lang="ts">
+interface FilterSelctLable {
+  label: string
+  supplement?: string
+  popoverInfoClass?: string
+  popoverInfoStyle?: string
+}
+
+defineProps<FilterSelctLable>()
+</script>
+
+<template>
+  <div class="filterSelectLabelContainer">
+    <span class="selectLabelContainer">
+      <span class="selectLabel"> {{ label }}</span>
+      <span class="selectSupplement" v-if="supplement">
+        <el-popover placement="top" trigger="hover">
+          <p
+            :class="popoverInfoClass ? popoverInfoClass : 'filterSelectPopoverInfo'"
+            :style="popoverInfoStyle ? popoverInfoStyle : ''"
+          >
+            {{ supplement }}
+          </p>
+          <template #reference>
+            <el-icon><QuestionFilled /></el-icon>
+          </template>
+        </el-popover>
+      </span>
+    </span>
+  </div>
+</template>
+
+<style scoped>
+.filterSelectPopoverInfo {
+  width: auto;
+  padding: 0 5px;
+  display: inline-block;
+  text-align: center;
+  color: black;
+}
+
+.selectLabelContainer,
+.selectSupplement {
+  display: flex;
+  align-items: center;
+}
+
+.selectSupplement {
+  margin-left: 5px;
+}
+</style>

+ 307 - 0
src/components/form/TableFilterForm/TableFilterForm.vue

@@ -0,0 +1,307 @@
+<script setup lang="ts">
+import { onMounted, ref } from 'vue'
+import FilterInput from './FilterInput.vue'
+import FilterSelectLabel from './FilterSelectLabel.vue'
+import FilterSelect from './FilterSelect.vue'
+import { FilterType, type QueryInfo } from '@/types/table'
+import { Filter } from '@element-plus/icons-vue'
+import { useCustomFilter } from '@/hooks/useCustomFilter'
+import CustomFilter from '../CustomFilter.vue'
+import { throttleFunc } from '@/utils/common'
+type CustomFilterRef = InstanceType<typeof CustomFilter>
+
+interface TableFilterFormData {
+  [key: string]: any
+}
+
+interface TableFilterFormProps {
+  queryInfo: Array<QueryInfo>
+}
+
+interface TableFilterItems {
+  [key: string]: QueryInfo[]
+  input: QueryInfo[]
+  select: QueryInfo[]
+  date: QueryInfo[]
+  custom: QueryInfo[]
+}
+
+const customFilterRef = ref<CustomFilterRef>()
+
+const {
+  customFilterDialog,
+  customFilterInfo,
+  customFilterList,
+  activeCustomFilterKey,
+  initCustomFilterInfo,
+  resetCustomFilterList,
+  openCustomFilter,
+  openedInit,
+  confirmCustomFilter
+} = useCustomFilter(customFilterRef)
+
+const props = defineProps<TableFilterFormProps>()
+
+const emits = defineEmits(['query'])
+
+// 节流时间
+const throttleTime = 1000
+
+// 各类型的查询选项集合
+const filterItems = ref<TableFilterItems>({
+  input: [],
+  select: [],
+  date: [],
+  custom: []
+})
+
+// 双向绑定一下
+const queryFormData = defineModel<TableFilterFormData>('queryFormData', {
+  required: true
+})
+
+// 备份表单的默认值信息
+const backupFormDefault: TableFilterFormData = {}
+
+/**
+ * 初始化查询选项集合
+ *
+ * 根据type值直接跟filterItems的键对应
+ *
+ */
+const initFilterItems = () => {
+  props.queryInfo.forEach((item) => {
+    let key = item.type
+    // 多选和单选公用select值
+    if (item.type === FilterType.MULTI_SELECT) {
+      key = FilterType.SELECT
+    }
+    filterItems.value[key].push(item)
+  })
+}
+
+/**
+ * 初始化查询框的数据,同时备份默认值信息,用于重置表单
+ *
+ */
+const initFormData = () => {
+  props.queryInfo?.map((item: QueryInfo) => {
+    queryFormData.value[item.name] = item.default
+    backupFormDefault[item.name] = item.default
+  })
+}
+
+/**
+ * 查询数据,触发事件,交由外部处理
+ *
+ */
+const queryData = () => {
+  emits('query')
+}
+
+/**
+ * 节流包装查询功能
+ */
+const throttleQuery = throttleFunc(queryData, throttleTime)
+
+/**
+ * 重置自定义表单数据
+ */
+const resetCustomFilterInfo = () => {
+  initCustomFilterInfo(filterItems.value.custom) // 重置自定义表单数据
+}
+
+/**
+ * 重置查询表单数据
+ */
+const resetFormData = () => {
+  for (const [k, v] of Object.entries(backupFormDefault)) {
+    // 需要对undefined情况处理一下,否则会报错
+    // JSON.stringify()第二个参数可以用来处理undefined的情况,第一个参数设置为_可以避免ts检查
+    queryFormData.value[k] = JSON.parse(
+      JSON.stringify(v, (_, val) => (typeof val === 'undefined' ? '' : val))
+    )
+  }
+  resetCustomFilterInfo()
+}
+
+/**
+ * 重置查询参数且查询
+ *
+ */
+const resetQuery = () => {
+  resetFormData() // 重置查询参数
+  queryData() // 重新查询数据
+}
+
+/**
+ * 包装重置查询功能
+ */
+const throttleResetQuery = throttleFunc(resetQuery, throttleTime)
+
+/**
+ * 接收重置表单命令
+ */
+const resetCustomFilterData = () => {
+  resetCustomFilterList(queryFormData)
+}
+
+onMounted(() => {
+  initFilterItems()
+  initFormData()
+  resetCustomFilterInfo()
+})
+
+defineExpose({
+  throttleResetQuery,
+  resetFormData
+})
+</script>
+
+<template>
+  <div class="tableFilterFormContainer">
+    <el-form
+      :inline="true"
+      ref="queryFormRef"
+      :model="queryFormData"
+      class="queryForm"
+      :label-position="'right'"
+    >
+      <template v-for="item in filterItems.input" :key="item.name">
+        <!-- 所有的input查询框 -->
+
+        <FilterInput
+          :item-info="item"
+          :label="item.label"
+          v-model="queryFormData[item.name]"
+          @input-enter-query="throttleQuery"
+        ></FilterInput>
+      </template>
+
+      <!-- 所有选择框 -->
+      <el-form-item
+        :label="item.label"
+        v-for="item in filterItems.select"
+        :key="item.name"
+        class="filterItem"
+      >
+        <template #label="{ label }">
+          <FilterSelectLabel :label="label" :supplement="item.supplementInfo"></FilterSelectLabel>
+        </template>
+        <FilterSelect :item="item" v-model="queryFormData[item.name]"></FilterSelect>
+      </el-form-item>
+
+      <!-- 所有日期选择框 -->
+      <el-form-item
+        :label="item.label"
+        v-for="item in filterItems.date"
+        :key="item.name"
+        class="filterItem"
+      >
+        <el-date-picker
+          v-model="queryFormData[item.name]"
+          type="date"
+          :value-format="item.otherOption.valueFormat"
+          :placeholder="item.placeholder"
+          clearable
+        />
+      </el-form-item>
+
+      <!-- 所有自定义筛选条件 -->
+      <el-form-item
+        :label="item.label"
+        v-for="item in filterItems.custom"
+        :key="item.name"
+        class="filterItem"
+      >
+        <el-button plain size="default" :icon="Filter" @click="openCustomFilter(item)"
+          >筛选
+        </el-button>
+      </el-form-item>
+    </el-form>
+    <!-- 分割线 -->
+    <div class="queryBox">
+      <el-divider class="queryPartition" content-position="center" direction="vertical" />
+      <div class="queryBtnBox">
+        <el-button class="queryBtn" color="#165dff" @click="throttleQuery">
+          <el-icon>
+            <Search />
+          </el-icon>
+          查询
+        </el-button>
+        <el-button class="refreshBtn" color="#f2f3f5" @click="throttleResetQuery">
+          <el-icon>
+            <RefreshRight />
+          </el-icon>
+          重置
+        </el-button>
+      </div>
+    </div>
+    <el-dialog v-model="customFilterDialog" title="自定义筛选条件" width="800" @open="openedInit">
+      <CustomFilter
+        :custom-filter-list="customFilterList[activeCustomFilterKey]"
+        :filter="customFilterInfo[activeCustomFilterKey]"
+        @reset-filter-list="resetCustomFilterData"
+        ref="customFilterRef"
+      ></CustomFilter>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button
+            type="primary"
+            @click="confirmCustomFilter(queryFormData)"
+            style="margin-right: 20px"
+          >
+            确定
+          </el-button>
+          <el-button @click="customFilterDialog = false">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.tableFilterFormContainer {
+  width: 98%;
+  box-sizing: border-box;
+
+  margin: 10px auto;
+  display: flex;
+  padding: 0 24px;
+}
+
+.queryForm {
+  width: 90%;
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.filterItem {
+  width: 20%;
+  display: flex;
+  align-items: center;
+}
+
+.queryBox {
+  width: 10%;
+  display: flex;
+  justify-content: space-around;
+}
+
+.queryPartition {
+  height: 90%;
+}
+
+.queryBtnBox {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.refreshBtn {
+  margin-top: 10%;
+  margin-bottom: 10%;
+}
+</style>