浏览代码

Merge pull request #334 from vvandk/master

feat: 表格工具栏新增列设置功能
Archer 1 年之前
父节点
当前提交
9cbccfae96

+ 3 - 1
package.json

@@ -106,7 +106,9 @@
     "vite-plugin-purge-icons": "^0.9.2",
     "vite-plugin-style-import": "2.0.0",
     "vite-plugin-svg-icons": "^2.0.1",
-    "vue-tsc": "^1.8.8"
+    "vue-tsc": "^1.8.8",
+    "vue-draggable-plus": "^0.2.6",
+    "lodash": "^4.17.21"
   },
   "engines": {
     "node": ">= 14.18.0"

+ 35 - 11
src/components/Table/src/Table.vue

@@ -17,6 +17,9 @@ import { getSlot } from '@/utils/tsxHelper'
 import TableActions from './components/TableActions.vue'
 // import Sortable from 'sortablejs'
 // import { Icon } from '@/components/Icon'
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
 
 export default defineComponent({
   name: 'Table',
@@ -121,7 +124,9 @@ export default defineComponent({
       default: () => undefined
     },
     rowKey: propTypes.string.def('id'),
-    emptyText: propTypes.string.def('No Data'),
+    emptyText: propTypes.string.def('暂无数据'),
+    // 表格工具栏缓存唯一标识符
+    activeUID: propTypes.string.def(''),
     defaultExpandAll: propTypes.bool.def(false),
     expandRowKeys: {
       type: Array as PropType<string[]>,
@@ -345,7 +350,7 @@ export default defineComponent({
     const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
       const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps)
       return columnsChildren.map((v) => {
-        if (v.hidden) return null
+        if (v.show === false) return null
         const props = { ...v } as any
         if (props.children) delete props.children
 
@@ -417,7 +422,7 @@ export default defineComponent({
       } = unref(getProps)
 
       return (columnsChildren || columns).map((v) => {
-        if (v.hidden) return null
+        if (v.show === false) return null
         if (v.type === 'index') {
           return (
             <ElTableColumn
@@ -429,6 +434,7 @@ export default defineComponent({
               headerAlign={v.headerAlign || headerAlign}
               label={v.label}
               width="65px"
+              fixed="left"
             ></ElTableColumn>
           )
         } else if (v.type === 'selection') {
@@ -494,6 +500,7 @@ export default defineComponent({
       if (getSlot(slots, 'append')) {
         tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args)
       }
+      const toolbar = getSlot(slots, 'toolbar')
 
       // const { sortable } = unref(getProps)
 
@@ -511,14 +518,31 @@ export default defineComponent({
 
       return (
         <div v-loading={unref(getProps).loading}>
-          {unref(getProps).showAction ? (
-            <TableActions
-              columns={unref(getProps).columns}
-              onChangSize={changSize}
-              onRefresh={refresh}
-            />
-          ) : null}
-          <ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}>
+          <div class="flex justify-between mb-1">
+            <div>{toolbar}</div>
+            <div class="pt-2">
+              {unref(getProps).showAction ? (
+                <TableActions
+                  activeUID={unref(getProps).activeUID}
+                  columns={unref(getProps).columns}
+                  el-table-ref={elTableRef}
+                  onChangSize={changSize}
+                  onRefresh={refresh}
+                />
+              ) : null}
+            </div>
+          </div>
+
+          <ElTable
+            ref={elTableRef}
+            data={unref(getProps).data}
+            {...unref(getBindValue)}
+            header-cell-style={
+              appStore.getIsDark
+                ? { color: '#CFD3DC', 'background-color': '#000' }
+                : { color: '#000', 'background-color': '#f5f7fa' }
+            }
+          >
             {{
               default: () => renderTableColumn(),
               ...tableSlots

+ 234 - 53
src/components/Table/src/components/TableActions.vue

@@ -1,24 +1,34 @@
 <script lang="tsx">
-import { defineComponent, unref, computed, PropType, watch } from 'vue'
+import { defineComponent, unref, computed, PropType, watch, ref, nextTick } from 'vue'
 import {
   ElTooltip,
   ElDropdown,
   ElDropdownMenu,
   ElDropdownItem,
-  ComponentSize
-  // ElPopover,
-  // ElTree
+  ComponentSize,
+  ElPopover,
+  ElCheckbox,
+  ElScrollbar,
+  ElButton,
+  ElTable,
+  ElDivider
 } from 'element-plus'
 import { Icon } from '@/components/Icon'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useAppStore } from '@/store/modules/app'
 import { TableColumn } from '../types'
-import { cloneDeep } from 'lodash-es'
-// import { eachTree } from '@/utils/tree'
+import { VueDraggable } from 'vue-draggable-plus'
+import { useRouter } from 'vue-router'
+import { useStorage } from '@/hooks/web/useStorage'
+import cloneDeep from 'lodash/cloneDeep'
+import { propTypes } from '@/utils/propTypes'
+import { moveElementToIndex } from '@/utils/index'
 
 const appStore = useAppStore()
 const sizeMap = computed(() => appStore.sizeMap)
 
+const { setStorage, getStorage, removeStorage } = useStorage()
+
 const { t } = useI18n()
 
 export default defineComponent({
@@ -27,7 +37,13 @@ export default defineComponent({
     columns: {
       type: Array as PropType<TableColumn[]>,
       default: () => []
-    }
+    },
+    elTableRef: {
+      type: Object as PropType<ComponentRef<typeof ElTable>>,
+      default: () => {}
+    },
+    // 表格工具栏缓存唯一标识符
+    activeUID: propTypes.string.def('')
   },
   emits: ['refresh', 'changSize'],
   setup(props, { emit }) {
@@ -39,25 +55,142 @@ export default defineComponent({
       emit('changSize', size)
     }
 
-    const columns = computed(() => {
-      return cloneDeep(props.columns).filter((v) => {
-        // 去掉type为selection的列和expand的列
-        if (v.type !== 'selection' && v.type !== 'expand') {
-          return v
+    const tableColumns = ref(props.columns)
+    const elTableRef = ref(props.elTableRef)
+    const activeUID = ref(props.activeUID)
+    const numberColumnStatus = ref(false)
+
+    // 获取 table columns 是否已经存在序号列,如果存在则更新初始状态,如果不存在则添加
+    const numberColumnField = tableColumns.value.find((item) => item.type === 'index')
+    if (numberColumnField === undefined) {
+      tableColumns.value.unshift({
+        field: '_serial_number',
+        label: '序号',
+        type: 'index',
+        show: false,
+        disabled: true
+      })
+    } else {
+      numberColumnStatus.value = numberColumnField.show
+    }
+
+    // 备份最初的 table columns
+    const oldTableColumns = cloneDeep(unref(tableColumns))
+
+    const checkAll = ref(false)
+    // 如果为True,则表示为半选状态
+    const isIndeterminate = ref(true)
+
+    // 全选状态改变事件
+    const handleCheckAllChange = (val: boolean) => {
+      tableColumns.value.forEach((item) => {
+        if (item.disabled !== true) {
+          item.show = val
         }
       })
-    })
+      isIndeterminate.value = tableColumns.value
+        .filter((item) => !item.disabled)
+        .some((item) => item.show)
+    }
+
+    // 列选中状态改变事件
+    const handleCheckChange = () => {
+      checkAll.value = tableColumns.value
+        .filter((item) => !item.disabled)
+        .every((item) => item.show)
+      if (checkAll.value) {
+        isIndeterminate.value = false
+      } else {
+        isIndeterminate.value = tableColumns.value
+          .filter((item) => !item.disabled)
+          .some((item) => item.show)
+      }
+    }
+
+    // 更新 table columns 中的序号列状态
+    const updateNumberColumnStatus = (syns: boolean, status: boolean = false) => {
+      const numberColumnField = tableColumns.value.find((item) => item.type === 'index')
+      if (numberColumnField) {
+        if (syns) {
+          numberColumnStatus.value = numberColumnField.show
+        } else {
+          numberColumnField.show = status
+        }
+      }
+    }
+
+    const { currentRoute } = useRouter()
+    const fullPath = currentRoute.value.fullPath
+    const cacheTableHeadersKey = `${fullPath}_${activeUID.value}`
+
+    if (cacheTableHeadersKey) {
+      // 获取缓存中的 table columns 状态并覆盖到当前的 table columns 中
+      const cacheData = JSON.parse(getStorage(cacheTableHeadersKey))
+      if (cacheData) {
+        tableColumns.value.forEach((item) => {
+          const fieldData = cacheData[item.field]
+          item._index = fieldData.index
+          item.show = fieldData.show
+          item.fixed = fieldData.fixed
+        })
+        tableColumns.value.sort((a, b) => a._index - b._index)
+        updateNumberColumnStatus(true)
+      }
+    }
 
     watch(
-      () => columns.value,
-      (newColumns) => {
-        console.log('columns change:', newColumns)
+      () => tableColumns.value,
+      async (val) => {
+        const cacheData = {}
+        for (let i = 0; i < val.length; i++) {
+          const item = val[i]
+          cacheData[item.field] = {
+            show: item.show,
+            index: i,
+            fixed: item.fixed
+          }
+        }
+        setStorage(cacheTableHeadersKey, JSON.stringify(cacheData))
+        handleCheckChange()
+        await nextTick()
+        elTableRef.value?.doLayout()
       },
       {
         deep: true
       }
     )
 
+    watch(
+      () => numberColumnStatus.value,
+      async (val) => {
+        updateNumberColumnStatus(false, val)
+        await nextTick()
+        elTableRef.value?.doLayout()
+      },
+      {
+        deep: true
+      }
+    )
+
+    // 重置所有状态
+    const resetTableColumns = async () => {
+      Object.assign(tableColumns.value, cloneDeep(oldTableColumns))
+      updateNumberColumnStatus(true)
+      await nextTick()
+      // 删除缓存
+      removeStorage(cacheTableHeadersKey)
+    }
+
+    // 更新元素顺序
+    const updateColumnsIndex = (val) => {
+      Object.assign(
+        tableColumns.value,
+        cloneDeep(moveElementToIndex(tableColumns.value, val.oldIndex, val.newIndex))
+      )
+    }
+
+    handleCheckChange()
+
     return () => (
       <>
         <div class="text-right h-28px flex items-center justify-end">
@@ -71,7 +204,7 @@ export default defineComponent({
             </span>
           </ElTooltip>
 
-          <ElTooltip content={t('common.size')} placement="top">
+          <ElTooltip content={t('common.density')} placement="top">
             <ElDropdown trigger="click" onCommand={changSize}>
               {{
                 default: () => {
@@ -106,43 +239,91 @@ export default defineComponent({
             </ElDropdown>
           </ElTooltip>
 
-          {/* <ElTooltip content={t('common.columnSetting')} placement="top"> */}
-          {/* <ElPopover trigger="click" placement="left">
-            {{
-              default: () => {
-                return (
-                  <div>
-                    <ElTree
-                      data={unref(columns)}
-                      show-checkbox
-                      default-checked-keys={unref(defaultCheckeds)}
-                      draggable
-                      node-key="field"
-                      allow-drop={(_draggingNode: any, _dropNode: any, type: string) => {
-                        if (type === 'inner') {
-                          return false
-                        } else {
-                          return true
-                        }
-                      }}
-                      onNode-drag-end={onNodeDragEnd}
-                      onCheck-change={onCheckChange}
+          <ElTooltip content={t('common.columnSetting')} placement="top">
+            <ElPopover trigger="click" placement="bottom" width="300px">
+              {{
+                default: () => {
+                  return (
+                    <div>
+                      <div style="border-bottom: 1px solid #d4d7de" class="flex justify-between">
+                        <div>
+                          <ElCheckbox
+                            v-model={checkAll.value}
+                            indeterminate={isIndeterminate.value}
+                            onChange={handleCheckAllChange}
+                          >
+                            {t('common.selectAll')}
+                          </ElCheckbox>
+                          <ElCheckbox v-model={numberColumnStatus.value}>
+                            {t('common.SerialNumberColumn')}
+                          </ElCheckbox>
+                        </div>
+                        <ElButton type="primary" link onClick={resetTableColumns}>
+                          {t('common.reset')}
+                        </ElButton>
+                      </div>
+                      <ElScrollbar max-height="400px">
+                        <VueDraggable
+                          modelValue={tableColumns.value}
+                          onEnd={updateColumnsIndex}
+                          handle=".cursor-move"
+                        >
+                          {tableColumns.value.map((element) => {
+                            if (element.type === 'index') return null
+                            return (
+                              <div class="flex justify-between">
+                                <div>
+                                  <span class="cursor-move mr-10px">
+                                    <Icon icon="akar-icons:drag-vertical" />
+                                  </span>
+                                  <ElCheckbox
+                                    v-model={element.show}
+                                    disabled={element.disabled === true}
+                                    onChange={handleCheckChange}
+                                  >
+                                    {element.label}
+                                  </ElCheckbox>
+                                </div>
+                                <div class="mt-7px mr-9px">
+                                  <span
+                                    class={element.fixed === 'left' ? 'color-[#409eff]' : ''}
+                                    onClick={() => {
+                                      element.fixed = element.fixed === 'left' ? undefined : 'left'
+                                    }}
+                                  >
+                                    <Icon icon="radix-icons:pin-left" class="cursor-pointer" />
+                                  </span>
+                                  <ElDivider direction="vertical" />
+                                  <span
+                                    class={element.fixed === 'right' ? 'color-[#409eff]' : ''}
+                                    onClick={() => {
+                                      element.fixed =
+                                        element.fixed === 'right' ? undefined : 'right'
+                                    }}
+                                  >
+                                    <Icon icon="radix-icons:pin-right" class="cursor-pointer" />
+                                  </span>
+                                </div>
+                              </div>
+                            )
+                          })}
+                        </VueDraggable>
+                      </ElScrollbar>
+                    </div>
+                  )
+                },
+                reference: () => {
+                  return (
+                    <Icon
+                      icon="ant-design:setting-outlined"
+                      class="cursor-pointer"
+                      hoverColor="var(--el-color-primary)"
                     />
-                  </div>
-                )
-              },
-              reference: () => {
-                return (
-                  <Icon
-                    icon="ant-design:setting-outlined"
-                    class="cursor-pointer"
-                    hoverColor="var(--el-color-primary)"
-                  />
-                )
-              }
-            }}
-          </ElPopover> */}
-          {/* </ElTooltip> */}
+                  )
+                }
+              }}
+            </ElPopover>
+          </ElTooltip>
         </div>
       </>
     )

+ 2 - 2
src/components/Table/src/types/index.ts

@@ -4,9 +4,9 @@ export interface TableColumn {
   label?: string
   type?: string
   /**
-   * 是否隐藏
+   * 是否显示,必填
    */
-  hidden?: boolean
+  show: boolean
   children?: TableColumn[]
   slots?: {
     default?: (...args: any[]) => JSX.Element | JSX.Element[] | null

+ 15 - 2
src/locales/en.ts

@@ -44,11 +44,14 @@ export default {
     refresh: 'Refresh',
     fullscreen: 'Fullscreen',
     size: 'Size',
+    density: 'Density',
     columnSetting: 'Column setting',
     lengthRange: 'The length should be between {min} and {max}',
     notSpace: 'Spaces are not allowed',
     notSpecialCharacters: 'Special characters are not allowed',
-    isEqual: 'The two are not equal'
+    isEqual: 'The two are not equal',
+    selectAll: 'Select all',
+    SerialNumberColumn: 'Index column'
   },
   lock: {
     lockScreen: 'Lock screen',
@@ -108,18 +111,27 @@ export default {
     welcome: 'Welcome to the system',
     message: 'Backstage management system',
     username: 'Username',
+    telephone: 'Telephone',
     password: 'Password',
     register: 'Register',
     checkPassword: 'Confirm password',
     login: 'Sign in',
+    passwordLogin: 'Password login',
+    smsLogin: 'SMS code login',
     otherLogin: 'Sign in with',
     remember: 'Remember me',
     hasUser: 'Existing account? Go to login',
     forgetPassword: 'Forget password',
     usernamePlaceholder: 'Please input username',
+    telephonePlaceholder: 'Please input telephone',
     passwordPlaceholder: 'Please input password',
     code: 'Verification code',
-    codePlaceholder: 'Please input verification code'
+    getCode: 'Get code',
+    codePlaceholder: 'Please input verification code',
+    SMSCode: 'sms code',
+    getSMSCode: 'get sms code',
+    SMSCodePlaceholder: 'Please input sms code',
+    SMSCodeRetry: 'S retry'
   },
   router: {
     login: 'Login',
@@ -442,6 +454,7 @@ export default {
     changeTitle: 'Change title',
     header: 'Header',
     selectAllNone: 'Select all / none',
+    selectAll: 'Select all',
     delOrAddAction: 'Delete or add action',
     showOrHiddenStripe: 'Show or hidden stripe',
     showOrHiddenBorder: 'Show or hidden border',

+ 15 - 2
src/locales/zh-CN.ts

@@ -44,11 +44,14 @@ export default {
     refresh: '刷新',
     fullscreen: '全屏',
     size: '尺寸',
+    density: '密度',
     columnSetting: '列设置',
     lengthRange: '长度在 {min} 到 {max} 个字符',
     notSpace: '不能包含空格',
     notSpecialCharacters: '不能包含特殊字符',
-    isEqual: '两次输入不一致'
+    isEqual: '两次输入不一致',
+    selectAll: '全选',
+    SerialNumberColumn: '序号列'
   },
   lock: {
     lockScreen: '锁定屏幕',
@@ -107,18 +110,27 @@ export default {
     welcome: '欢迎使用本系统',
     message: '开箱即用的中后台管理系统',
     username: '用户名',
+    telephone: '手机号',
     password: '密码',
     register: '注册',
     checkPassword: '确认密码',
     login: '登录',
+    passwordLogin: '密码登录',
+    smsLogin: '短信验证码登录',
     otherLogin: '其它登录方式',
     remember: '记住我',
     hasUser: '已有账号?去登录',
     forgetPassword: '忘记密码',
     usernamePlaceholder: '请输入用户名',
+    telephonePlaceholder: '请输入手机号',
     passwordPlaceholder: '请输入密码',
     code: '验证码',
-    codePlaceholder: '请输入验证码'
+    getCode: '获取验证码',
+    codePlaceholder: '请输入验证码',
+    SMSCode: '短信验证码',
+    getSMSCode: '获取短信验证码',
+    SMSCodePlaceholder: '请输入短信验证码',
+    SMSCodeRetry: 'S后重新'
   },
   router: {
     login: '登录',
@@ -435,6 +447,7 @@ export default {
     changeTitle: '修改标题',
     header: '头部',
     selectAllNone: '全选/全不选',
+    selectAll: '全选',
     delOrAddAction: '删除/添加操作列',
     showOrHiddenStripe: '显示/隐藏斑马纹',
     showOrHiddenBorder: '显示/隐藏边框',

+ 142 - 0
src/utils/index.ts

@@ -122,3 +122,145 @@ export function toAnyString() {
 export function firstUpperCase(str: string) {
   return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
 }
+
+// 根据当前时间获取祝福语
+export const getGreeting = (): string => {
+  const now = new Date()
+  const hour = now.getHours()
+
+  if (hour >= 6 && hour < 10) {
+    return '早上好'
+  } else if (hour >= 10 && hour < 13) {
+    return '中午好'
+  } else if (hour >= 13 && hour < 18) {
+    return '下午好'
+  } else {
+    return '晚上好'
+  }
+}
+
+// 获取当前星期几
+export const getDayOfWeek = (): string => {
+  const daysOfWeek: string[] = [
+    '星期日',
+    '星期一',
+    '星期二',
+    '星期三',
+    '星期四',
+    '星期五',
+    '星期六'
+  ]
+  const date: Date = new Date()
+  const dayOfWeekIndex: number = date.getDay()
+  return daysOfWeek[dayOfWeekIndex]
+}
+
+// 数字转金额
+// 作者:时光足迹
+// 链接:https://juejin.cn/post/7028086399601475591
+// 来源:稀土掘金
+export const formatMoney = (amount, currency = true): string => {
+  const formatter = new Intl.NumberFormat('zh-CN', {
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2,
+    useGrouping: true
+  })
+
+  const formattedAmount = formatter.format(amount)
+
+  if (currency) {
+    return `¥${formattedAmount}`
+  }
+
+  return formattedAmount
+}
+
+/**
+ * 小数转折扣
+ * 例子:0.85 -> 8.5折
+ * 例子:0.5 -> 5折
+ */
+export const convertToDiscount = (decimal: number | undefined): string => {
+  if (decimal === undefined) {
+    return ''
+  }
+  const discount = decimal * 10
+  if (discount === 10) {
+    return '无折扣'
+  }
+  return discount % 1 === 0 ? `${discount}折` : `${discount.toFixed(1)}折`
+}
+
+/**
+ * 获取当前时间
+ * 返回:yyyy-MM-dd HH:mm:ss
+ */
+export const getCurrentDateTime = (): string => {
+  const now: Date = new Date()
+
+  const year: number = now.getFullYear()
+  const month: number = now.getMonth() + 1
+  const day: number = now.getDate()
+  const hours: number = now.getHours()
+  const minutes: number = now.getMinutes()
+  const seconds: number = now.getSeconds()
+
+  // 格式化为字符串
+  const formattedDateTime = `${year}-${padZero(month)}-${padZero(day)} ${padZero(hours)}:${padZero(
+    minutes
+  )}:${padZero(seconds)}`
+
+  return formattedDateTime
+}
+
+/**
+ * 获取当前日期
+ * 返回:yyyy-MM-dd HH:mm:ss
+ */
+export const getCurrentDate = (): string => {
+  const now: Date = new Date()
+
+  const year: number = now.getFullYear()
+  const month: number = now.getMonth() + 1
+  const day: number = now.getDate()
+
+  // 格式化为字符串
+  const formattedDate = `${year}-${padZero(month)}-${padZero(day)}`
+
+  return formattedDate
+}
+
+// 辅助函数:在数字小于10时,在前面补零
+export const padZero = (num: number): string => {
+  return num < 10 ? `0${num}` : `${num}`
+}
+
+// 将base64编码的字符串转换为文件
+export const base64ToFile = (dataURI, filename): File => {
+  const arr = dataURI.split(',')
+  const mime = arr[0].match(/:(.*?);/)[1]
+  const bstr = atob(arr[1])
+  let n = bstr.length
+  const u8arr = new Uint8Array(n)
+  while (n--) {
+    u8arr[n] = bstr.charCodeAt(n)
+  }
+  return new File([u8arr], filename, { type: mime })
+}
+
+// 将指定索引的元素移动到目标索引的函数
+export const moveElementToIndex = (array: any[], fromIndex: number, toIndex: number) => {
+  const clonedArray = [...array] // 克隆数组以避免修改原始数组
+
+  if (
+    fromIndex >= 0 &&
+    fromIndex < clonedArray.length &&
+    toIndex >= 0 &&
+    toIndex < clonedArray.length
+  ) {
+    const [element] = clonedArray.splice(fromIndex, 1) // 移除指定索引的元素
+    clonedArray.splice(toIndex, 0, element) // 将元素插入目标索引位置
+  }
+
+  return clonedArray
+}