horse soft 基础框架前端界面
autumnal_wind@yeah.net
2024-05-27 387083ef40c31fed2f7f5588e8d9048ff97bdf38
feat: 参数设置改造(弹出框)
31个文件已添加
5个文件已修改
3984 ■■■■■ 已修改文件
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/global.module.scss 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.scss 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/src/Dialog.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Icon/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Icon/src/Icon.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Icon/src/IconSelect.vue 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Icon/src/data.ts 1961 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/event/useScrollTo.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useCache.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useConfigGlobal.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useCrudSchemas.ts 326 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useDesign.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useEmitt.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useForm.ts 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useGuide.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useI18n.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useIcon.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useLocale.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useMessage.ts 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useNProgress.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useNetwork.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useNow.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/usePageLoading.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useTable.ts 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useTagsView.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useTimeAgo.ts 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useTitle.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useValidator.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/hooks/web/useWatermark.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/is.ts 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basis/sysParam/config/DetailForm.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basis/sysParam/config/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -54,6 +54,7 @@
    "@unocss/preset-uno": "^0.50.8",
    "@vitejs/plugin-vue": "5.0.4",
    "@vue/compiler-sfc": "3.2.45",
    "@purge-icons/generated": "^0.9.0",
    "autoprefixer": "10.4.14",
    "eslint": "8.36.0",
    "eslint-config-prettier": "8.8.0",
src/assets/styles/global.module.scss
New file
@@ -0,0 +1,6 @@
@import './variables.scss';
// 导出变量
:export {
  namespace: $namespace;
  elNamespace: $elNamespace;
}
src/assets/styles/index.scss
@@ -7,6 +7,7 @@
@import './ruoyi.scss';
@import 'animate.css';
@import 'element-plus/dist/index.css';
@import "./variables.scss";
body {
  height: 100%;
src/assets/styles/variables.scss
New file
@@ -0,0 +1,4 @@
// 命名空间
$namespace: v;
// el命名空间
$elNamespace: el;
src/components/Dialog/index.ts
New file
@@ -0,0 +1,3 @@
import Dialog from './src/Dialog.vue'
export { Dialog }
src/components/Dialog/src/Dialog.vue
New file
@@ -0,0 +1,140 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { isNumber } from '@/utils/is'
defineOptions({ name: 'Dialog' })
const slots = useSlots()
const props = defineProps({
  modelValue: propTypes.bool.def(false),
  title: propTypes.string.def('Dialog'),
  fullscreen: propTypes.bool.def(true),
  width: propTypes.oneOfType([String, Number]).def('40%'),
  scroll: propTypes.bool.def(false), // 是否开启滚动条。如果是的话,按照 maxHeight 设置最大高度
  maxHeight: propTypes.oneOfType([String, Number]).def('400px')
})
const getBindValue = computed(() => {
  const delArr: string[] = ['fullscreen', 'title', 'maxHeight', 'appendToBody']
  const attrs = useAttrs()
  const obj = { ...attrs, ...props }
  for (const key in obj) {
    if (delArr.indexOf(key) !== -1) {
      delete obj[key]
    }
  }
  return obj
})
const isFullscreen = ref(false)
const toggleFull = () => {
  isFullscreen.value = !unref(isFullscreen)
}
const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
watch(
  () => isFullscreen.value,
  async (val: boolean) => {
    await nextTick()
    if (val) {
      const windowHeight = document.documentElement.offsetHeight
      dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
    } else {
      dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
    }
  },
  {
    immediate: true
  }
)
const dialogStyle = computed(() => {
  return {
    height: unref(dialogHeight)
  }
})
</script>
<template>
  <ElDialog
    v-bind="getBindValue"
    :close-on-click-modal="true"
    :fullscreen="isFullscreen"
    :width="width"
    destroy-on-close
    lock-scroll
    draggable
    class="com-dialog"
    :show-close="false"
  >
    <template #header="{ close }">
      <div class="relative h-54px flex items-center justify-between pl-15px pr-15px">
        <slot name="title">
          {{ title }}
        </slot>
        <div
          class="absolute right-15px top-[50%] h-54px flex translate-y-[-50%] items-center justify-between"
        >
          <Icon
            v-if="fullscreen"
            class="is-hover mr-10px cursor-pointer"
            :icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
            color="var(--el-color-info)"
            hover-color="var(--el-color-primary)"
            @click="toggleFull"
          />
          <Icon
            class="is-hover cursor-pointer"
            icon="ep:close"
            hover-color="var(--el-color-primary)"
            color="var(--el-color-info)"
            @click="close"
          />
        </div>
      </div>
    </template>
    <ElScrollbar v-if="scroll" :style="dialogStyle">
      <slot></slot>
    </ElScrollbar>
    <slot v-else></slot>
    <template v-if="slots.footer" #footer>
      <slot name="footer"></slot>
    </template>
  </ElDialog>
</template>
<style lang="scss">
.com-dialog {
  .el-overlay-dialog {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .el-dialog {
    margin: 0 !important;
    &__header {
      height: 54px;
      padding: 0;
      margin-right: 0 !important;
      border-bottom: 1px solid var(--el-border-color);
    }
    &__body {
      padding: 15px !important;
    }
    &__footer {
      border-top: 1px solid var(--el-border-color);
    }
    &__headerbtn {
      top: 0;
    }
  }
}
</style>
src/components/Icon/index.ts
New file
@@ -0,0 +1,4 @@
import Icon from './src/Icon.vue'
import IconSelect from './src/IconSelect.vue'
export { Icon, IconSelect }
src/components/Icon/src/Icon.vue
New file
@@ -0,0 +1,86 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import Iconify from '@purge-icons/generated'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'Icon' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('icon')
const props = defineProps({
  // icon name
  icon: propTypes.string,
  // icon color
  color: propTypes.string,
  // icon size
  size: propTypes.number.def(16),
  // icon svg class
  svgClass: propTypes.string.def('')
})
const elRef = ref<ElRef>(null)
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
const symbolId = computed(() => {
  return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
})
const getIconifyStyle = computed(() => {
  const { color, size } = props
  return {
    fontSize: `${size}px`,
    height: '1em',
    color
  }
})
const getSvgClass = computed(() => {
  const { svgClass } = props
  return `iconify ${svgClass}`
})
const updateIcon = async (icon: string) => {
  if (unref(isLocal)) return
  const el = unref(elRef)
  if (!el) return
  await nextTick()
  if (!icon) return
  const svg = Iconify.renderSVG(icon, {})
  if (svg) {
    el.textContent = ''
    el.appendChild(svg)
  } else {
    const span = document.createElement('span')
    span.className = 'iconify'
    span.dataset.icon = icon
    el.textContent = ''
    el.appendChild(span)
  }
}
watch(
  () => props.icon,
  (icon: string) => {
    updateIcon(icon)
  }
)
</script>
<template>
  <ElIcon :class="prefixCls" :color="color" :size="size">
    <svg v-if="isLocal" :class="getSvgClass" aria-hidden="true">
      <use :xlink:href="symbolId" />
    </svg>
    <span v-else ref="elRef" :class="$attrs.class" :style="getIconifyStyle">
      <span :class="getSvgClass" :data-icon="symbolId"></span>
    </span>
  </ElIcon>
</template>
src/components/Icon/src/IconSelect.vue
New file
@@ -0,0 +1,229 @@
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import { cloneDeep } from 'lodash-es'
import { IconJson } from '@/components/Icon/src/data'
defineOptions({ name: 'IconSelect' })
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined
const props = defineProps({
  modelValue: {
    require: false,
    type: String
  }
})
const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>()
const visible = ref(false)
const inputValue = toRef(props, 'modelValue')
const iconList = ref(IconJson)
const icon = ref('add-location')
const currentActiveType = ref('ep:')
// 深拷贝图标数据,前端做搜索
const copyIconList = cloneDeep(iconList.value)
const pageSize = ref(96)
const currentPage = ref(1)
// 搜索条件
const filterValue = ref('')
const tabsList = [
  {
    label: 'Element Plus',
    name: 'ep:'
  },
  {
    label: 'Font Awesome 4',
    name: 'fa:'
  },
  {
    label: 'Font Awesome 5 Solid',
    name: 'fa-solid:'
  }
]
const pageList = computed(() => {
  if (currentPage.value === 1) {
    return copyIconList[currentActiveType.value]
      ?.filter((v) => v.includes(filterValue.value))
      .slice(currentPage.value - 1, pageSize.value)
  } else {
    return copyIconList[currentActiveType.value]
      ?.filter((v) => v.includes(filterValue.value))
      .slice(
        pageSize.value * (currentPage.value - 1),
        pageSize.value * (currentPage.value - 1) + pageSize.value
      )
  }
})
const iconCount = computed(() => {
  return copyIconList[currentActiveType.value] == undefined
    ? 0
    : copyIconList[currentActiveType.value].length
})
const iconItemStyle = computed((): ParameterCSSProperties => {
  return (item) => {
    if (inputValue.value === currentActiveType.value + item) {
      return {
        borderColor: 'var(--el-color-primary)',
        color: 'var(--el-color-primary)'
      }
    }
  }
})
function handleClick({ props }) {
  currentPage.value = 1
  currentActiveType.value = props.name
  emit('update:modelValue', currentActiveType.value + iconList.value[currentActiveType.value][0])
  icon.value = iconList.value[currentActiveType.value][0]
}
function onChangeIcon(item) {
  icon.value = item
  emit('update:modelValue', currentActiveType.value + item)
  visible.value = false
}
function onCurrentChange(page) {
  currentPage.value = page
}
watch(
  () => {
    return props.modelValue
  },
  () => {
    if (props.modelValue && props.modelValue.indexOf(':') >= 0) {
      currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1)
      icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1)
    }
  }
)
watch(
  () => {
    return filterValue.value
  },
  () => {
    currentPage.value = 1
  }
)
</script>
<template>
  <div class="selector">
    <ElInput v-model="inputValue" @click="visible = !visible">
      <template #append>
        <ElPopover
          :popper-options="{
            placement: 'auto'
          }"
          :visible="visible"
          :width="350"
          popper-class="pure-popper"
          trigger="click"
        >
          <template #reference>
            <div
              class="h-32px w-40px flex cursor-pointer items-center justify-center"
              @click="visible = !visible"
            >
              <Icon :icon="currentActiveType + icon" />
            </div>
          </template>
          <ElInput v-model="filterValue" class="p-2" clearable placeholder="搜索图标" />
          <ElDivider border-style="dashed" />
          <ElTabs v-model="currentActiveType" @tab-click="handleClick">
            <ElTabPane
              v-for="(pane, index) in tabsList"
              :key="index"
              :label="pane.label"
              :name="pane.name"
            >
              <ElDivider border-style="dashed" class="tab-divider" />
              <ElScrollbar height="220px">
                <ul class="ml-2 flex flex-wrap px-2">
                  <li
                    v-for="(item, key) in pageList"
                    :key="key"
                    :style="iconItemStyle(item)"
                    :title="item"
                    class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
                    @click="onChangeIcon(item)"
                  >
                    <Icon :icon="currentActiveType + item" />
                  </li>
                </ul>
              </ElScrollbar>
            </ElTabPane>
          </ElTabs>
          <ElDivider border-style="dashed" />
          <ElPagination
            :current-page="currentPage"
            :page-size="pageSize"
            :total="iconCount"
            background
            class="h-10 flex items-center justify-center"
            layout="prev, pager, next"
            small
            @current-change="onCurrentChange"
          />
        </ElPopover>
      </template>
    </ElInput>
  </div>
</template>
<style lang="scss" scoped>
.el-divider--horizontal {
  margin: 1px auto !important;
}
.tab-divider.el-divider--horizontal {
  margin: 0 !important;
}
.icon-item {
  &:hover {
    color: var(--el-color-primary);
    border-color: var(--el-color-primary);
    transform: scaleX(1.05);
    transition: all 0.4s;
  }
}
:deep(.el-tabs__nav-next) {
  font-size: 15px;
  line-height: 32px;
  box-shadow: -5px 0 5px -6px #ccc;
}
:deep(.el-tabs__nav-prev) {
  font-size: 15px;
  line-height: 32px;
  box-shadow: 5px 0 5px -6px #ccc;
}
:deep(.el-input-group__append) {
  padding: 0;
}
:deep(.el-tabs__item) {
  height: 30px;
  font-size: 12px;
  font-weight: normal;
  line-height: 30px;
}
:deep(.el-tabs__header),
:deep(.el-tabs__nav-wrap) {
  position: static;
  margin: 0;
}
</style>
src/components/Icon/src/data.ts
New file
@@ -0,0 +1,1961 @@
export const IconJson = {
  'ep:': [
    'add-location',
    'aim',
    'alarm-clock',
    'apple',
    'arrow-down',
    'arrow-down-bold',
    'arrow-left',
    'arrow-left-bold',
    'arrow-right',
    'arrow-right-bold',
    'arrow-up',
    'arrow-up-bold',
    'avatar',
    'back',
    'baseball',
    'basketball',
    'bell',
    'bell-filled',
    'bicycle',
    'bottom',
    'bottom-left',
    'bottom-right',
    'bowl',
    'box',
    'briefcase',
    'brush',
    'brush-filled',
    'burger',
    'calendar',
    'camera',
    'camera-filled',
    'caret-bottom',
    'caret-left',
    'caret-right',
    'caret-top',
    'cellphone',
    'chat-dot-round',
    'chat-dot-square',
    'chat-line-round',
    'chat-line-square',
    'chat-round',
    'chat-square',
    'check',
    'checked',
    'cherry',
    'chicken',
    'circle-check',
    'circle-check-filled',
    'circle-close',
    'circle-close-filled',
    'circle-plus',
    'circle-plus-filled',
    'clock',
    'close',
    'close-bold',
    'cloudy',
    'coffee',
    'coffee-cup',
    'coin',
    'cold-drink',
    'collection',
    'collection-tag',
    'comment',
    'compass',
    'connection',
    'coordinate',
    'copy-document',
    'cpu',
    'credit-card',
    'crop',
    'd-arrow-left',
    'd-arrow-right',
    'd-caret',
    'data-analysis',
    'data-board',
    'data-line',
    'delete',
    'delete-filled',
    'delete-location',
    'dessert',
    'discount',
    'dish',
    'dish-dot',
    'document',
    'document-add',
    'document-checked',
    'document-copy',
    'document-delete',
    'document-remove',
    'download',
    'drizzling',
    'edit',
    'edit-pen',
    'eleme',
    'eleme-filled',
    'expand',
    'failed',
    'female',
    'files',
    'film',
    'filter',
    'finished',
    'first-aid-kit',
    'flag',
    'fold',
    'folder',
    'folder-add',
    'folder-checked',
    'folder-delete',
    'folder-opened',
    'folder-remove',
    'food',
    'football',
    'fork-spoon',
    'fries',
    'full-screen',
    'goblet',
    'goblet-full',
    'goblet-square',
    'goblet-square-full',
    'goods',
    'goods-filled',
    'grape',
    'grid',
    'guide',
    'headset',
    'help',
    'help-filled',
    'histogram',
    'home-filled',
    'hot-water',
    'house',
    'ice-cream',
    'ice-cream-round',
    'ice-cream-square',
    'ice-drink',
    'ice-tea',
    'info-filled',
    'iphone',
    'key',
    'knife-fork',
    'lightning',
    'link',
    'list',
    'loading',
    'location',
    'location-filled',
    'location-information',
    'lock',
    'lollipop',
    'magic-stick',
    'magnet',
    'male',
    'management',
    'map-location',
    'medal',
    'menu',
    'message',
    'message-box',
    'mic',
    'microphone',
    'milk-tea',
    'minus',
    'money',
    'monitor',
    'moon',
    'moon-night',
    'more',
    'more-filled',
    'mostly-cloudy',
    'mouse',
    'mug',
    'mute',
    'mute-notification',
    'no-smoking',
    'notebook',
    'notification',
    'odometer',
    'office-building',
    'open',
    'operation',
    'opportunity',
    'orange',
    'paperclip',
    'partly-cloudy',
    'pear',
    'phone',
    'phone-filled',
    'picture',
    'picture-filled',
    'picture-rounded',
    'pie-chart',
    'place',
    'platform',
    'plus',
    'pointer',
    'position',
    'postcard',
    'pouring',
    'present',
    'price-tag',
    'printer',
    'promotion',
    'question-filled',
    'rank',
    'reading',
    'reading-lamp',
    'refresh',
    'refresh-left',
    'refresh-right',
    'refrigerator',
    'remove',
    'remove-filled',
    'right',
    'scale-to-original',
    'school',
    'scissor',
    'search',
    'select',
    'sell',
    'semi-select',
    'service',
    'set-up',
    'setting',
    'share',
    'ship',
    'shop',
    'shopping-bag',
    'shopping-cart',
    'shopping-cart-full',
    'smoking',
    'soccer',
    'sold-out',
    'sort',
    'sort-down',
    'sort-up',
    'stamp',
    'star',
    'star-filled',
    'stopwatch',
    'success-filled',
    'sugar',
    'suitcase',
    'sunny',
    'sunrise',
    'sunset',
    'switch',
    'switch-button',
    'takeaway-box',
    'ticket',
    'tickets',
    'timer',
    'toilet-paper',
    'tools',
    'top',
    'top-left',
    'top-right',
    'trend-charts',
    'trophy',
    'turn-off',
    'umbrella',
    'unlock',
    'upload',
    'upload-filled',
    'user',
    'user-filled',
    'van',
    'video-camera',
    'video-camera-filled',
    'video-pause',
    'video-play',
    'view',
    'wallet',
    'wallet-filled',
    'warning',
    'warning-filled',
    'watch',
    'watermelon',
    'wind-power',
    'zoom-in',
    'zoom-out'
  ],
  'fa:': [
    '500px',
    'address-book',
    'address-book-o',
    'address-card',
    'address-card-o',
    'adjust',
    'adn',
    'align-center',
    'align-justify',
    'align-left',
    'amazon',
    'ambulance',
    'american-sign-language-interpreting',
    'anchor',
    'android',
    'angellist',
    'angle-double-left',
    'angle-double-up',
    'angle-down',
    'angle-left',
    'angle-up',
    'apple',
    'archive',
    'area-chart',
    'arrow-circle-left',
    'arrow-circle-o-left',
    'arrow-circle-o-up',
    'arrow-circle-up',
    'arrow-left',
    'arrow-up',
    'arrows',
    'arrows-alt',
    'arrows-h',
    'arrows-v',
    'assistive-listening-systems',
    'asterisk',
    'at',
    'audio-description',
    'automobile',
    'backward',
    'balance-scale',
    'ban',
    'bandcamp',
    'bank',
    'bar-chart',
    'barcode',
    'bars',
    'bath',
    'battery',
    'battery-0',
    'battery-1',
    'battery-2',
    'battery-3',
    'bed',
    'beer',
    'behance',
    'behance-square',
    'bell',
    'bell-o',
    'bell-slash',
    'bell-slash-o',
    'bicycle',
    'binoculars',
    'birthday-cake',
    'bitbucket',
    'bitbucket-square',
    'bitcoin',
    'black-tie',
    'blind',
    'bluetooth',
    'bluetooth-b',
    'bold',
    'bolt',
    'bomb',
    'book',
    'bookmark',
    'bookmark-o',
    'braille',
    'briefcase',
    'bug',
    'building',
    'building-o',
    'bullhorn',
    'bullseye',
    'bus',
    'buysellads',
    'cab',
    'calculator',
    'calendar',
    'calendar-check-o',
    'calendar-minus-o',
    'calendar-o',
    'calendar-plus-o',
    'calendar-times-o',
    'camera',
    'camera-retro',
    'caret-down',
    'caret-left',
    'caret-square-o-left',
    'caret-square-o-up',
    'caret-up',
    'cart-arrow-down',
    'cart-plus',
    'cc',
    'cc-amex',
    'cc-diners-club',
    'cc-discover',
    'cc-jcb',
    'cc-mastercard',
    'cc-paypal',
    'cc-stripe',
    'cc-visa',
    'certificate',
    'chain',
    'chain-broken',
    'check',
    'check-circle',
    'check-circle-o',
    'check-square',
    'check-square-o',
    'chevron-circle-left',
    'chevron-circle-up',
    'chevron-down',
    'chevron-left',
    'chevron-up',
    'child',
    'chrome',
    'circle',
    'circle-o',
    'circle-o-notch',
    'circle-thin',
    'clipboard',
    'clock-o',
    'clone',
    'close',
    'cloud',
    'cloud-download',
    'cloud-upload',
    'cny',
    'code',
    'code-fork',
    'codepen',
    'codiepie',
    'coffee',
    'cog',
    'cogs',
    'columns',
    'comment',
    'comment-o',
    'commenting',
    'commenting-o',
    'comments',
    'comments-o',
    'compass',
    'compress',
    'connectdevelop',
    'contao',
    'copy',
    'copyright',
    'creative-commons',
    'credit-card',
    'credit-card-alt',
    'crop',
    'crosshairs',
    'css3',
    'cube',
    'cubes',
    'cut',
    'cutlery',
    'dashboard',
    'dashcube',
    'database',
    'deaf',
    'dedent',
    'delicious',
    'desktop',
    'deviantart',
    'diamond',
    'digg',
    'dollar',
    'dot-circle-o',
    'download',
    'dribbble',
    'drivers-license',
    'drivers-license-o',
    'dropbox',
    'drupal',
    'edge',
    'edit',
    'eercast',
    'eject',
    'ellipsis-h',
    'ellipsis-v',
    'empire',
    'envelope',
    'envelope-o',
    'envelope-open',
    'envelope-open-o',
    'envelope-square',
    'envira',
    'eraser',
    'etsy',
    'eur',
    'exchange',
    'exclamation',
    'exclamation-circle',
    'exclamation-triangle',
    'expand',
    'expeditedssl',
    'external-link',
    'external-link-square',
    'eye',
    'eye-slash',
    'eyedropper',
    'fa',
    'facebook',
    'facebook-official',
    'facebook-square',
    'fast-backward',
    'fax',
    'feed',
    'female',
    'fighter-jet',
    'file',
    'file-archive-o',
    'file-audio-o',
    'file-code-o',
    'file-excel-o',
    'file-image-o',
    'file-movie-o',
    'file-o',
    'file-pdf-o',
    'file-powerpoint-o',
    'file-text',
    'file-text-o',
    'file-word-o',
    'film',
    'filter',
    'fire',
    'fire-extinguisher',
    'firefox',
    'first-order',
    'flag',
    'flag-checkered',
    'flag-o',
    'flask',
    'flickr',
    'floppy-o',
    'folder',
    'folder-o',
    'folder-open',
    'folder-open-o',
    'font',
    'fonticons',
    'fort-awesome',
    'forumbee',
    'foursquare',
    'free-code-camp',
    'frown-o',
    'futbol-o',
    'gamepad',
    'gavel',
    'gbp',
    'genderless',
    'get-pocket',
    'gg',
    'gg-circle',
    'gift',
    'git',
    'git-square',
    'github',
    'github-alt',
    'github-square',
    'gitlab',
    'gittip',
    'glass',
    'glide',
    'glide-g',
    'globe',
    'google',
    'google-plus',
    'google-plus-circle',
    'google-plus-square',
    'google-wallet',
    'graduation-cap',
    'grav',
    'group',
    'h-square',
    'hacker-news',
    'hand-grab-o',
    'hand-lizard-o',
    'hand-o-left',
    'hand-o-up',
    'hand-paper-o',
    'hand-peace-o',
    'hand-pointer-o',
    'hand-scissors-o',
    'hand-spock-o',
    'handshake-o',
    'hashtag',
    'hdd-o',
    'header',
    'headphones',
    'heart',
    'heart-o',
    'heartbeat',
    'history',
    'home',
    'hospital-o',
    'hourglass',
    'hourglass-1',
    'hourglass-2',
    'hourglass-3',
    'hourglass-o',
    'houzz',
    'html5',
    'i-cursor',
    'id-badge',
    'ils',
    'image',
    'imdb',
    'inbox',
    'indent',
    'industry',
    'info',
    'info-circle',
    'inr',
    'instagram',
    'internet-explorer',
    'intersex',
    'ioxhost',
    'italic',
    'joomla',
    'jsfiddle',
    'key',
    'keyboard-o',
    'krw',
    'language',
    'laptop',
    'lastfm',
    'lastfm-square',
    'leaf',
    'leanpub',
    'lemon-o',
    'level-up',
    'life-bouy',
    'lightbulb-o',
    'line-chart',
    'linkedin',
    'linkedin-square',
    'linode',
    'linux',
    'list',
    'list-alt',
    'list-ol',
    'list-ul',
    'location-arrow',
    'lock',
    'long-arrow-left',
    'long-arrow-up',
    'low-vision',
    'magic',
    'magnet',
    'mail-forward',
    'mail-reply',
    'mail-reply-all',
    'male',
    'map',
    'map-marker',
    'map-o',
    'map-pin',
    'map-signs',
    'mars',
    'mars-double',
    'mars-stroke',
    'mars-stroke-h',
    'mars-stroke-v',
    'maxcdn',
    'meanpath',
    'medium',
    'medkit',
    'meetup',
    'meh-o',
    'mercury',
    'microchip',
    'microphone',
    'microphone-slash',
    'minus',
    'minus-circle',
    'minus-square',
    'minus-square-o',
    'mixcloud',
    'mobile',
    'modx',
    'money',
    'moon-o',
    'motorcycle',
    'mouse-pointer',
    'music',
    'neuter',
    'newspaper-o',
    'object-group',
    'object-ungroup',
    'odnoklassniki',
    'odnoklassniki-square',
    'opencart',
    'openid',
    'opera',
    'optin-monster',
    'pagelines',
    'paint-brush',
    'paper-plane',
    'paper-plane-o',
    'paperclip',
    'paragraph',
    'pause',
    'pause-circle',
    'pause-circle-o',
    'paw',
    'paypal',
    'pencil',
    'pencil-square',
    'percent',
    'phone',
    'phone-square',
    'pie-chart',
    'pied-piper',
    'pied-piper-alt',
    'pied-piper-pp',
    'pinterest',
    'pinterest-p',
    'pinterest-square',
    'plane',
    'play',
    'play-circle',
    'play-circle-o',
    'plug',
    'plus',
    'plus-circle',
    'plus-square',
    'plus-square-o',
    'podcast',
    'power-off',
    'print',
    'product-hunt',
    'puzzle-piece',
    'qq',
    'qrcode',
    'question',
    'question-circle',
    'question-circle-o',
    'quora',
    'quote-left',
    'quote-right',
    'ra',
    'random',
    'ravelry',
    'recycle',
    'reddit',
    'reddit-alien',
    'reddit-square',
    'refresh',
    'registered',
    'renren',
    'repeat',
    'retweet',
    'road',
    'rocket',
    'rotate-left',
    'rouble',
    'rss-square',
    'safari',
    'scribd',
    'search',
    'search-minus',
    'search-plus',
    'sellsy',
    'server',
    'share-alt',
    'share-alt-square',
    'share-square',
    'share-square-o',
    'shield',
    'ship',
    'shirtsinbulk',
    'shopping-bag',
    'shopping-basket',
    'shopping-cart',
    'shower',
    'sign-in',
    'sign-language',
    'sign-out',
    'signal',
    'simplybuilt',
    'sitemap',
    'skyatlas',
    'skype',
    'slack',
    'sliders',
    'slideshare',
    'smile-o',
    'snapchat',
    'snapchat-ghost',
    'snapchat-square',
    'snowflake-o',
    'sort',
    'sort-alpha-asc',
    'sort-alpha-desc',
    'sort-amount-asc',
    'sort-amount-desc',
    'sort-asc',
    'sort-numeric-asc',
    'sort-numeric-desc',
    'soundcloud',
    'space-shuttle',
    'spinner',
    'spoon',
    'spotify',
    'square',
    'square-o',
    'stack-exchange',
    'stack-overflow',
    'star',
    'star-half',
    'star-half-empty',
    'star-o',
    'steam',
    'steam-square',
    'step-backward',
    'stethoscope',
    'sticky-note',
    'sticky-note-o',
    'stop',
    'stop-circle',
    'stop-circle-o',
    'street-view',
    'strikethrough',
    'stumbleupon',
    'stumbleupon-circle',
    'subscript',
    'subway',
    'suitcase',
    'sun-o',
    'superpowers',
    'superscript',
    'table',
    'tablet',
    'tag',
    'tags',
    'tasks',
    'telegram',
    'television',
    'tencent-weibo',
    'terminal',
    'text-height',
    'text-width',
    'th',
    'th-large',
    'th-list',
    'themeisle',
    'thermometer',
    'thermometer-0',
    'thermometer-1',
    'thermometer-2',
    'thermometer-3',
    'thumb-tack',
    'thumbs-down',
    'thumbs-o-up',
    'thumbs-up',
    'ticket',
    'times-circle',
    'times-circle-o',
    'times-rectangle',
    'times-rectangle-o',
    'tint',
    'toggle-off',
    'toggle-on',
    'trademark',
    'train',
    'transgender-alt',
    'trash',
    'trash-o',
    'tree',
    'trello',
    'tripadvisor',
    'trophy',
    'truck',
    'try',
    'tty',
    'tumblr',
    'tumblr-square',
    'twitch',
    'twitter',
    'twitter-square',
    'umbrella',
    'underline',
    'universal-access',
    'unlock',
    'unlock-alt',
    'upload',
    'usb',
    'user',
    'user-circle',
    'user-circle-o',
    'user-md',
    'user-o',
    'user-plus',
    'user-secret',
    'user-times',
    'venus',
    'venus-double',
    'venus-mars',
    'viacoin',
    'viadeo',
    'viadeo-square',
    'video-camera',
    'vimeo',
    'vimeo-square',
    'vine',
    'vk',
    'volume-control-phone',
    'volume-down',
    'volume-off',
    'volume-up',
    'wechat',
    'weibo',
    'whatsapp',
    'wheelchair',
    'wheelchair-alt',
    'wifi',
    'wikipedia-w',
    'window-maximize',
    'window-minimize',
    'window-restore',
    'windows',
    'wordpress',
    'wpbeginner',
    'wpexplorer',
    'wpforms',
    'wrench',
    'xing',
    'xing-square',
    'y-combinator',
    'yahoo',
    'yelp',
    'yoast',
    'youtube',
    'youtube-play',
    'youtube-square'
  ],
  'fa-solid:': [
    'abacus',
    'ad',
    'address-book',
    'address-card',
    'adjust',
    'air-freshener',
    'align-center',
    'align-justify',
    'align-left',
    'align-right',
    'allergies',
    'ambulance',
    'american-sign-language-interpreting',
    'anchor',
    'angle-double-down',
    'angle-double-left',
    'angle-double-right',
    'angle-double-up',
    'angle-down',
    'angle-left',
    'angle-right',
    'angle-up',
    'angry',
    'ankh',
    'apple-alt',
    'archive',
    'archway',
    'arrow-alt-circle-down',
    'arrow-alt-circle-left',
    'arrow-alt-circle-right',
    'arrow-alt-circle-up',
    'arrow-circle-down',
    'arrow-circle-left',
    'arrow-circle-right',
    'arrow-circle-up',
    'arrow-down',
    'arrow-left',
    'arrow-right',
    'arrow-up',
    'arrows-alt',
    'arrows-alt-h',
    'arrows-alt-v',
    'assistive-listening-systems',
    'asterisk',
    'at',
    'atlas',
    'atom',
    'audio-description',
    'award',
    'baby',
    'baby-carriage',
    'backspace',
    'backward',
    'bacon',
    'bacteria',
    'bacterium',
    'bahai',
    'balance-scale',
    'balance-scale-left',
    'balance-scale-right',
    'ban',
    'band-aid',
    'barcode',
    'bars',
    'baseball-ball',
    'basketball-ball',
    'bath',
    'battery-empty',
    'battery-full',
    'battery-half',
    'battery-quarter',
    'battery-three-quarters',
    'bed',
    'beer',
    'bell',
    'bell-slash',
    'bezier-curve',
    'bible',
    'bicycle',
    'biking',
    'binoculars',
    'biohazard',
    'birthday-cake',
    'blender',
    'blender-phone',
    'blind',
    'blog',
    'bold',
    'bolt',
    'bomb',
    'bone',
    'bong',
    'book',
    'book-dead',
    'book-medical',
    'book-open',
    'book-reader',
    'bookmark',
    'border-all',
    'border-none',
    'border-style',
    'bowling-ball',
    'box',
    'box-open',
    'box-tissue',
    'boxes',
    'braille',
    'brain',
    'bread-slice',
    'briefcase',
    'briefcase-medical',
    'broadcast-tower',
    'broom',
    'brush',
    'bug',
    'building',
    'bullhorn',
    'bullseye',
    'burn',
    'bus',
    'bus-alt',
    'business-time',
    'calculator',
    'calculator-alt',
    'calendar',
    'calendar-alt',
    'calendar-check',
    'calendar-day',
    'calendar-minus',
    'calendar-plus',
    'calendar-times',
    'calendar-week',
    'camera',
    'camera-retro',
    'campground',
    'candy-cane',
    'cannabis',
    'capsules',
    'car',
    'car-alt',
    'car-battery',
    'car-crash',
    'car-side',
    'caravan',
    'caret-down',
    'caret-left',
    'caret-right',
    'caret-square-down',
    'caret-square-left',
    'caret-square-right',
    'caret-square-up',
    'caret-up',
    'carrot',
    'cart-arrow-down',
    'cart-plus',
    'cash-register',
    'cat',
    'certificate',
    'chair',
    'chalkboard',
    'chalkboard-teacher',
    'charging-station',
    'chart-area',
    'chart-bar',
    'chart-line',
    'chart-pie',
    'check',
    'check-circle',
    'check-double',
    'check-square',
    'cheese',
    'chess',
    'chess-bishop',
    'chess-board',
    'chess-king',
    'chess-knight',
    'chess-pawn',
    'chess-queen',
    'chess-rook',
    'chevron-circle-down',
    'chevron-circle-left',
    'chevron-circle-right',
    'chevron-circle-up',
    'chevron-down',
    'chevron-left',
    'chevron-right',
    'chevron-up',
    'child',
    'church',
    'circle',
    'circle-notch',
    'city',
    'clinic-medical',
    'clipboard',
    'clipboard-check',
    'clipboard-list',
    'clock',
    'clone',
    'closed-captioning',
    'cloud',
    'cloud-download-alt',
    'cloud-meatball',
    'cloud-moon',
    'cloud-moon-rain',
    'cloud-rain',
    'cloud-showers-heavy',
    'cloud-sun',
    'cloud-sun-rain',
    'cloud-upload-alt',
    'cocktail',
    'code',
    'code-branch',
    'coffee',
    'cog',
    'cogs',
    'coins',
    'columns',
    'comment',
    'comment-alt',
    'comment-dollar',
    'comment-dots',
    'comment-medical',
    'comment-slash',
    'comments',
    'comments-dollar',
    'compact-disc',
    'compass',
    'compress',
    'compress-alt',
    'compress-arrows-alt',
    'concierge-bell',
    'cookie',
    'cookie-bite',
    'copy',
    'copyright',
    'couch',
    'credit-card',
    'crop',
    'crop-alt',
    'cross',
    'crosshairs',
    'crow',
    'crown',
    'crutch',
    'cube',
    'cubes',
    'cut',
    'database',
    'deaf',
    'democrat',
    'desktop',
    'dharmachakra',
    'diagnoses',
    'dice',
    'dice-d20',
    'dice-d6',
    'dice-five',
    'dice-four',
    'dice-one',
    'dice-six',
    'dice-three',
    'dice-two',
    'digital-tachograph',
    'directions',
    'disease',
    'divide',
    'dizzy',
    'dna',
    'dog',
    'dollar-sign',
    'dolly',
    'dolly-flatbed',
    'donate',
    'door-closed',
    'door-open',
    'dot-circle',
    'dove',
    'download',
    'drafting-compass',
    'dragon',
    'draw-polygon',
    'drum',
    'drum-steelpan',
    'drumstick-bite',
    'dumbbell',
    'dumpster',
    'dumpster-fire',
    'dungeon',
    'edit',
    'egg',
    'eject',
    'ellipsis-h',
    'ellipsis-v',
    'empty-set',
    'envelope',
    'envelope-open',
    'envelope-open-text',
    'envelope-square',
    'equals',
    'eraser',
    'ethernet',
    'euro-sign',
    'exchange-alt',
    'exclamation',
    'exclamation-circle',
    'exclamation-triangle',
    'expand',
    'expand-alt',
    'expand-arrows-alt',
    'external-link-alt',
    'external-link-square-alt',
    'eye',
    'eye-dropper',
    'eye-slash',
    'fan',
    'fast-backward',
    'fast-forward',
    'faucet',
    'fax',
    'feather',
    'feather-alt',
    'female',
    'fighter-jet',
    'file',
    'file-alt',
    'file-archive',
    'file-audio',
    'file-code',
    'file-contract',
    'file-csv',
    'file-download',
    'file-excel',
    'file-export',
    'file-image',
    'file-import',
    'file-invoice',
    'file-invoice-dollar',
    'file-medical',
    'file-medical-alt',
    'file-pdf',
    'file-powerpoint',
    'file-prescription',
    'file-signature',
    'file-upload',
    'file-video',
    'file-word',
    'fill',
    'fill-drip',
    'film',
    'filter',
    'fingerprint',
    'fire',
    'fire-alt',
    'fire-extinguisher',
    'first-aid',
    'fish',
    'fist-raised',
    'flag',
    'flag-checkered',
    'flag-usa',
    'flask',
    'flushed',
    'folder',
    'folder-minus',
    'folder-open',
    'folder-plus',
    'font',
    'football-ball',
    'forward',
    'frog',
    'frown',
    'frown-open',
    'function',
    'funnel-dollar',
    'futbol',
    'gamepad',
    'gas-pump',
    'gavel',
    'gem',
    'genderless',
    'ghost',
    'gift',
    'gifts',
    'glass-cheers',
    'glass-martini',
    'glass-martini-alt',
    'glass-whiskey',
    'glasses',
    'globe',
    'globe-africa',
    'globe-americas',
    'globe-asia',
    'globe-europe',
    'golf-ball',
    'gopuram',
    'graduation-cap',
    'greater-than',
    'greater-than-equal',
    'grimace',
    'grin',
    'grin-alt',
    'grin-beam',
    'grin-beam-sweat',
    'grin-hearts',
    'grin-squint',
    'grin-squint-tears',
    'grin-stars',
    'grin-tears',
    'grin-tongue',
    'grin-tongue-squint',
    'grin-tongue-wink',
    'grin-wink',
    'grip-horizontal',
    'grip-lines',
    'grip-lines-vertical',
    'grip-vertical',
    'guitar',
    'h-square',
    'hamburger',
    'hammer',
    'hamsa',
    'hand-holding',
    'hand-holding-heart',
    'hand-holding-medical',
    'hand-holding-usd',
    'hand-holding-water',
    'hand-lizard',
    'hand-middle-finger',
    'hand-paper',
    'hand-peace',
    'hand-point-down',
    'hand-point-left',
    'hand-point-right',
    'hand-point-up',
    'hand-pointer',
    'hand-rock',
    'hand-scissors',
    'hand-sparkles',
    'hand-spock',
    'hands',
    'hands-helping',
    'hands-wash',
    'handshake',
    'handshake-alt-slash',
    'handshake-slash',
    'hanukiah',
    'hard-hat',
    'hashtag',
    'hat-cowboy',
    'hat-cowboy-side',
    'hat-wizard',
    'hdd',
    'head-side-cough',
    'head-side-cough-slash',
    'head-side-mask',
    'head-side-virus',
    'heading',
    'headphones',
    'headphones-alt',
    'headset',
    'heart',
    'heart-broken',
    'heartbeat',
    'helicopter',
    'highlighter',
    'hiking',
    'hippo',
    'history',
    'hockey-puck',
    'holly-berry',
    'home',
    'horse',
    'horse-head',
    'hospital',
    'hospital-alt',
    'hospital-symbol',
    'hospital-user',
    'hot-tub',
    'hotdog',
    'hotel',
    'hourglass',
    'hourglass-end',
    'hourglass-half',
    'hourglass-start',
    'house-damage',
    'house-user',
    'hryvnia',
    'i-cursor',
    'ice-cream',
    'icicles',
    'icons',
    'id-badge',
    'id-card',
    'id-card-alt',
    'igloo',
    'image',
    'images',
    'inbox',
    'indent',
    'industry',
    'infinity',
    'info',
    'info-circle',
    'integral',
    'intersection',
    'italic',
    'jedi',
    'joint',
    'journal-whills',
    'kaaba',
    'key',
    'keyboard',
    'khanda',
    'kiss',
    'kiss-beam',
    'kiss-wink-heart',
    'kiwi-bird',
    'lambda',
    'landmark',
    'language',
    'laptop',
    'laptop-code',
    'laptop-house',
    'laptop-medical',
    'laugh',
    'laugh-beam',
    'laugh-squint',
    'laugh-wink',
    'layer-group',
    'leaf',
    'lemon',
    'less-than',
    'less-than-equal',
    'level-down-alt',
    'level-up-alt',
    'life-ring',
    'lightbulb',
    'link',
    'lira-sign',
    'list',
    'list-alt',
    'list-ol',
    'list-ul',
    'location-arrow',
    'lock',
    'lock-open',
    'long-arrow-alt-down',
    'long-arrow-alt-left',
    'long-arrow-alt-right',
    'long-arrow-alt-up',
    'low-vision',
    'luggage-cart',
    'lungs',
    'lungs-virus',
    'magic',
    'magnet',
    'mail-bulk',
    'male',
    'map',
    'map-marked',
    'map-marked-alt',
    'map-marker',
    'map-marker-alt',
    'map-pin',
    'map-signs',
    'marker',
    'mars',
    'mars-double',
    'mars-stroke',
    'mars-stroke-h',
    'mars-stroke-v',
    'mask',
    'medal',
    'medkit',
    'meh',
    'meh-blank',
    'meh-rolling-eyes',
    'memory',
    'menorah',
    'mercury',
    'meteor',
    'microchip',
    'microphone',
    'microphone-alt',
    'microphone-alt-slash',
    'microphone-slash',
    'microscope',
    'minus',
    'minus-circle',
    'minus-square',
    'mitten',
    'mobile',
    'mobile-alt',
    'money-bill',
    'money-bill-alt',
    'money-bill-wave',
    'money-bill-wave-alt',
    'money-check',
    'money-check-alt',
    'monument',
    'moon',
    'mortar-pestle',
    'mosque',
    'motorcycle',
    'mountain',
    'mouse',
    'mouse-pointer',
    'mug-hot',
    'music',
    'network-wired',
    'neuter',
    'newspaper',
    'not-equal',
    'notes-medical',
    'object-group',
    'object-ungroup',
    'oil-can',
    'om',
    'omega',
    'otter',
    'outdent',
    'pager',
    'paint-brush',
    'paint-roller',
    'palette',
    'pallet',
    'paper-plane',
    'paperclip',
    'parachute-box',
    'paragraph',
    'parking',
    'passport',
    'pastafarianism',
    'paste',
    'pause',
    'pause-circle',
    'paw',
    'peace',
    'pen',
    'pen-alt',
    'pen-fancy',
    'pen-nib',
    'pen-square',
    'pencil-alt',
    'pencil-ruler',
    'people-arrows',
    'people-carry',
    'pepper-hot',
    'percent',
    'percentage',
    'person-booth',
    'phone',
    'phone-alt',
    'phone-slash',
    'phone-square',
    'phone-square-alt',
    'phone-volume',
    'photo-video',
    'pi',
    'piggy-bank',
    'pills',
    'pizza-slice',
    'place-of-worship',
    'plane',
    'plane-arrival',
    'plane-departure',
    'plane-slash',
    'play',
    'play-circle',
    'plug',
    'plus',
    'plus-circle',
    'plus-square',
    'podcast',
    'poll',
    'poll-h',
    'poo',
    'poo-storm',
    'poop',
    'portrait',
    'pound-sign',
    'power-off',
    'pray',
    'praying-hands',
    'prescription',
    'prescription-bottle',
    'prescription-bottle-alt',
    'print',
    'procedures',
    'project-diagram',
    'pump-medical',
    'pump-soap',
    'puzzle-piece',
    'qrcode',
    'question',
    'question-circle',
    'quidditch',
    'quote-left',
    'quote-right',
    'quran',
    'radiation',
    'radiation-alt',
    'rainbow',
    'random',
    'receipt',
    'record-vinyl',
    'recycle',
    'redo',
    'redo-alt',
    'registered',
    'remove-format',
    'reply',
    'reply-all',
    'republican',
    'restroom',
    'retweet',
    'ribbon',
    'ring',
    'road',
    'robot',
    'rocket',
    'route',
    'rss',
    'rss-square',
    'ruble-sign',
    'ruler',
    'ruler-combined',
    'ruler-horizontal',
    'ruler-vertical',
    'running',
    'rupee-sign',
    'sad-cry',
    'sad-tear',
    'satellite',
    'satellite-dish',
    'save',
    'school',
    'screwdriver',
    'scroll',
    'sd-card',
    'search',
    'search-dollar',
    'search-location',
    'search-minus',
    'search-plus',
    'seedling',
    'server',
    'shapes',
    'share',
    'share-alt',
    'share-alt-square',
    'share-square',
    'shekel-sign',
    'shield-alt',
    'shield-virus',
    'ship',
    'shipping-fast',
    'shoe-prints',
    'shopping-bag',
    'shopping-basket',
    'shopping-cart',
    'shower',
    'shuttle-van',
    'sigma',
    'sign',
    'sign-in-alt',
    'sign-language',
    'sign-out-alt',
    'signal',
    'signal-alt',
    'signal-alt-slash',
    'signal-slash',
    'signature',
    'sim-card',
    'sink',
    'sitemap',
    'skating',
    'skiing',
    'skiing-nordic',
    'skull',
    'skull-crossbones',
    'slash',
    'sleigh',
    'sliders-h',
    'smile',
    'smile-beam',
    'smile-wink',
    'smog',
    'smoking',
    'smoking-ban',
    'sms',
    'snowboarding',
    'snowflake',
    'snowman',
    'snowplow',
    'soap',
    'socks',
    'solar-panel',
    'sort',
    'sort-alpha-down',
    'sort-alpha-down-alt',
    'sort-alpha-up',
    'sort-alpha-up-alt',
    'sort-amount-down',
    'sort-amount-down-alt',
    'sort-amount-up',
    'sort-amount-up-alt',
    'sort-down',
    'sort-numeric-down',
    'sort-numeric-down-alt',
    'sort-numeric-up',
    'sort-numeric-up-alt',
    'sort-up',
    'spa',
    'space-shuttle',
    'spell-check',
    'spider',
    'spinner',
    'splotch',
    'spray-can',
    'square',
    'square-full',
    'square-root',
    'square-root-alt',
    'stamp',
    'star',
    'star-and-crescent',
    'star-half',
    'star-half-alt',
    'star-of-david',
    'star-of-life',
    'step-backward',
    'step-forward',
    'stethoscope',
    'sticky-note',
    'stop',
    'stop-circle',
    'stopwatch',
    'stopwatch-20',
    'store',
    'store-alt',
    'store-alt-slash',
    'store-slash',
    'stream',
    'street-view',
    'strikethrough',
    'stroopwafel',
    'subscript',
    'subway',
    'suitcase',
    'suitcase-rolling',
    'sun',
    'superscript',
    'surprise',
    'swatchbook',
    'swimmer',
    'swimming-pool',
    'synagogue',
    'sync',
    'sync-alt',
    'syringe',
    'table',
    'table-tennis',
    'tablet',
    'tablet-alt',
    'tablets',
    'tachometer-alt',
    'tag',
    'tags',
    'tally',
    'tape',
    'tasks',
    'taxi',
    'teeth',
    'teeth-open',
    'temperature-high',
    'temperature-low',
    'tenge',
    'terminal',
    'text-height',
    'text-width',
    'th',
    'th-large',
    'th-list',
    'theater-masks',
    'thermometer',
    'thermometer-empty',
    'thermometer-full',
    'thermometer-half',
    'thermometer-quarter',
    'thermometer-three-quarters',
    'theta',
    'thumbs-down',
    'thumbs-up',
    'thumbtack',
    'ticket-alt',
    'tilde',
    'times',
    'times-circle',
    'tint',
    'tint-slash',
    'tired',
    'toggle-off',
    'toggle-on',
    'toilet',
    'toilet-paper',
    'toilet-paper-slash',
    'toolbox',
    'tools',
    'tooth',
    'torah',
    'torii-gate',
    'tractor',
    'trademark',
    'traffic-light',
    'trailer',
    'train',
    'tram',
    'transgender',
    'transgender-alt',
    'trash',
    'trash-alt',
    'trash-restore',
    'trash-restore-alt',
    'tree',
    'trophy',
    'truck',
    'truck-loading',
    'truck-monster',
    'truck-moving',
    'truck-pickup',
    'tshirt',
    'tty',
    'tv',
    'umbrella',
    'umbrella-beach',
    'underline',
    'undo',
    'undo-alt',
    'union',
    'universal-access',
    'university',
    'unlink',
    'unlock',
    'unlock-alt',
    'upload',
    'user',
    'user-alt',
    'user-alt-slash',
    'user-astronaut',
    'user-check',
    'user-circle',
    'user-clock',
    'user-cog',
    'user-edit',
    'user-friends',
    'user-graduate',
    'user-injured',
    'user-lock',
    'user-md',
    'user-minus',
    'user-ninja',
    'user-nurse',
    'user-plus',
    'user-secret',
    'user-shield',
    'user-slash',
    'user-tag',
    'user-tie',
    'user-times',
    'users',
    'users-cog',
    'users-slash',
    'utensil-spoon',
    'utensils',
    'value-absolute',
    'vector-square',
    'venus',
    'venus-double',
    'venus-mars',
    'vest',
    'vest-patches',
    'vial',
    'vials',
    'video',
    'video-slash',
    'vihara',
    'virus',
    'virus-slash',
    'viruses',
    'voicemail',
    'volleyball-ball',
    'volume',
    'volume-down',
    'volume-mute',
    'volume-off',
    'volume-slash',
    'volume-up',
    'vote-yea',
    'vr-cardboard',
    'walking',
    'wallet',
    'warehouse',
    'water',
    'wave-square',
    'weight',
    'weight-hanging',
    'wheelchair',
    'wifi',
    'wifi-slash',
    'wind',
    'window-close',
    'window-maximize',
    'window-minimize',
    'window-restore',
    'wine-bottle',
    'wine-glass',
    'wine-glass-alt',
    'won-sign',
    'wrench',
    'x-ray',
    'yen-sign',
    'yin-yang'
  ]
}
src/hooks/event/useScrollTo.ts
New file
@@ -0,0 +1,60 @@
export interface ScrollToParams {
  el: HTMLElement
  to: number
  position: string
  duration?: number
  callback?: () => void
}
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
  t /= d / 2
  if (t < 1) {
    return (c / 2) * t * t + b
  }
  t--
  return (-c / 2) * (t * (t - 2) - 1) + b
}
const move = (el: HTMLElement, position: string, amount: number) => {
  el[position] = amount
}
export function useScrollTo({
  el,
  position = 'scrollLeft',
  to,
  duration = 500,
  callback
}: ScrollToParams) {
  const isActiveRef = ref(false)
  const start = el[position]
  const change = to - start
  const increment = 20
  let currentTime = 0
  function animateScroll() {
    if (!unref(isActiveRef)) {
      return
    }
    currentTime += increment
    const val = easeInOutQuad(currentTime, start, change, duration)
    move(el, position, val)
    if (currentTime < duration && unref(isActiveRef)) {
      requestAnimationFrame(animateScroll)
    } else {
      if (callback) {
        callback()
      }
    }
  }
  function run() {
    isActiveRef.value = true
    animateScroll()
  }
  function stop() {
    isActiveRef.value = false
  }
  return { start: run, stop }
}
src/hooks/web/useCache.ts
New file
@@ -0,0 +1,39 @@
/**
 * 配置浏览器本地存储的方式,可直接存储对象数组。
 */
import WebStorageCache from 'web-storage-cache'
type CacheType = 'localStorage' | 'sessionStorage'
export const CACHE_KEY = {
  // 用户相关
  ROLE_ROUTERS: 'roleRouters',
  USER: 'user',
  // 系统设置
  IS_DARK: 'isDark',
  LANG: 'lang',
  THEME: 'theme',
  LAYOUT: 'layout',
  DICT_CACHE: 'dictCache',
  // 登录表单
  LoginForm: 'loginForm',
  TenantId: 'tenantId'
}
export const useCache = (type: CacheType = 'localStorage') => {
  const wsCache: WebStorageCache = new WebStorageCache({
    storage: type
  })
  return {
    wsCache
  }
}
export const deleteUserCache = () => {
  const { wsCache } = useCache()
  wsCache.delete(CACHE_KEY.USER)
  wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
  // 注意,不要清理 LoginForm 登录表单
}
src/hooks/web/useConfigGlobal.ts
New file
@@ -0,0 +1,9 @@
import { ConfigGlobalTypes } from '@/types/configGlobal'
export const useConfigGlobal = () => {
  const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes
  return {
    configGlobal
  }
}
src/hooks/web/useCrudSchemas.ts
New file
@@ -0,0 +1,326 @@
import { reactive } from 'vue'
import { AxiosPromise } from 'axios'
import { findIndex } from '@/utils'
import { eachTree, filter, treeMap } from '@/utils/tree'
import { getBoolDictOptions, getDictOptions, getIntDictOptions } from '@/utils/dict'
import { FormSchema } from '@/types/form'
import { TableColumn } from '@/types/table'
import { DescriptionsSchema } from '@/types/descriptions'
import { ComponentOptions, ComponentProps } from '@/types/components'
import { DictTag } from '@/components/DictTag'
import { cloneDeep, merge } from 'lodash-es'
export type CrudSchema = Omit<TableColumn, 'children'> & {
  isSearch?: boolean // 是否在查询显示
  search?: CrudSearchParams // 查询的详细配置
  isTable?: boolean // 是否在列表显示
  table?: CrudTableParams // 列表的详细配置
  isForm?: boolean // 是否在表单显示
  form?: CrudFormParams // 表单的详细配置
  isDetail?: boolean // 是否在详情显示
  detail?: CrudDescriptionsParams // 详情的详细配置
  children?: CrudSchema[]
  dictType?: string // 字典类型
  dictClass?: 'string' | 'number' | 'boolean' // 字典数据类型 string | number | boolean
}
type CrudSearchParams = {
  // 是否显示在查询项
  show?: boolean
  // 接口
  api?: () => Promise<any>
  // 搜索字段
  field?: string
} & Omit<FormSchema, 'field'>
type CrudTableParams = {
  // 是否显示表头
  show?: boolean
  // 列宽配置
  width?: number | string
  // 列是否固定在左侧或者右侧
  fixed?: 'left' | 'right'
} & Omit<FormSchema, 'field'>
type CrudFormParams = {
  // 是否显示表单项
  show?: boolean
  // 接口
  api?: () => Promise<any>
} & Omit<FormSchema, 'field'>
type CrudDescriptionsParams = {
  // 是否显示表单项
  show?: boolean
} & Omit<DescriptionsSchema, 'field'>
interface AllSchemas {
  searchSchema: FormSchema[]
  tableColumns: TableColumn[]
  formSchema: FormSchema[]
  detailSchema: DescriptionsSchema[]
}
const { t } = useI18n()
// 过滤所有结构
export const useCrudSchemas = (
  crudSchema: CrudSchema[]
): {
  allSchemas: AllSchemas
} => {
  // 所有结构数据
  const allSchemas = reactive<AllSchemas>({
    searchSchema: [],
    tableColumns: [],
    formSchema: [],
    detailSchema: []
  })
  const searchSchema = filterSearchSchema(crudSchema, allSchemas)
  allSchemas.searchSchema = searchSchema || []
  const tableColumns = filterTableSchema(crudSchema)
  allSchemas.tableColumns = tableColumns || []
  const formSchema = filterFormSchema(crudSchema, allSchemas)
  allSchemas.formSchema = formSchema
  const detailSchema = filterDescriptionsSchema(crudSchema)
  allSchemas.detailSchema = detailSchema
  return {
    allSchemas
  }
}
// 过滤 Search 结构
const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
  const searchSchema: FormSchema[] = []
  // 获取字典列表队列
  const searchRequestTask: Array<() => Promise<void>> = []
  eachTree(crudSchema, (schemaItem: CrudSchema) => {
    // 判断是否显示
    if (schemaItem?.isSearch || schemaItem.search?.show) {
      let component = schemaItem?.search?.component || 'Input'
      const options: ComponentOptions[] = []
      let comonentProps: ComponentProps = {}
      if (schemaItem.dictType) {
        const allOptions: ComponentOptions = { label: '全部', value: '' }
        options.push(allOptions)
        getDictOptions(schemaItem.dictType).forEach((dict) => {
          options.push(dict)
        })
        comonentProps = {
          options: options
        }
        if (!schemaItem.search?.component) component = 'Select'
      }
      // updated by AKing: 解决了当使用默认的dict选项时,form中事件不能触发的问题
      const searchSchemaItem = merge(
        {
          // 默认为 input
          component,
          ...schemaItem.search,
          field: schemaItem.field,
          label: schemaItem.search?.label || schemaItem.label
        },
        { componentProps: comonentProps }
      )
      if (searchSchemaItem.api) {
        searchRequestTask.push(async () => {
          const res = await (searchSchemaItem.api as () => AxiosPromise)()
          if (res) {
            const index = findIndex(allSchemas.searchSchema, (v: FormSchema) => {
              return v.field === searchSchemaItem.field
            })
            if (index !== -1) {
              allSchemas.searchSchema[index]!.componentProps!.options = filterOptions(
                res,
                searchSchemaItem.componentProps.optionsAlias?.labelField
              )
            }
          }
        })
      }
      // 删除不必要的字段
      delete searchSchemaItem.show
      searchSchema.push(searchSchemaItem)
    }
  })
  for (const task of searchRequestTask) {
    task()
  }
  return searchSchema
}
// 过滤 table 结构
const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
  const tableColumns = treeMap<CrudSchema>(crudSchema, {
    conversion: (schema: CrudSchema) => {
      if (schema?.isTable !== false && schema?.table?.show !== false) {
        // add by 芋艿:增加对 dict 字典数据的支持
        if (!schema.formatter && schema.dictType) {
          schema.formatter = (_: Recordable, __: TableColumn, cellValue: any) => {
            return h(DictTag, {
              type: schema.dictType!, // ! 表示一定不为空
              value: cellValue
            })
          }
        }
        return {
          ...schema.table,
          ...schema
        }
      }
    }
  })
  // 第一次过滤会有 undefined 所以需要二次过滤
  return filter<TableColumn>(tableColumns as TableColumn[], (data) => {
    if (data.children === void 0) {
      delete data.children
    }
    return !!data.field
  })
}
// 过滤 form 结构
const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
  const formSchema: FormSchema[] = []
  // 获取字典列表队列
  const formRequestTask: Array<() => Promise<void>> = []
  eachTree(crudSchema, (schemaItem: CrudSchema) => {
    // 判断是否显示
    if (schemaItem?.isForm !== false && schemaItem?.form?.show !== false) {
      let component = schemaItem?.form?.component || 'Input'
      let defaultValue: any = ''
      if (schemaItem.form?.value) {
        defaultValue = schemaItem.form?.value
      } else {
        if (component === 'InputNumber') {
          defaultValue = 0
        }
      }
      let comonentProps: ComponentProps = {}
      if (schemaItem.dictType) {
        const options: ComponentOptions[] = []
        if (schemaItem.dictClass && schemaItem.dictClass === 'number') {
          getIntDictOptions(schemaItem.dictType).forEach((dict) => {
            options.push(dict)
          })
        } else if (schemaItem.dictClass && schemaItem.dictClass === 'boolean') {
          getBoolDictOptions(schemaItem.dictType).forEach((dict) => {
            options.push(dict)
          })
        } else {
          getDictOptions(schemaItem.dictType).forEach((dict) => {
            options.push(dict)
          })
        }
        comonentProps = {
          options: options
        }
        if (!(schemaItem.form && schemaItem.form.component)) component = 'Select'
      }
      // updated by AKing: 解决了当使用默认的dict选项时,form中事件不能触发的问题
      const formSchemaItem = merge(
        {
          // 默认为 input
          component,
          value: defaultValue,
          ...schemaItem.form,
          field: schemaItem.field,
          label: schemaItem.form?.label || schemaItem.label
        },
        { componentProps: comonentProps }
      )
      if (formSchemaItem.api) {
        formRequestTask.push(async () => {
          const res = await (formSchemaItem.api as () => AxiosPromise)()
          if (res) {
            const index = findIndex(allSchemas.formSchema, (v: FormSchema) => {
              return v.field === formSchemaItem.field
            })
            if (index !== -1) {
              allSchemas.formSchema[index]!.componentProps!.options = filterOptions(
                res,
                formSchemaItem.componentProps.optionsAlias?.labelField
              )
            }
          }
        })
      }
      // 删除不必要的字段
      delete formSchemaItem.show
      formSchema.push(formSchemaItem)
    }
  })
  for (const task of formRequestTask) {
    task()
  }
  return formSchema
}
// 过滤 descriptions 结构
const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => {
  const descriptionsSchema: FormSchema[] = []
  eachTree(crudSchema, (schemaItem: CrudSchema) => {
    // 判断是否显示
    if (schemaItem?.isDetail !== false && schemaItem.detail?.show !== false) {
      const descriptionsSchemaItem = {
        ...schemaItem.detail,
        field: schemaItem.field,
        label: schemaItem.detail?.label || schemaItem.label
      }
      if (schemaItem.dictType) {
        descriptionsSchemaItem.dictType = schemaItem.dictType
      }
      if (schemaItem.detail?.dateFormat || schemaItem.formatter == 'formatDate') {
        // 优先使用 detail 下的配置,如果没有默认为 YYYY-MM-DD HH:mm:ss
        descriptionsSchemaItem.dateFormat = schemaItem?.detail?.dateFormat
          ? schemaItem?.detail?.dateFormat
          : 'YYYY-MM-DD HH:mm:ss'
      }
      // 删除不必要的字段
      delete descriptionsSchemaItem.show
      descriptionsSchema.push(descriptionsSchemaItem)
    }
  })
  return descriptionsSchema
}
// 给options添加国际化
const filterOptions = (options: Recordable, labelField?: string) => {
  return options?.map((v: Recordable) => {
    if (labelField) {
      v['labelField'] = t(v.labelField)
    } else {
      v['label'] = t(v.label)
    }
    return v
  })
}
// 将 tableColumns 指定 fields 放到最前面
export const sortTableColumns = (tableColumns: TableColumn[], field: string) => {
  const fieldIndex = tableColumns.findIndex((item) => item.field === field)
  const fieldColumn = cloneDeep(tableColumns[fieldIndex])
  tableColumns.splice(fieldIndex, 1)
  // 添加到开头
  tableColumns.unshift(fieldColumn)
}
src/hooks/web/useDesign.ts
New file
@@ -0,0 +1,18 @@
import variables from '@/assets/styles/global.module.scss'
export const useDesign = () => {
  const scssVariables = variables
  /**
   * @param scope 类名
   * @returns 返回空间名-类名
   */
  const getPrefixCls = (scope: string) => {
    return `${scssVariables.namespace}-${scope}`
  }
  return {
    variables: scssVariables,
    getPrefixCls
  }
}
src/hooks/web/useEmitt.ts
New file
@@ -0,0 +1,22 @@
import mitt from 'mitt'
interface Option {
  name: string // 事件名称
  callback: Fn // 回调
}
const emitter = mitt()
export const useEmitt = (option?: Option) => {
  if (option) {
    emitter.on(option.name, option.callback)
    onBeforeUnmount(() => {
      emitter.off(option.name)
    })
  }
  return {
    emitter
  }
}
src/hooks/web/useForm.ts
New file
@@ -0,0 +1,94 @@
import type { Form, FormExpose } from '@/components/Form'
import type { ElForm } from 'element-plus'
import type { FormProps } from '@/components/Form/src/types'
import { FormSchema, FormSetPropsType } from '@/types/form'
export const useForm = (props?: FormProps) => {
  // From实例
  const formRef = ref<typeof Form & FormExpose>()
  // ElForm实例
  const elFormRef = ref<ComponentRef<typeof ElForm>>()
  /**
   * @param ref Form实例
   * @param elRef ElForm实例
   */
  const register = (ref: typeof Form & FormExpose, elRef: ComponentRef<typeof ElForm>) => {
    formRef.value = ref
    elFormRef.value = elRef
  }
  const getForm = async () => {
    await nextTick()
    const form = unref(formRef)
    if (!form) {
      console.error('The form is not registered. Please use the register method to register')
    }
    return form
  }
  // 一些内置的方法
  const methods: {
    setProps: (props: Recordable) => void
    setValues: (data: Recordable) => void
    getFormData: <T = Recordable | undefined>() => Promise<T>
    setSchema: (schemaProps: FormSetPropsType[]) => void
    addSchema: (formSchema: FormSchema, index?: number) => void
    delSchema: (field: string) => void
  } = {
    setProps: async (props: FormProps = {}) => {
      const form = await getForm()
      form?.setProps(props)
      if (props.model) {
        form?.setValues(props.model)
      }
    },
    setValues: async (data: Recordable) => {
      const form = await getForm()
      form?.setValues(data)
    },
    /**
     * @param schemaProps 需要设置的schemaProps
     */
    setSchema: async (schemaProps: FormSetPropsType[]) => {
      const form = await getForm()
      form?.setSchema(schemaProps)
    },
    /**
     * @param formSchema 需要新增数据
     * @param index 在哪里新增
     */
    addSchema: async (formSchema: FormSchema, index?: number) => {
      const form = await getForm()
      form?.addSchema(formSchema, index)
    },
    /**
     * @param field 删除哪个数据
     */
    delSchema: async (field: string) => {
      const form = await getForm()
      form?.delSchema(field)
    },
    /**
     * @returns form data
     */
    getFormData: async <T = Recordable>(): Promise<T> => {
      const form = await getForm()
      return form?.formModel as T
    }
  }
  props && methods.setProps(props)
  return {
    register,
    elFormRef,
    methods
  }
}
src/hooks/web/useGuide.ts
New file
@@ -0,0 +1,49 @@
import { Config, driver } from 'driver.js'
import 'driver.js/dist/driver.css'
import { useDesign } from '@/hooks/web/useDesign'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const { variables } = useDesign()
export const useGuide = (options?: Config) => {
  const driverObj = driver(
    options || {
      showProgress: true,
      nextBtnText: t('common.nextLabel'),
      prevBtnText: t('common.prevLabel'),
      doneBtnText: t('common.doneLabel'),
      steps: [
        {
          element: `#${variables.namespace}-menu`,
          popover: {
            title: t('common.menu'),
            description: t('common.menuDes'),
            side: 'right'
          }
        },
        {
          element: `#${variables.namespace}-tool-header`,
          popover: {
            title: t('common.tool'),
            description: t('common.toolDes'),
            side: 'left'
          }
        },
        {
          element: `#${variables.namespace}-tags-view`,
          popover: {
            title: t('common.tagsView'),
            description: t('common.tagsViewDes'),
            side: 'bottom'
          }
        }
      ]
    }
  )
  return {
    ...driverObj
  }
}
src/hooks/web/useI18n.ts
New file
@@ -0,0 +1,53 @@
import { i18n } from '@/plugins/vueI18n'
type I18nGlobalTranslation = {
  (key: string): string
  (key: string, locale: string): string
  (key: string, locale: string, list: unknown[]): string
  (key: string, locale: string, named: Record<string, unknown>): string
  (key: string, list: unknown[]): string
  (key: string, named: Record<string, unknown>): string
}
type I18nTranslationRestParameters = [string, any]
const getKey = (namespace: string | undefined, key: string) => {
  if (!namespace) {
    return key
  }
  if (key.startsWith(namespace)) {
    return key
  }
  return `${namespace}.${key}`
}
export const useI18n = (
  namespace?: string
): {
  t: I18nGlobalTranslation
} => {
  const normalFn = {
    t: (key: string) => {
      return getKey(namespace, key)
    }
  }
  if (!i18n) {
    return normalFn
  }
  const { t, ...methods } = i18n.global
  const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
    if (!key) return ''
    if (!key.includes('.') && !namespace) return key
    //@ts-ignore
    return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
  }
  return {
    ...methods,
    t: tFn
  }
}
export const t = (key: string) => key
src/hooks/web/useIcon.ts
New file
@@ -0,0 +1,8 @@
import { h } from 'vue'
import type { VNode } from 'vue'
import { Icon } from '@/components/Icon'
import { IconTypes } from '@/types/icon'
export const useIcon = (props: IconTypes): VNode => {
  return h(Icon, props)
}
src/hooks/web/useLocale.ts
New file
@@ -0,0 +1,35 @@
import { i18n } from '@/plugins/vueI18n'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
const setI18nLanguage = (locale: LocaleType) => {
  const localeStore = useLocaleStoreWithOut()
  if (i18n.mode === 'legacy') {
    i18n.global.locale = locale
  } else {
    ;(i18n.global.locale as any).value = locale
  }
  localeStore.setCurrentLocale({
    lang: locale
  })
  setHtmlPageLang(locale)
}
export const useLocale = () => {
  // Switching the language will change the locale of useI18n
  // And submit to configuration modification
  const changeLocale = async (locale: LocaleType) => {
    const globalI18n = i18n.global
    const langModule = await import(`../../locales/${locale}.ts`)
    globalI18n.setLocaleMessage(locale, langModule.default)
    setI18nLanguage(locale)
  }
  return {
    changeLocale
  }
}
src/hooks/web/useMessage.ts
New file
@@ -0,0 +1,95 @@
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { useI18n } from './useI18n'
export const useMessage = () => {
  const { t } = useI18n()
  return {
    // 消息提示
    info(content: string) {
      ElMessage.info(content)
    },
    // 错误消息
    error(content: string) {
      ElMessage.error(content)
    },
    // 成功消息
    success(content: string) {
      ElMessage.success(content)
    },
    // 警告消息
    warning(content: string) {
      ElMessage.warning(content)
    },
    // 弹出提示
    alert(content: string) {
      ElMessageBox.alert(content, t('common.confirmTitle'))
    },
    // 错误提示
    alertError(content: string) {
      ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'error' })
    },
    // 成功提示
    alertSuccess(content: string) {
      ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'success' })
    },
    // 警告提示
    alertWarning(content: string) {
      ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'warning' })
    },
    // 通知提示
    notify(content: string) {
      ElNotification.info(content)
    },
    // 错误通知
    notifyError(content: string) {
      ElNotification.error(content)
    },
    // 成功通知
    notifySuccess(content: string) {
      ElNotification.success(content)
    },
    // 警告通知
    notifyWarning(content: string) {
      ElNotification.warning(content)
    },
    // 确认窗体
    confirm(content: string, tip?: string) {
      return ElMessageBox.confirm(content, tip ? tip : t('common.confirmTitle'), {
        confirmButtonText: t('common.ok'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      })
    },
    // 删除窗体
    delConfirm(content?: string, tip?: string) {
      return ElMessageBox.confirm(
        content ? content : t('common.delMessage'),
        tip ? tip : t('common.confirmTitle'),
        {
          confirmButtonText: t('common.ok'),
          cancelButtonText: t('common.cancel'),
          type: 'warning'
        }
      )
    },
    // 导出窗体
    exportConfirm(content?: string, tip?: string) {
      return ElMessageBox.confirm(
        content ? content : t('common.exportMessage'),
        tip ? tip : t('common.confirmTitle'),
        {
          confirmButtonText: t('common.ok'),
          cancelButtonText: t('common.cancel'),
          type: 'warning'
        }
      )
    },
    // 提交内容
    prompt(content: string, tip: string) {
      return ElMessageBox.prompt(content, tip, {
        confirmButtonText: t('common.ok'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      })
    }
  }
}
src/hooks/web/useNProgress.ts
New file
@@ -0,0 +1,33 @@
import { useCssVar } from '@vueuse/core'
import type { NProgressOptions } from 'nprogress'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
export const useNProgress = () => {
  NProgress.configure({ showSpinner: false } as NProgressOptions)
  const initColor = async () => {
    await nextTick()
    const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef
    if (bar) {
      bar.style.background = unref(primaryColor.value)
    }
  }
  initColor()
  const start = () => {
    NProgress.start()
  }
  const done = () => {
    NProgress.done()
  }
  return {
    start,
    done
  }
}
src/hooks/web/useNetwork.ts
New file
@@ -0,0 +1,21 @@
import { ref, onBeforeUnmount } from 'vue'
const useNetwork = () => {
  const online = ref(true)
  const updateNetwork = () => {
    online.value = navigator.onLine
  }
  window.addEventListener('online', updateNetwork)
  window.addEventListener('offline', updateNetwork)
  onBeforeUnmount(() => {
    window.removeEventListener('online', updateNetwork)
    window.removeEventListener('offline', updateNetwork)
  })
  return { online }
}
export { useNetwork }
src/hooks/web/useNow.ts
New file
@@ -0,0 +1,60 @@
import { dateUtil } from '@/utils/dateUtil'
import { reactive, toRefs } from 'vue'
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
export const useNow = (immediate = true) => {
  let timer: IntervalHandle
  const state = reactive({
    year: 0,
    month: 0,
    week: '',
    day: 0,
    hour: '',
    minute: '',
    second: 0,
    meridiem: ''
  })
  const update = () => {
    const now = dateUtil()
    const h = now.format('HH')
    const m = now.format('mm')
    const s = now.get('s')
    state.year = now.get('y')
    state.month = now.get('M') + 1
    state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
    state.day = now.get('date')
    state.hour = h
    state.minute = m
    state.second = s
    state.meridiem = now.format('A')
  }
  function start() {
    update()
    clearInterval(timer)
    timer = setInterval(() => update(), 1000)
  }
  function stop() {
    clearInterval(timer)
  }
  tryOnMounted(() => {
    immediate && start()
  })
  tryOnUnmounted(() => {
    stop()
  })
  return {
    ...toRefs(state),
    start,
    stop
  }
}
src/hooks/web/usePageLoading.ts
New file
@@ -0,0 +1,18 @@
import { useAppStoreWithOut } from '@/store/modules/app'
const appStore = useAppStoreWithOut()
export const usePageLoading = () => {
  const loadStart = () => {
    appStore.setPageLoading(true)
  }
  const loadDone = () => {
    appStore.setPageLoading(false)
  }
  return {
    loadStart,
    loadDone
  }
}
src/hooks/web/useTable.ts
New file
@@ -0,0 +1,223 @@
import download from '@/utils/download'
import { Table, TableExpose } from '@/components/Table'
import { ElMessage, ElMessageBox, ElTable } from 'element-plus'
import { computed, nextTick, reactive, ref, unref, watch } from 'vue'
import type { TableProps } from '@/components/Table/src/types'
import { TableSetPropsType } from '@/types/table'
const { t } = useI18n()
interface ResponseType<T = any> {
  list: T[]
  total?: number
}
interface UseTableConfig<T = any> {
  getListApi: (option: any) => Promise<T>
  delListApi?: (option: any) => Promise<T>
  exportListApi?: (option: any) => Promise<T>
  // 返回数据格式配置
  response?: ResponseType
  // 默认传递的参数
  defaultParams?: Recordable
  props?: TableProps
}
interface TableObject<T = any> {
  pageSize: number
  currentPage: number
  total: number
  tableList: T[]
  params: any
  loading: boolean
  exportLoading: boolean
  currentRow: Nullable<T>
}
export const useTable = <T = any>(config?: UseTableConfig<T>) => {
  const tableObject = reactive<TableObject<T>>({
    // 页数
    pageSize: 10,
    // 当前页
    currentPage: 1,
    // 总条数
    total: 10,
    // 表格数据
    tableList: [],
    // AxiosConfig 配置
    params: {
      ...(config?.defaultParams || {})
    },
    // 加载中
    loading: true,
    // 导出加载中
    exportLoading: false,
    // 当前行的数据
    currentRow: null
  })
  const paramsObj = computed(() => {
    return {
      ...tableObject.params,
      pageSize: tableObject.pageSize,
      pageNo: tableObject.currentPage
    }
  })
  watch(
    () => tableObject.currentPage,
    () => {
      methods.getList()
    }
  )
  watch(
    () => tableObject.pageSize,
    () => {
      // 当前页不为1时,修改页数后会导致多次调用getList方法
      if (tableObject.currentPage === 1) {
        methods.getList()
      } else {
        tableObject.currentPage = 1
        methods.getList()
      }
    }
  )
  // Table实例
  const tableRef = ref<typeof Table & TableExpose>()
  // ElTable实例
  const elTableRef = ref<ComponentRef<typeof ElTable>>()
  const register = (ref: typeof Table & TableExpose, elRef: ComponentRef<typeof ElTable>) => {
    tableRef.value = ref
    elTableRef.value = elRef
  }
  const getTable = async () => {
    await nextTick()
    const table = unref(tableRef)
    if (!table) {
      console.error('The table is not registered. Please use the register method to register')
    }
    return table
  }
  const delData = async (ids: string | number | string[] | number[]) => {
    let idsLength = 1
    if (ids instanceof Array) {
      idsLength = ids.length
      await Promise.all(
        ids.map(async (id: string | number) => {
          await (config?.delListApi && config?.delListApi(id))
        })
      )
    } else {
      await (config?.delListApi && config?.delListApi(ids))
    }
    ElMessage.success(t('common.delSuccess'))
    // 计算出临界点
    tableObject.currentPage =
      tableObject.total % tableObject.pageSize === idsLength || tableObject.pageSize === 1
        ? tableObject.currentPage > 1
          ? tableObject.currentPage - 1
          : tableObject.currentPage
        : tableObject.currentPage
    await methods.getList()
  }
  const methods = {
    getList: async () => {
      tableObject.loading = true
      const res = await config?.getListApi(unref(paramsObj)).finally(() => {
        tableObject.loading = false
      })
      if (res) {
        tableObject.tableList = (res as unknown as ResponseType).list
        tableObject.total = (res as unknown as ResponseType).total ?? 0
      }
    },
    setProps: async (props: TableProps = {}) => {
      const table = await getTable()
      table?.setProps(props)
    },
    setColumn: async (columnProps: TableSetPropsType[]) => {
      const table = await getTable()
      table?.setColumn(columnProps)
    },
    getSelections: async () => {
      const table = await getTable()
      return (table?.selections || []) as T[]
    },
    // 与Search组件结合
    setSearchParams: (data: Recordable) => {
      tableObject.params = Object.assign(tableObject.params, {
        pageSize: tableObject.pageSize,
        pageNo: 1,
        ...data
      })
      // 页码不等于1时更新页码重新获取数据,页码等于1时重新获取数据
      if (tableObject.currentPage !== 1) {
        tableObject.currentPage = 1
      } else {
        methods.getList()
      }
    },
    // 删除数据
    delList: async (
      ids: string | number | string[] | number[],
      multiple: boolean,
      message = true
    ) => {
      const tableRef = await getTable()
      if (multiple) {
        if (!tableRef?.selections.length) {
          ElMessage.warning(t('common.delNoData'))
          return
        }
      }
      if (message) {
        ElMessageBox.confirm(t('common.delMessage'), t('common.confirmTitle'), {
          confirmButtonText: t('common.ok'),
          cancelButtonText: t('common.cancel'),
          type: 'warning'
        }).then(async () => {
          await delData(ids)
        })
      } else {
        await delData(ids)
      }
    },
    // 导出列表
    exportList: async (fileName: string) => {
      tableObject.exportLoading = true
      ElMessageBox.confirm(t('common.exportMessage'), t('common.confirmTitle'), {
        confirmButtonText: t('common.ok'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      })
        .then(async () => {
          const res = await config?.exportListApi?.(unref(paramsObj) as unknown as T)
          if (res) {
            download.excel(res as unknown as Blob, fileName)
          }
        })
        .finally(() => {
          tableObject.exportLoading = false
        })
    }
  }
  config?.props && methods.setProps(config.props)
  return {
    register,
    elTableRef,
    tableObject,
    methods,
    // add by 芋艿:返回 tableMethods 属性,和 tableObject 更统一
    tableMethods: methods
  }
}
src/hooks/web/useTagsView.ts
New file
@@ -0,0 +1,63 @@
import { useTagsViewStoreWithOut } from '@/store/modules/tagsView'
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'
import { computed, nextTick, unref } from 'vue'
export const useTagsView = () => {
  const tagsViewStore = useTagsViewStoreWithOut()
  const { replace, currentRoute } = useRouter()
  const selectedTag = computed(() => tagsViewStore.getSelectedTag)
  const closeAll = (callback?: Fn) => {
    tagsViewStore.delAllViews()
    callback?.()
  }
  const closeLeft = (callback?: Fn) => {
    tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
    callback?.()
  }
  const closeRight = (callback?: Fn) => {
    tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
    callback?.()
  }
  const closeOther = (callback?: Fn) => {
    tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
    callback?.()
  }
  const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
    if (view?.meta?.affix) return
    tagsViewStore.delView(view || unref(currentRoute))
    callback?.()
  }
  const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
    tagsViewStore.delCachedView()
    const { path, query } = view || unref(currentRoute)
    await nextTick()
    replace({
      path: '/redirect' + path,
      query: query
    })
    callback?.()
  }
  const setTitle = (title: string, path?: string) => {
    tagsViewStore.setTitle(title, path)
  }
  return {
    closeAll,
    closeLeft,
    closeRight,
    closeOther,
    closeCurrent,
    refreshPage,
    setTitle
  }
}
src/hooks/web/useTimeAgo.ts
New file
@@ -0,0 +1,49 @@
import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
const TIME_AGO_MESSAGE_MAP: {
  'zh-CN': UseTimeAgoMessages
  en: UseTimeAgoMessages
} = {
  // @ts-ignore
  'zh-CN': {
    justNow: '刚刚',
    past: (n) => (n.match(/\d/) ? `${n}前` : n),
    future: (n) => (n.match(/\d/) ? `${n}后` : n),
    month: (n, past) => (n === 1 ? (past ? '上个月' : '下个月') : `${n} 个月`),
    year: (n, past) => (n === 1 ? (past ? '去年' : '明年') : `${n} 年`),
    day: (n, past) => (n === 1 ? (past ? '昨天' : '明天') : `${n} 天`),
    week: (n, past) => (n === 1 ? (past ? '上周' : '下周') : `${n} 周`),
    hour: (n) => `${n} 小时`,
    minute: (n) => `${n} 分钟`,
    second: (n) => `${n} 秒`
  },
  // @ts-ignore
  en: {
    justNow: 'just now',
    past: (n) => (n.match(/\d/) ? `${n} ago` : n),
    future: (n) => (n.match(/\d/) ? `in ${n}` : n),
    month: (n, past) =>
      n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`,
    year: (n, past) =>
      n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`,
    day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`),
    week: (n, past) =>
      n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`,
    hour: (n) => `${n} hour${n > 1 ? 's' : ''}`,
    minute: (n) => `${n} minute${n > 1 ? 's' : ''}`,
    second: (n) => `${n} second${n > 1 ? 's' : ''}`
  }
}
export const useTimeAgo = (time: Date | number | string) => {
  const localeStore = useLocaleStoreWithOut()
  const currentLocale = computed(() => localeStore.getCurrentLocale)
  const timeAgo = useTimeAgoCore(time, {
    messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang]
  })
  return timeAgo
}
src/hooks/web/useTitle.ts
New file
@@ -0,0 +1,24 @@
import { watch, ref } from 'vue'
import { isString } from '@/utils/is'
import { useAppStoreWithOut } from '@/store/modules/app'
const appStore = useAppStoreWithOut()
export const useTitle = (newTitle?: string) => {
  const { t } = useI18n()
  const title = ref(
    newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle
  )
  watch(
    title,
    (n, o) => {
      if (isString(n) && n !== o && document) {
        document.title = n
      }
    },
    { immediate: true }
  )
  return title
}
src/hooks/web/useValidator.ts
New file
@@ -0,0 +1,60 @@
import { useI18n } from '@/hooks/web/useI18n'
import { FormItemRule } from 'element-plus'
const { t } = useI18n()
interface LengthRange {
  min: number
  max: number
  message?: string
}
export const useValidator = () => {
  const required = (message?: string): FormItemRule => {
    return {
      required: true,
      message: message || t('common.required')
    }
  }
  const lengthRange = (options: LengthRange): FormItemRule => {
    const { min, max, message } = options
    return {
      min,
      max,
      message: message || t('common.lengthRange', { min, max })
    }
  }
  const notSpace = (message?: string): FormItemRule => {
    return {
      validator: (_, val, callback) => {
        if (val?.indexOf(' ') !== -1) {
          callback(new Error(message || t('common.notSpace')))
        } else {
          callback()
        }
      }
    }
  }
  const notSpecialCharacters = (message?: string): FormItemRule => {
    return {
      validator: (_, val, callback) => {
        if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
          callback(new Error(message || t('common.notSpecialCharacters')))
        } else {
          callback()
        }
      }
    }
  }
  return {
    required,
    lengthRange,
    notSpace,
    notSpecialCharacters
  }
}
src/hooks/web/useWatermark.ts
New file
@@ -0,0 +1,55 @@
const domSymbol = Symbol('watermark-dom')
export function useWatermark(appendEl: HTMLElement | null = document.body) {
  let func: Fn = () => {}
  const id = domSymbol.toString()
  const clear = () => {
    const domId = document.getElementById(id)
    if (domId) {
      const el = appendEl
      el && el.removeChild(domId)
    }
    window.removeEventListener('resize', func)
  }
  const createWatermark = (str: string) => {
    clear()
    const can = document.createElement('canvas')
    can.width = 300
    can.height = 240
    const cans = can.getContext('2d')
    if (cans) {
      cans.rotate((-20 * Math.PI) / 120)
      cans.font = '15px Vedana'
      cans.fillStyle = 'rgba(0, 0, 0, 0.15)'
      cans.textAlign = 'left'
      cans.textBaseline = 'middle'
      cans.fillText(str, can.width / 20, can.height)
    }
    const div = document.createElement('div')
    div.id = id
    div.style.pointerEvents = 'none'
    div.style.top = '0px'
    div.style.left = '0px'
    div.style.position = 'absolute'
    div.style.zIndex = '100000000'
    div.style.width = document.documentElement.clientWidth + 'px'
    div.style.height = document.documentElement.clientHeight + 'px'
    div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat'
    const el = appendEl
    el && el.appendChild(div)
    return id
  }
  function setWatermark(str: string) {
    createWatermark(str)
    func = () => {
      createWatermark(str)
    }
    window.addEventListener('resize', func)
  }
  return { setWatermark, clear }
}
src/main.ts
@@ -21,6 +21,7 @@
// svg图标
import 'virtual:svg-icons-register';
import '@purge-icons/generated'
import ElementIcons from '@/plugins/svgicon';
// permission control
src/utils/is.ts
New file
@@ -0,0 +1,117 @@
// copy to vben-admin
const toString = Object.prototype.toString
export const is = (val: unknown, type: string) => {
  return toString.call(val) === `[object ${type}]`
}
export const isDef = <T = unknown>(val?: T): val is T => {
  return typeof val !== 'undefined'
}
export const isUnDef = <T = unknown>(val?: T): val is T => {
  return !isDef(val)
}
export const isObject = (val: any): val is Record<any, any> => {
  return val !== null && is(val, 'Object')
}
export const isEmpty = <T = unknown>(val: T): val is T => {
  if (val === null) {
    return true
  }
  if (isArray(val) || isString(val)) {
    return val.length === 0
  }
  if (val instanceof Map || val instanceof Set) {
    return val.size === 0
  }
  if (isObject(val)) {
    return Object.keys(val).length === 0
  }
  return false
}
export const isDate = (val: unknown): val is Date => {
  return is(val, 'Date')
}
export const isNull = (val: unknown): val is null => {
  return val === null
}
export const isNullAndUnDef = (val: unknown): val is null | undefined => {
  return isUnDef(val) && isNull(val)
}
export const isNullOrUnDef = (val: unknown): val is null | undefined => {
  return isUnDef(val) || isNull(val)
}
export const isNumber = (val: unknown): val is number => {
  return is(val, 'Number')
}
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
  return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
export const isString = (val: unknown): val is string => {
  return is(val, 'String')
}
export const isFunction = (val: unknown): val is Function => {
  return typeof val === 'function'
}
export const isBoolean = (val: unknown): val is boolean => {
  return is(val, 'Boolean')
}
export const isRegExp = (val: unknown): val is RegExp => {
  return is(val, 'RegExp')
}
export const isArray = (val: any): val is Array<any> => {
  return val && Array.isArray(val)
}
export const isWindow = (val: any): val is Window => {
  return typeof window !== 'undefined' && is(val, 'Window')
}
export const isElement = (val: unknown): val is Element => {
  return isObject(val) && !!val.tagName
}
export const isMap = (val: unknown): val is Map<any, any> => {
  return is(val, 'Map')
}
export const isServer = typeof window === 'undefined'
export const isClient = !isServer
export const isUrl = (path: string): boolean => {
  const reg =
    /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
  return reg.test(path)
}
export const isDark = (): boolean => {
  return window.matchMedia('(prefers-color-scheme: dark)').matches
}
// 是否是图片链接
export const isImgPath = (path: string): boolean => {
  return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path)
}
export const isEmptyVal = (val: any): boolean => {
  return val === '' || val === null || val === undefined
}
src/views/basis/sysParam/config/DetailForm.vue
@@ -1,6 +1,6 @@
<template>
  <div class="p-2">
    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px">
    <Dialog :title="dialog.title" v-model="dialog.visible" width="600px" :draggable="draggable">
      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px" v-loading="formLoading">
        <el-form-item label="参数名称" prop="configName">
          <el-input v-model="formData.configName" placeholder="请输入参数名称"/>
@@ -20,13 +20,11 @@
          <el-input v-model="formData.remark" type="textarea" placeholder="请输入内容"/>
        </el-form-item>
      </el-form>
      <div class="dialog-footer">
        <slot name="footer">
          <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
      <template #footer>
        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="dialog.visible = false">取 消</el-button>
        </slot>
      </div>
    </el-dialog>
      </template>
    </Dialog>
  </div>
</template>
@@ -51,6 +49,9 @@
  visible: false,
  title: ''
});
const resizable = ref(true)
const draggable = ref(true)
const isFullscreen = ref(false)
// 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formLoading = ref(false)
// 表单的类型:create - 新增;update - 修改
src/views/basis/sysParam/config/index.vue
@@ -86,7 +86,7 @@
      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
    </el-card>
    <!--    弹出的详细页面,增加和修改-->
    <detail-form ref="detailFormRef" @success="getList"/>
    <detail-form ref="detailFormRef" @success="getList" apped-to-body/>
  </div>
</template>