浏览代码

修复表格大小变化时滚动条位置错误的问题;更新自定义指标方案保存功能,可快捷切换方案;更新方案保存数据结构;

fxs 7 月之前
父节点
当前提交
228dea8444

+ 1 - 0
components.d.ts

@@ -46,6 +46,7 @@ declare module 'vue' {
     MyInput: typeof import('./src/components/form/MyInput.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SaveForm: typeof import('./src/components/dialog/saveForm.vue')['default']
     Table: typeof import('./src/components/table/Table.vue')['default']
     TableQueryForm: typeof import('./src/components/table/TableQueryForm.vue')['default']
     TimeLineChart: typeof import('./src/components/echarts/TimeLineChart.vue')['default']

+ 2 - 0
src/App.vue

@@ -1,5 +1,7 @@
 <script setup lang="ts">
 import { zhCn } from 'element-plus/es/locales.mjs'
+import { computed, reactive, ref } from 'vue'
+const customIndicatorSchemeVisble = ref<boolean>(false)
 </script>
 
 <template>

+ 176 - 202
src/components/dialog/customIndicatorDialog.vue

@@ -1,22 +1,47 @@
 <script setup lang="ts">
 import type { BaseFieldItem, TableFields } from '@/types/Tables/table'
+import type { FormInstance, FormRules } from 'element-plus'
+import type { AllAdmgeTtFields } from '@/types/Tables/tableData/ttAd'
+import type {
+  DialogProps,
+  SaveForm,
+} from '@/types/Tables/customIndicatorDialog'
 
 import { ref, reactive, watch } from 'vue'
 import { useTable } from '@/hooks/useTable'
 import { computed } from 'vue'
+import { useCustomIndicatorDialog } from '@/hooks/useCustomIndicatorDialog'
+import { useDialogDrag } from '@/hooks/useDialogDrag'
 
 const { isFixedField } = useTable()
+const {
+  initSortedTableFieldsInfo,
+  validCustomName,
+  updateFixedIndicator,
+  updateUnfixedIndicator,
+  getActivedIndicator,
+  updateSortedTableFieldsInfo,
+} = useCustomIndicatorDialog()
+
+const props = withDefaults(defineProps<DialogProps>(), {
+  SaveFormData: () => {
+    return {
+      isSaveCustom: false,
+      saveCustomName: '',
+    }
+  },
+})
 
-interface DialogProps {
-  indicatorFields: Array<BaseFieldItem<TableFields>>
-  defaultActiveNav: string
-  tableFieldsInfo: Array<TableFields>
-  fixedFields: Array<string>
-}
+const emits = defineEmits(['updateFields'])
 
-const props = withDefaults(defineProps<DialogProps>(), {})
+const saveForm = reactive<SaveForm>(props.SaveFormData)
 
-const emits = defineEmits(['updateFields'])
+const saveFormRules = reactive<FormRules<SaveForm>>({
+  saveCustomName: [{ validator: validCustomName, trigger: 'blur' }],
+})
+
+// 保存自定义指标表单实例
+const saveFormRef = ref<FormInstance>()
 
 // 被排序过后的表格字段信息
 const sortedTableFieldsInfo = reactive<Array<TableFields>>([])
@@ -27,65 +52,33 @@ const newTableFieldsInfo = reactive<Array<TableFields>>([])
 // 自定义指标的展示状态
 const customIndicatorVisible = ref<boolean>(false)
 
+// 每次展开的时候,右侧栏目的排序需要跟表格字段的顺序同步
+// 不然会导致表格字段和右侧的栏目的顺序不一致
+// 关闭后需要重置这个状态,每次打开都要排一次
+const isFirstShow = ref<boolean>(true)
+
 // 自定义指标搜索值
 const indicatorSearch = ref<string>('')
 
 // 自定义指标左侧导航选中值
 const indicatorNavActive = ref<string>(props.defaultActiveNav)
 
-// 自定义指标中需要固定的指标
-const fixedIndicator = computed<TableFields[]>(() => {
-  let result: TableFields[] = []
-  // 对于固定的指标,为了防止传入的数据有误,所有直接state设置为true
-  props.indicatorFields.forEach(item => {
-    result.push(
-      ...item.children.filter(item => {
-        if (item.fixed) item.state = true
-        return item.fixed
-      }),
-    )
-  })
-  console.log('asdf')
-  console.log(JSON.parse(JSON.stringify(result)))
-  return result
-})
-
-// 没有被固定的指标
-// 这里去更新state是为了让表格的字段能够正确同步
-const unFixedIndicator = computed<TableFields[]>(() => {
-  let result: TableFields[] = []
-  // 拿到所有的子元素
-  let allChilds = props.indicatorFields.flatMap(
-    item => item.children,
-  ) as Array<TableFields>
-  allChilds.map(item => (item.state = false))
-
-  // 拿到所有的选中值
-  let allSelected = props.indicatorFields.flatMap(
-    item => item.value,
-  ) as Array<string>
-
-  // 之所以要这样去筛选,是为了保证顺序,
-  // 如果直接通过filter去找存在于allSelected中的元素,
-  // 那么当一个在前面的选项首先取消选择,然后又重新选中的时候,这个选项又会直接出现在他原来的位置,而不是末尾
-  allSelected.forEach(item => {
-    let child = allChilds.find(
-      child => !child.fixed && child.name === item,
-    ) as TableFields
-    if (child) {
-      child.state = true
-      result.push(child)
-    }
-  })
-  console.log(result)
-  return result
-})
-
 // 拖拽的容器
-const dragContentRef = ref<HTMLElement>()
+const dragContentRef = ref<HTMLElement | null>(null)
 
 // 当前正在被拖拽的元素
-const dragingRef = ref<HTMLElement>()
+const dragingRef = ref<HTMLElement | null>(null)
+
+// 所有需要固定的指标
+const fixedIndicator = reactive<TableFields[]>([])
+
+// 所有需要不固定的指标
+const unFixedIndicator = reactive<TableFields[]>([])
+
+const { dragStart, dragEnter, dragOver } = useDialogDrag(
+  dragingRef,
+  dragContentRef,
+)
 
 // 当默认值变化的时候,要更新一下,这里主要用在初始化的时候,数据可能还没有获取到
 watch(
@@ -98,23 +91,44 @@ watch(
   },
 )
 
-const indicatorOpen = () => {}
-
-const indicatorClose = () => {
-  // 重置到第一个锚点
-  indicatorNavActive.value = props.indicatorFields[0]?.name
+/**
+ * @description: 多选框值改变,去更新不固定字段的展示
+ * @return {*}
+ */
+const changeIndicatorChecked = () => {
+  updateUnfixedIndicator(
+    props.indicatorFields,
+    isFirstShow,
+    props.tableFieldsInfo,
+    unFixedIndicator,
+  )
 }
 
 /**
- * @description: 初始化一下被排序过的表格字段信息,这里要注意深拷贝
+ * @description: 对话框打开,做一些公布设置
  * @return {*}
  */
-const initSortedTableFieldsInfo = () => {
-  sortedTableFieldsInfo.splice(
-    0,
-    sortedTableFieldsInfo.length,
-    ...JSON.parse(JSON.stringify(props.tableFieldsInfo)),
+const indicatorOpen = () => {
+  isFirstShow.value = true // 用于同步表格和右侧栏目的顺序
+  // 给不固定框一个初始值
+  updateUnfixedIndicator(
+    props.indicatorFields,
+    isFirstShow,
+    props.tableFieldsInfo,
+    unFixedIndicator,
   )
+  updateFixedIndicator(props.indicatorFields, fixedIndicator) // 固定框给初始值
+}
+
+/**
+ * @description: 对话框关闭
+ * @return {*}
+ */
+const indicatorClose = () => {
+  // 重置到第一个锚点
+  indicatorNavActive.value = props.indicatorFields[0]?.name
+  // isSaveCustom.value = false
+  saveForm.isSaveCustom = false
 }
 
 /**
@@ -144,118 +158,19 @@ const showCustomIndicator = () => {
 }
 
 /**
- * @description: 判断当前是否可以插入
- * @param {*} e 触发事件的事件对象
- * @return {*}
- */
-const canInsert = (e: DragEvent): boolean => {
-  return !(
-    !e.target ||
-    !dragingRef.value ||
-    !dragContentRef.value ||
-    dragingRef.value === e.target ||
-    e.target === dragContentRef.value
-  )
-}
-
-/**
- * @description: 拖动开始
- * @param {*} e 拖动事件对象
- * @return {*}
- */
-const dragStart = (e: DragEvent) => {
-  if (!dragContentRef.value || !e.target || !e.dataTransfer) return
-  e.dataTransfer.effectAllowed = 'move' // 设置拖拽类型
-  dragingRef.value = e.target as HTMLElement // 获取当前拖拽的元素
-}
-
-/**
- * @description: 拖拽过程
- * @param {*} e 拖拽事件对象
+ * @description: 生成新的表格字段信息
  * @return {*}
  */
-const dragEnter = (e: DragEvent) => {
-  e.preventDefault()
-  if (!canInsert(e)) return
-  let listArray = Array.from(dragContentRef.value!.childNodes) // 整个可拖动的列表
-  let curIndex = listArray.indexOf(dragingRef.value!) // 当前拖动元素在整个列表中的文职
-  let targeIndex = listArray.indexOf(e.target as HTMLElement) // 目前拖动到的元素在整个列表中的位置
-
-  if (curIndex < targeIndex) {
-    dragContentRef.value!.insertBefore(
-      dragingRef.value!,
-      (e.target as HTMLElement).nextSibling,
-    )
-  } else {
-    dragContentRef.value!.insertBefore(
-      dragingRef.value!,
-      e.target as HTMLElement,
-    )
-  }
-}
-
-/**
- * @description: 拖拽结束
- * @param {*} e 拖拽对象
- * @return {*}
- */
-const dragOver = (e: DragEvent) => {
-  e.preventDefault()
-  if (!e.target) return
-}
-
-/**
- * @description: 更新一下排序后的表格字段信息
- * @return {*}
- */
-const updateSortedTableFieldsInfo = () => {
-  sortedTableFieldsInfo.splice(0, sortedTableFieldsInfo.length)
-  let sortedNodes = Array.from(dragContentRef.value!.children)
-  let sortedFields: Array<string> = []
-  // 把所有能够排序的节点的name提取出来,这些就是已经排好序的字段名
-  sortedNodes.forEach((node: Element) => {
-    if (node.nodeType === 1) {
-      // 判断是否为元素节点
-      sortedFields.push(node.getAttribute('name') as string)
-    }
-  })
-
-  // 根据排好序的字段名,去原本的字段信息中去找对应的字段名,把他加入到新字段信息中
-  // 这里的排序与unFixedIndicator的排序不一样,这里是为了同步用户的拖动
-  // unFixedIndicator的排序是为了同步新增的选项
-  sortedFields.forEach((item: string) => {
-    let fFiled = unFixedIndicator.value.find(field => field.name === item)
-    if (fFiled) sortedTableFieldsInfo.push(fFiled)
-  })
-  console.log('fixed')
-  console.log(fixedIndicator.value)
-  // 还剩下固定字段的信息,把他加入数组头部
-  sortedTableFieldsInfo.unshift(...fixedIndicator.value)
-}
-
-/**
- * @description: 获取当前被激活的指标
- * @return {*}
- */
-const getActivedIndicator = (): string[] => {
-  let result: string[] = []
-  props.indicatorFields.forEach(item => {
-    item.children.forEach(field => {
-      if (field.state) result.push(field.name)
-    })
-  })
-  return result
-}
-
-/**
- * @description: 应用配置的自定义指标
- * @return {*}
- */
-const applyCustomIndicator = () => {
+const generateNewTableField = () => {
   customIndicatorVisible.value = false
-  updateSortedTableFieldsInfo() // 排序
+  updateSortedTableFieldsInfo(
+    sortedTableFieldsInfo,
+    dragContentRef,
+    unFixedIndicator,
+    fixedIndicator,
+  ) // 排序
   // 拿到当前激活的所有自定义指标
-  let actived = getActivedIndicator()
+  let actived = getActivedIndicator(props.indicatorFields)
   // 把目前排好序的字段信息复制给新的字段信息
   newTableFieldsInfo.splice(
     0,
@@ -264,19 +179,45 @@ const applyCustomIndicator = () => {
   )
   // 根据当前已经激活的字段name,去吧字段信息中的state改为true
   newTableFieldsInfo.map(item => {
-    // console.log(item.name)
     if (actived.includes(item.name)) {
       item.state = true
     } else {
       item.state = false
     }
   })
-  console.log(newTableFieldsInfo)
-  emits('updateFields', newTableFieldsInfo)
 }
 
 /**
- * @description: 取消选择
+ * @description: 验证保存自定义指标的表单是否填写正确了
+ * @param {FormInstance | undefined} saveFormRef 自定义指标表单实例
+ * @return {Promise<boolean>} 验证结果
+ */
+const validSaveForm = async (
+  saveFormRef: FormInstance | undefined,
+): Promise<boolean> => {
+  if (!saveFormRef) return false
+  let result = await saveFormRef.validate()
+  return result
+}
+
+/**
+ * @description: 应用配置的自定义指标
+ * @return {*}
+ */
+const applyCustomIndicator = async (saveFormRef: FormInstance | undefined) => {
+  let fileName = null
+  // 需要保存自定义指标为常用模板就需要去检查一下这个文件名
+  if (saveForm.isSaveCustom) {
+    let result = await validSaveForm(saveFormRef)
+    if (!result) return
+    fileName = saveForm.saveCustomName
+  }
+  generateNewTableField()
+  emits('updateFields', newTableFieldsInfo, fileName)
+}
+
+/**
+ * @description: 取消已经选中的指标
  * @param {*} name 字段名
  * @return {*}
  */
@@ -291,7 +232,7 @@ const cancelSelect = (name: string) => {
   }
 }
 
-initSortedTableFieldsInfo()
+initSortedTableFieldsInfo(sortedTableFieldsInfo, props.tableFieldsInfo)
 
 defineExpose({ showCustomIndicator })
 </script>
@@ -340,7 +281,10 @@ defineExpose({ showCustomIndicator })
               </div>
               <el-row class="indicatorItem">
                 <el-col :span="8" v-for="item in field.children">
-                  <el-checkbox-group v-model="field.value">
+                  <el-checkbox-group
+                    v-model="field.value"
+                    @change="changeIndicatorChecked"
+                  >
                     <el-checkbox
                       style="margin-bottom: 16px"
                       :value="item.name"
@@ -387,11 +331,43 @@ defineExpose({ showCustomIndicator })
       </div>
 
       <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="customIndicatorVisible = false">取消</el-button>
-          <el-button type="primary" @click="applyCustomIndicator">
-            应用
-          </el-button>
+        <div class="dialogFooter">
+          <div class="footerTools">
+            <el-form
+              ref="saveFormRef"
+              :model="saveForm"
+              :rules="saveFormRules"
+              inline
+            >
+              <el-form-item class="saveFormItem" prop="isSaveCustom">
+                <el-checkbox
+                  v-model="saveForm.isSaveCustom"
+                  label="保存为常用自定义指标"
+                  size="default"
+                />
+              </el-form-item>
+              <el-form-item
+                v-if="saveForm.isSaveCustom"
+                prop="saveCustomName"
+                class="saveFormItem"
+              >
+                <el-input
+                  v-model="saveForm.saveCustomName"
+                  style="width: 240px"
+                  placeholder="请输入自定义指标名"
+                />
+              </el-form-item>
+            </el-form>
+          </div>
+          <div class="footerBtn">
+            <el-button @click="customIndicatorVisible = false">取消</el-button>
+            <el-button
+              type="primary"
+              @click="applyCustomIndicator(saveFormRef)"
+            >
+              应用
+            </el-button>
+          </div>
         </div>
       </template>
     </el-dialog>
@@ -525,20 +501,6 @@ defineExpose({ showCustomIndicator })
 .dragBlock:not(.notAllow):hover {
   background-color: #e7e7e7;
 }
-
-// .draging {
-//   position: relative;
-// }
-
-// .draging::after {
-//   content: '';
-//   position: absolute;
-//   top: 0;
-//   right: 0;
-//   bottom: 0;
-//   left: 0;
-// }
-
 // 禁止拖拽
 .notAllow {
   cursor: not-allowed;
@@ -595,4 +557,16 @@ defineExpose({ showCustomIndicator })
   overflow-x: hidden;
   overflow-y: auto;
 }
+
+.saveFormItem {
+  margin-bottom: 0;
+  margin-right: 15px;
+  // padding: 0 5px;
+}
+
+.dialogFooter {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
 </style>

+ 0 - 1
src/components/navigation/Menu.vue

@@ -85,7 +85,6 @@ const routerDefaultActive = computed(() => {
   if (attrs.router) {
     let menuItem = props.menuList.find(item => {
       let basePath = item.path // 你可以动态设置这个值
-      console.log(basePath)
       if (basePath) {
         const escapedBasePath = basePath.replace(/\//g, '\\/') // 转义斜杠
         const regex = new RegExp(`^${escapedBasePath}(?:\/.*)?$`)

+ 102 - 11
src/components/table/Table.vue

@@ -8,7 +8,7 @@ import type {
 import type { TableData } from '@/types/Tables/table'
 import type { PaginationConfig } from '@/types/Tables/pagination'
 
-import { computed, onMounted, reactive, ref, watch } from 'vue'
+import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
 
 import { Plus, Operation } from '@element-plus/icons-vue'
 import { useTable } from '@/hooks/useTable'
@@ -46,6 +46,8 @@ const emits = defineEmits([
   'updateCustomIndicator',
   'pageSizeChange',
   'curPageChange',
+  'saveToTemplate',
+  'changeScheme',
 ])
 
 const {
@@ -83,6 +85,21 @@ const paginationConfig = reactive<PaginationConfig>({
 // 表格可见性的观察者
 const tableVisOb = new IntersectionObserver(obScroll)
 
+/**
+ * @description: 生成一个表格大小的观察者,当大小变化去动态的调整滚动条的位置
+ * @param {ResizeObserverEntry[]} entries 观察的实例
+ * @return {*}
+ */
+const tableSizeOb = new ResizeObserver((entries: ResizeObserverEntry[]) => {
+  if (!elScrollBarH.value || !tableContainer.value) return
+  // 找到表格容器
+  let container = entries.find(item => item.target === tableContainer.value)
+  if (!container) return
+  // console.log(container)
+  let left = container.target.getBoundingClientRect().left
+  elScrollBarH.value.style.left = left + 'px'
+})
+
 // 表格字段信息
 const tableFieldsInfo = reactive<Array<TableFields>>([])
 
@@ -92,6 +109,9 @@ const indicatorFields = reactive<Array<BaseFieldItem<TableFields>>>([])
 // 自定义指标侧边栏的默认选中
 const defaultActiveNav = ref<string>('')
 
+// 当前选中的方案名,不包括默认方案
+const schemeActive = ref<string>('')
+
 // 批量操作选中的值
 const batchOper = ref<string>()
 
@@ -180,10 +200,35 @@ const showCustomIndicator = () => {
  * @param {*} newTableFieldsInfo 新的表格字段信息
  * @return {*}
  */
-const applyCustomIndicator = (newTableFieldsInfo: Array<TableFields>) => {
+const applyCustomIndicator = (
+  newTableFieldsInfo: Array<TableFields>,
+  fileName: string,
+) => {
   tableFieldsInfo.splice(0, tableFieldsInfo.length, ...newTableFieldsInfo)
 
-  emits('updateCustomIndicator', indicatorFields, tableFieldsInfo)
+  emits('updateCustomIndicator', indicatorFields, tableFieldsInfo, fileName)
+}
+
+/**
+ * @description: 更换方案
+ * @param {*} schemeName 方案name
+ * @return {*}
+ */
+const changeScheme = (schemeName: string) => {
+  emits('changeScheme', schemeName)
+}
+
+/**
+ * @description: 更新指标方案
+ * @return {*}
+ */
+const updateIndicatorScheme = () => {
+  nextTick(() => {
+    initIndicatorFields(indicatorFields, props.tableFields, defaultActiveNav)
+    initTableFields(tableFieldsInfo, props.sortedTableFields, indicatorFields)
+    console.log(JSON.parse(JSON.stringify(props.tableFields)))
+    console.log(JSON.parse(JSON.stringify(props.sortedTableFields)))
+  })
 }
 
 watch(
@@ -200,8 +245,7 @@ watch(
 
     tableQueryFormRef.value?.initfilterForm()
     tableQueryFormRef.value?.initFilterFields()
-    initIndicatorFields(indicatorFields, props.tableFields, defaultActiveNav)
-    initTableFields(tableFieldsInfo, props.sortedTableFields, indicatorFields)
+    updateIndicatorScheme() // 更新指标方案
   },
   {
     deep: true,
@@ -210,6 +254,11 @@ watch(
 onMounted(() => {
   initScroll()
   tableVisOb.observe(tableContent.value as HTMLElement)
+  tableSizeOb.observe(tableContainer.value as HTMLElement)
+})
+
+defineExpose({
+  updateIndicatorScheme,
 })
 </script>
 
@@ -249,13 +298,31 @@ onMounted(() => {
             <el-button class="exportData w120 ml16" plain>导出数据</el-button>
           </slot>
           <slot name="customIndicator">
-            <el-button
-              @click="showCustomIndicator"
-              class="customIndicator w120 ml16"
-              plain
-              :icon="Operation"
-              >自定义指标</el-button
+            <el-popover
+              placement="bottom"
+              trigger="hover"
+              popper-style="padding:0 0px; background-color: #fff;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);"
             >
+              <div class="popoverContainer">
+                <div
+                  class="schemeItem"
+                  v-for="scheme in schemeList"
+                  @click="changeScheme(scheme.label)"
+                >
+                  <span class="schemeLabel"></span> {{ scheme.label }}
+                </div>
+              </div>
+              <template #reference>
+                <el-button
+                  @click="showCustomIndicator"
+                  class="customIndicator w120 ml16"
+                  plain
+                  :icon="Operation"
+                  >自定义指标</el-button
+                >
+              </template>
+            </el-popover>
           </slot>
         </div>
       </div>
@@ -404,4 +471,28 @@ onMounted(() => {
   // position: fixed !important;
   bottom: 5px;
 }
+
+.popoverContainer {
+  // width: 100%;
+  // background-color: lightblue;
+}
+
+.schemeItem {
+  width: 100%;
+  height: 40px;
+  line-height: 40px;
+
+  cursor: pointer;
+}
+
+.schemeItem:hover {
+  background-color: #cbcccc;
+}
+
+.schemeLabel {
+  line-height: 40px;
+  padding: 20px 10px;
+  font-size: 14px;
+  color: #606266;
+}
 </style>

+ 180 - 0
src/hooks/useCustomIndicatorDialog.ts

@@ -0,0 +1,180 @@
+import type { BaseFieldItem, TableFields } from '@/types/Tables/table'
+import type { Ref } from 'vue'
+
+export function useCustomIndicatorDialog() {
+  /**
+   * @description: 初始化一下被排序过的表格字段信息,这里要注意深拷贝
+   * @return {*}
+   */
+  const initSortedTableFieldsInfo = (
+    sortedTableFieldsInfo: Array<TableFields>,
+    propsTableFieldsInfo: Array<TableFields>,
+  ) => {
+    sortedTableFieldsInfo.splice(
+      0,
+      sortedTableFieldsInfo.length,
+      ...JSON.parse(JSON.stringify(propsTableFieldsInfo)),
+    )
+  }
+
+  /**
+   * @description: 验证自定义指标名称
+   * @param {*} _rule 规则
+   * @param {*} value 值
+   * @param {*} callback 回调
+   * @return {*}
+   */
+  const validCustomName = (_rule: any, value: any, callback: any) => {
+    const regex = new RegExp(
+      `^[\u4e00-\u9fa5a-zA-Z][\u4e00-\u9fa5_a-zA-Z0-9]{0,9}$`,
+    )
+    if (regex.test(value)) {
+      callback()
+    } else {
+      callback(
+        new Error('请输入1-10位中文、英文、数字或下划线,必须以中英文字符开头'),
+      )
+    }
+  }
+
+  /**
+   * @description: 更新固定指标的框的值
+   * @return {*}
+   */
+  const updateFixedIndicator = (
+    propIndicator: Array<BaseFieldItem<TableFields>>,
+    fixedIndicator: TableFields[],
+  ) => {
+    let result: TableFields[] = []
+    // 对于固定的指标,为了防止传入的数据有误,所有直接state设置为true
+    propIndicator.forEach(item => {
+      result.push(
+        ...item.children.filter(item => {
+          return item.fixed
+        }),
+      )
+    })
+    fixedIndicator.splice(0, fixedIndicator.length, ...result)
+  }
+
+  /**
+   * @description: 同步右侧栏目和表格字段的顺序
+   * @param {Array<TableFields>} propsTableFields 表格字段
+   * @param {Array<string>} allSelected 所有目前选中的字段名
+   * @return {*}
+   */
+  const syncTableField = (
+    propsTableFields: Array<TableFields>,
+    allSelected: Array<string>,
+  ) => {
+    let syncFieldSelected: Array<string> = []
+    propsTableFields.forEach(item => {
+      let fieldName = allSelected.find(select => select === item.name)
+      if (fieldName) syncFieldSelected.push(fieldName)
+    })
+    allSelected.splice(0, allSelected.length, ...syncFieldSelected)
+  }
+
+  /**
+   * @description: 更新不固定框的状态和顺序
+   * @return {*}
+   */
+  const updateUnfixedIndicator = (
+    propIndicator: Array<BaseFieldItem<TableFields>>,
+    isFirstShow: Ref<boolean>,
+    propsTableFields: Array<TableFields>,
+    unFixedIndicator: TableFields[],
+  ) => {
+    let result: TableFields[] = []
+
+    // 拿到所有的子元素
+    let allChilds = propIndicator.flatMap(
+      item => item.children,
+    ) as Array<TableFields>
+
+    // 重置所有状态
+    allChilds.forEach(item => {
+      if (!item.fixed) item.state = false
+    })
+
+    // 获取所有选中的值
+    let allSelected = propIndicator.flatMap(item => item.value) as Array<string>
+
+    // 在打开对话框的时候要去同步一下表格目前显示的字段和对话框右侧栏目的顺序
+    if (isFirstShow.value && propsTableFields.length > 0) {
+      syncTableField(propsTableFields, allSelected)
+      isFirstShow.value = false // 只需要第一次的时候同步一下
+    }
+
+    // 之所以要这样去筛选,是为了保证顺序,
+    //  如果直接通过filter去找存在于allSelected中的元素,
+    //  那么当一个在前面的选项首先取消选择,然后又重新选中的时候,这个选项又会直接出现在他原来的位置,而不是末尾
+    allSelected.forEach(item => {
+      let child = allChilds.find(child => !child.fixed && child.name === item)
+      if (child) {
+        child.state = true // 更新状态
+        result.push(child)
+      }
+    })
+    unFixedIndicator.splice(0, unFixedIndicator.length, ...result)
+  }
+
+  /**
+   * @description: 更新一下排序后的表格字段信息
+   * @return {*}
+   */
+  const updateSortedTableFieldsInfo = (
+    sortedTableFieldsInfo: Array<TableFields>,
+    dragContentRef: Ref<HTMLElement | null>,
+
+    unFixedIndicator: TableFields[],
+    fixedIndicator: TableFields[],
+  ) => {
+    sortedTableFieldsInfo.splice(0, sortedTableFieldsInfo.length)
+    let sortedNodes = Array.from(dragContentRef.value!.children)
+    let sortedFields: Array<string> = []
+    // 把所有能够排序的节点的name提取出来,这些就是已经排好序的字段名
+    sortedNodes.forEach((node: Element) => {
+      if (node.nodeType === 1) {
+        // 判断是否为元素节点
+        sortedFields.push(node.getAttribute('name') as string)
+      }
+    })
+
+    // 根据排好序的字段名,去原本的字段信息中去找对应的字段名,把他加入到新字段信息中
+    // 这里的排序与unFixedIndicator的排序不一样,这里是为了同步用户的拖动
+    // unFixedIndicator的排序是为了同步新增的选项
+    sortedFields.forEach((item: string) => {
+      let fFiled = unFixedIndicator.find(field => field.name === item)
+      if (fFiled) sortedTableFieldsInfo.push(fFiled)
+    })
+    // 还剩下固定字段的信息,把他加入数组头部
+    sortedTableFieldsInfo.unshift(...fixedIndicator)
+  }
+
+  /**
+   * @description: 获取当前被激活的指标
+   * @return {*}
+   */
+  const getActivedIndicator = (
+    propIndicator: Array<BaseFieldItem<TableFields>>,
+  ): string[] => {
+    let result: string[] = []
+    propIndicator.forEach(item => {
+      item.children.forEach(field => {
+        if (field.state) result.push(field.name)
+      })
+    })
+    return result
+  }
+
+  return {
+    initSortedTableFieldsInfo,
+    validCustomName,
+    updateFixedIndicator,
+    syncTableField,
+    updateUnfixedIndicator,
+    getActivedIndicator,
+    updateSortedTableFieldsInfo,
+  }
+}

+ 82 - 0
src/hooks/useDialogDrag.ts

@@ -0,0 +1,82 @@
+/*
+ * @Author: fxs bjnsfxs@163.com
+ * @Date: 2024-10-25 15:26:00
+ * @LastEditors: fxs bjnsfxs@163.com
+ * @LastEditTime: 2024-10-25 15:32:53
+ * @FilePath: \Quantity-Creation-Management-System\src\hooks\useDialogDrag.ts
+ * @Description: 只用于自定义指标对话框的拖拽功能
+ *
+ */
+
+import type { Ref } from 'vue'
+
+export function useDialogDrag(
+  dragingRef: Ref<HTMLElement | null>,
+  dragContentRef: Ref<HTMLElement | null>,
+) {
+  /**
+   * @description: 判断当前是否可以插入
+   * @param {*} e 触发事件的事件对象
+   * @return {*}
+   */
+  const canInsert = (e: DragEvent): boolean => {
+    return !(
+      !e.target ||
+      !dragingRef.value ||
+      !dragContentRef.value ||
+      dragingRef.value === e.target ||
+      e.target === dragContentRef.value
+    )
+  }
+
+  /**
+   * @description: 拖动开始
+   * @param {*} e 拖动事件对象
+   * @return {*}
+   */
+  const dragStart = (e: DragEvent) => {
+    if (!dragContentRef.value || !e.target || !e.dataTransfer) return
+    e.dataTransfer.effectAllowed = 'move' // 设置拖拽类型
+    dragingRef.value = e.target as HTMLElement // 获取当前拖拽的元素
+  }
+
+  /**
+   * @description: 拖拽过程
+   * @param {*} e 拖拽事件对象
+   * @return {*}
+   */
+  const dragEnter = (e: DragEvent) => {
+    e.preventDefault()
+    if (!canInsert(e)) return
+    let listArray = Array.from(dragContentRef.value!.childNodes) // 整个可拖动的列表
+    let curIndex = listArray.indexOf(dragingRef.value!) // 当前拖动元素在整个列表中的文职
+    let targeIndex = listArray.indexOf(e.target as HTMLElement) // 目前拖动到的元素在整个列表中的位置
+
+    if (curIndex < targeIndex) {
+      dragContentRef.value!.insertBefore(
+        dragingRef.value!,
+        (e.target as HTMLElement).nextSibling,
+      )
+    } else {
+      dragContentRef.value!.insertBefore(
+        dragingRef.value!,
+        e.target as HTMLElement,
+      )
+    }
+  }
+
+  /**
+   * @description: 拖拽结束
+   * @param {*} e 拖拽对象
+   * @return {*}
+   */
+  const dragOver = (e: DragEvent) => {
+    e.preventDefault()
+    if (!e.target) return
+  }
+  return {
+    dragStart,
+    dragEnter,
+    dragOver,
+  }
+}

+ 90 - 17
src/hooks/useTable.ts

@@ -6,12 +6,15 @@ import type {
   TableFields,
   TableInfoItem,
   SaveFields,
+  SaveCustomIncicatorScheme,
+  CustomIndicatorScheme,
 } from '@/types/Tables/table'
 import type { Ref } from 'vue'
 import type { PaginationConfig } from '@/types/Tables/pagination'
 
 import { resetReactive } from '@/utils/common'
 import axiosInstance from '@/utils/axios/axiosInstance'
+
 export function useTable() {
   const getData = async (
     url: string,
@@ -19,7 +22,7 @@ export function useTable() {
   ): Promise<Array<TableData>> => {
     try {
       const res = (await axiosInstance.get(url, config)) as ResponseInfo
-      console.log(res)
+
       if (res.code !== 0) throw new Error('请求失败')
       return res.data as TableData[]
     } catch (err) {
@@ -42,19 +45,87 @@ export function useTable() {
   }
 
   /**
+   * @description: 生成一个唯一的方案键
+   * @param {string} schemeName 方案名
+   * @param {string} return 唯一键
+   * @return {*}
+   */
+  function generateUniqueKey(schemeName: string): string {
+    const hash = Array.from(schemeName).reduce((acc, char) => {
+      return (acc * 31 + char.charCodeAt(0)) % 1e12
+    }, 0)
+
+    return String(hash).padStart(12, '0')
+  }
+
+  /**
+   * @description: 获取保存的表格信息
+   * @param {string} name 这个是整个表格的名字,用于区分不同表格
+   * @param {string} customSchemeName 这个是自定义方案的名称
+   * @return {SaveCustomIncicatorScheme | SaveFields | undefined} 返回的可能是整个表格的所有方案,也可能只是某个具体的方案,取决于传参
+   */
+  function getTableInfo(name: string): SaveCustomIncicatorScheme | undefined
+  function getTableInfo(
+    name: string,
+    customSchemeName: string,
+  ): SaveFields | undefined
+
+  function getTableInfo(
+    name: string,
+    customSchemeName?: string,
+  ): SaveCustomIncicatorScheme | SaveFields | undefined {
+    try {
+      let info = localStorage.getItem(name)
+      if (!info) throw new Error('没有该信息')
+      let keyName: string = ''
+      if (customSchemeName) {
+        keyName = generateUniqueKey(customSchemeName)
+        return (JSON.parse(info) as SaveCustomIncicatorScheme)[keyName]
+      }
+      return JSON.parse(info) as SaveCustomIncicatorScheme
+    } catch (err) {
+      console.log('获取失败')
+      console.log(err)
+      return undefined
+    }
+  }
+
+  /**
    * @description: 保存表格信息
    * @return {*}
    */
   const saveTableInfo = (
     name: string,
     tableInfo: TableInfoItem<TableData, BaseFieldItem<TableFields>>,
+    isSaveToTemplate: boolean, // 是否要保存成自定义方案
+    customSchemeName?: string, // 自定义指标的方案名
   ) => {
     try {
-      let result = {
+      customSchemeName = customSchemeName ? customSchemeName : '默认'
+      let keyName = generateUniqueKey(customSchemeName)
+
+      let result: SaveFields = {
+        name: keyName, // 这个是拿来作为标识符,也同时是这个方案的键
+        label: customSchemeName,
         fields: tableInfo.fields, // 字段数组
         tableSortedFields: tableInfo.tableSortedFields, // 排序过后的表格字段,仅用于展示表格字段
       }
-      localStorage.setItem(name, JSON.stringify(result))
+
+      let saveInfo = getTableInfo(name)
+      // 没有这个信息需要先去创建一个默认版本
+      if (!saveInfo) {
+        saveInfo = {
+          [keyName]: result,
+        }
+      }
+      saveInfo[keyName] = result
+      // if (isSaveToTemplate) {
+      //   saveInfo[keyName] = result
+      // } else {
+      //   saveInfo['default'] = result
+      // }
+
+      localStorage.setItem(name, JSON.stringify(saveInfo))
     } catch (err) {
       console.log('保存失败')
       console.log(err)
@@ -62,23 +133,24 @@ export function useTable() {
   }
 
   /**
-   * @description: 获取保存的表格信息
-   * @param {*} TableInfo
+   * @description: 获取所有自定义方案的数组
+   * @param {string} name 表格名
    * @return {*}
    */
-  const getTableInfo = (name: string): SaveFields | undefined => {
-    try {
-      let info = localStorage.getItem(name)
-      if (!info) throw new Error('没有该信息')
-      return JSON.parse(info) as {
-        fields: BaseFieldItem<TableFields>[]
-        tableSortedFields: TableFields[]
-      }
-    } catch (err) {
-      console.log('获取失败')
-      console.log(err)
-      return undefined
+  const getCustomScheme = (name: string): Array<CustomIndicatorScheme> => {
+    let info = getTableInfo(name)
+    let result: Array<CustomIndicatorScheme> = []
+    if (!info) return []
+    for (let [key, scheme] of Object.entries(info)) {
+      let name = decodeURIComponent(key)
+      let label = decodeURIComponent(scheme.label)
+
+      result.push({
+        name,
+        label,
+      })
     }
+    return result
   }
 
   /**
@@ -189,5 +261,6 @@ export function useTable() {
     initTableFields,
     initIndicatorFields,
     setCacheTableData,
+    getCustomScheme,
   }
 }

+ 16 - 0
src/types/Tables/customIndicatorDialog.ts

@@ -0,0 +1,16 @@
+import type { TableFields, BaseFieldItem } from './table'
+
+interface SaveForm {
+  isSaveCustom: boolean
+  saveCustomName: string
+}
+
+interface DialogProps {
+  indicatorFields: Array<BaseFieldItem<TableFields>>
+  defaultActiveNav: string
+  tableFieldsInfo: Array<TableFields>
+  fixedFields: Array<string>
+  SaveFormData?: SaveForm
+}
+
+export type { DialogProps, SaveForm }

+ 17 - 0
src/types/Tables/table.ts

@@ -85,14 +85,29 @@ interface TableProps {
   fixedFields: Array<string> // 需要固定显示的字段
   remotePagination?: boolean // 是否需要远程分页
   paginationConfig: TablePaginationProps // 分页配置
+  schemeList: Array<CustomIndicatorScheme> // 自定义方案列表
 }
 
 // 保存的表格信息格式
 interface SaveFields {
+  name: string // 这个是文件的标识名,也是这个文件的索引键
+  label: string // 这个是用户自定义的文件名
   fields: BaseFieldItem<TableFields>[]
   tableSortedFields: TableFields[]
 }
 
+// 保存的自定义方案格式
+// 如果没有的话会创建一个键位default的方案
+interface SaveCustomIncicatorScheme {
+  [key: string]: SaveFields
+}
+
+// 读取出来的方案列表信息
+interface CustomIndicatorScheme {
+  name: string // 方案名
+  label: string // 方案标签名,即用户自定义的名称
+}
+
 // 每个表格需要的信息格式,包含通用的属性
 
 interface TableInfoItem<
@@ -116,6 +131,8 @@ export type {
   TableFilterItem,
   TableProps,
   SaveFields,
+  SaveCustomIncicatorScheme,
+  CustomIndicatorScheme,
   TableData,
   FilterInfo,
   TableFields,

+ 7 - 0
src/utils/localStorage/saveTableCustomIndicator.ts

@@ -0,0 +1,7 @@
+class TableCustomIndicatorController {
+  #tableName: string = '' // 整个表格的表格名
+
+  constructor(tableName: string) {
+    this.#tableName = tableName
+  }
+}

+ 37 - 5
src/views/Promotion/adManage/ttad.vue

@@ -11,6 +11,7 @@ import type {
   TableInfoItem,
   BaseFieldItem,
   TableFields,
+  CustomIndicatorScheme,
 } from '@/types/Tables/table'
 import type { TablePaginationProps } from '@/types/Tables/pagination'
 import type { BaseMenu } from '@/types/Promotion/Menu'
@@ -41,6 +42,8 @@ import { useTable } from '@/hooks/useTable'
 import Menu from '@/components/navigation/Menu.vue'
 import Table from '@/components/table/Table.vue'
 
+type TableType = InstanceType<typeof Table>
+
 // 表格信息
 interface TableInfo extends BaseTableInfo {
   account: TableInfoItem<AdmgeTTAccData, BaseFieldItem<AdmgeTTAccFileds>> // 账户信息
@@ -78,7 +81,15 @@ interface Operation extends Operations {
 
 const { AllApi } = useRequest()
 const { disableDate, shortcuts } = useDate()
-const { getData, saveTableInfo, getTableInfo, updateTableFields } = useTable()
+const {
+  getData,
+  saveTableInfo,
+  getTableInfo,
+  updateTableFields,
+  getCustomScheme,
+} = useTable()
+
+const tableRef = ref<TableType>()
 
 // 当前菜单选中
 const activeMenu = ref('account')
@@ -562,6 +573,7 @@ const tableInfo = reactive<TableInfo>({
   },
 })
 
+// 分页配置
 const tablePaginationConfig = reactive<TablePaginationProps>({
   total: 0,
   pageSizeList: [10, 20, 40],
@@ -608,6 +620,9 @@ const ttAdMenu: BaseMenu[] = [
 // 选择的日期
 const selectDate = ref(shortcuts[0].value)
 
+// 自定义指标保存的方案
+const customIndicatorScheme = ref<Array<CustomIndicatorScheme>>([])
+
 /**
  * @description: 保存的文件名
  * @return {*}
@@ -639,9 +654,9 @@ const menuActiveChange = (val: string) => {
  * @description: 更新表格的字段信息
  * @return {*}
  */
-const updateTableInfo = () => {
-  let info = getTableInfo(fileName.value)
-
+const updateTableInfo = (customName: string = '默认') => {
+  let info = getTableInfo(fileName.value, customName)
+  console.log(info)
   if (info) {
     tableInfo[activeMenu.value].fields = info.fields
     tableInfo[activeMenu.value].tableSortedFields = info.tableSortedFields
@@ -679,6 +694,7 @@ const updateTableData = async () => {
 const updateCustomIndicator = (
   newIndicator: Array<BaseFieldItem<TableFields>>,
   newSortedTablefileds: Array<TableFields>,
+  customSaveName: string,
 ) => {
   updateTableFields(
     tableInfo,
@@ -687,7 +703,8 @@ const updateCustomIndicator = (
     newSortedTablefileds,
   )
     .then(() => {
-      saveTableInfo(fileName.value, tableInfo[activeMenu.value])
+      saveTableInfo(fileName.value, tableInfo[activeMenu.value], customSaveName)
+      customIndicatorScheme.value = getCustomScheme(fileName.value)
     })
     .catch(() => {
       ElMessage.error('更新失败')
@@ -695,8 +712,20 @@ const updateCustomIndicator = (
   // saveTableInfo()
 }
 
+/**
+ * @description: 改变方案,要去更新一下表格字段
+ * @param {*} schemeName 新的方案名
+ * @return {*}
+ */
+const changeScheme = (schemeName: string) => {
+  console.log(schemeName)
+  updateTableInfo(schemeName)
+  tableRef.value?.updateIndicatorScheme()
+}
+
 updateTableData()
 onMounted(() => {
+  customIndicatorScheme.value = getCustomScheme(fileName.value)
   // let info = getTableInfo(fileName.value)
   // if (Object.keys(info).length !== 0) {
   //   Object.assign(tableInfo, info)
@@ -733,13 +762,16 @@ onMounted(() => {
     </div>
     <div class="ttAdContent">
       <Table
+        ref="tableRef"
         :pagination-config="tablePaginationConfig"
         :table-fields="tableInfo[activeMenu].fields"
         :sorted-table-fields="tableInfo[activeMenu].tableSortedFields"
         :table-data="tableInfo[activeMenu].data"
         :fixed-fields="tableInfo[activeMenu].fixedFields"
         :filters-info="tableInfo[activeMenu].filters"
+        :scheme-list="customIndicatorScheme"
         @update-custom-indicator="updateCustomIndicator"
+        @change-scheme="changeScheme"
       >
         <template #operations>
           <div>

+ 0 - 33
src/views/Promotion/promotion.vue

@@ -6,39 +6,6 @@ import Menu from '@/components/navigation/Menu.vue'
 
 import { ref } from 'vue'
 
-// <el-menu
-//         :default-active="defaultActive"
-//         class="promotionMenu"
-//         :collapse="isCollapse"
-//         style="min-width: 180px"
-//         router
-//       >
-//         <template v-for="menuItem in menu" :key="menuItem.name">
-//           <!-- 有子菜单 -->
-//           <el-sub-menu v-if="menuItem.children" :index="menuItem.name">
-//             <template #title>
-//               <el-icon><location /></el-icon>
-//               <span>{{ menuItem.title }}</span>
-//             </template>
-//             <el-menu-item v-for="item in menuItem.children" :index="item.path">
-//               <el-icon><document /></el-icon>
-//               <span>{{ item.title }}</span>
-//             </el-menu-item>
-//           </el-sub-menu>
-//           <!-- 无子菜单 -->
-//           <el-menu-item :index="menuItem.path" v-else>
-//             <el-icon><document /></el-icon>
-//             <span>{{ menuItem.title }}</span>
-//           </el-menu-item>
-//         </template>
-//       </el-menu>
-//       <div class="sideBarFold" @click="isCollapse = !isCollapse">
-//         <el-icon :size="20"><Fold /></el-icon>
-//       </div>
-
-// 侧边菜单的展示控制
-const isCollapse = ref(false)
-
 // 菜单默认选中
 const defaultActive = ref<string>(router.currentRoute.value.path)