From 387083ef40c31fed2f7f5588e8d9048ff97bdf38 Mon Sep 17 00:00:00 2001
From: autumnal_wind@yeah.net <autumnal_wind@yeah.net>
Date: 星期一, 27 五月 2024 16:42:24 +0800
Subject: [PATCH] feat: 参数设置改造(弹出框)
---
src/assets/styles/index.scss | 1
src/components/Icon/src/IconSelect.vue | 229 +++
src/hooks/web/useLocale.ts | 35
src/components/Dialog/index.ts | 3
src/hooks/web/useConfigGlobal.ts | 9
src/views/basis/sysParam/config/DetailForm.vue | 17
src/components/Icon/src/Icon.vue | 86 +
src/hooks/web/useNProgress.ts | 33
src/hooks/web/useEmitt.ts | 22
src/hooks/web/useForm.ts | 94 +
src/views/basis/sysParam/config/index.vue | 2
src/hooks/web/useI18n.ts | 53
src/hooks/web/usePageLoading.ts | 18
src/components/Icon/index.ts | 4
src/hooks/web/useDesign.ts | 18
src/components/Dialog/src/Dialog.vue | 140 ++
src/hooks/web/useGuide.ts | 49
src/assets/styles/variables.scss | 4
src/hooks/web/useTable.ts | 223 +++
src/hooks/web/useTagsView.ts | 63
src/hooks/web/useCache.ts | 39
src/hooks/web/useNetwork.ts | 21
src/hooks/web/useValidator.ts | 60
src/hooks/web/useIcon.ts | 8
src/hooks/web/useMessage.ts | 95 +
src/hooks/web/useCrudSchemas.ts | 326 ++++
src/utils/is.ts | 117 +
src/main.ts | 1
src/hooks/web/useNow.ts | 60
src/components/Icon/src/data.ts | 1961 +++++++++++++++++++++++++++++
src/hooks/web/useTimeAgo.ts | 49
src/hooks/event/useScrollTo.ts | 60
src/hooks/web/useWatermark.ts | 55
package.json | 1
src/assets/styles/global.module.scss | 6
src/hooks/web/useTitle.ts | 24
36 files changed, 3,977 insertions(+), 9 deletions(-)
diff --git a/package.json b/package.json
index 87c7fe9..3498421 100644
--- a/package.json
+++ b/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",
diff --git a/src/assets/styles/global.module.scss b/src/assets/styles/global.module.scss
new file mode 100644
index 0000000..8448a92
--- /dev/null
+++ b/src/assets/styles/global.module.scss
@@ -0,0 +1,6 @@
+@import './variables.scss';
+// 导出变量
+:export {
+ namespace: $namespace;
+ elNamespace: $elNamespace;
+}
diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss
index 7494381..3d52e5a 100644
--- a/src/assets/styles/index.scss
+++ b/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%;
diff --git a/src/assets/styles/variables.scss b/src/assets/styles/variables.scss
new file mode 100644
index 0000000..00b66f1
--- /dev/null
+++ b/src/assets/styles/variables.scss
@@ -0,0 +1,4 @@
+// 命名空间
+$namespace: v;
+// el命名空间
+$elNamespace: el;
diff --git a/src/components/Dialog/index.ts b/src/components/Dialog/index.ts
new file mode 100644
index 0000000..1655dad
--- /dev/null
+++ b/src/components/Dialog/index.ts
@@ -0,0 +1,3 @@
+import Dialog from './src/Dialog.vue'
+
+export { Dialog }
diff --git a/src/components/Dialog/src/Dialog.vue b/src/components/Dialog/src/Dialog.vue
new file mode 100644
index 0000000..9a8ca54
--- /dev/null
+++ b/src/components/Dialog/src/Dialog.vue
@@ -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>
diff --git a/src/components/Icon/index.ts b/src/components/Icon/index.ts
new file mode 100644
index 0000000..33d1de3
--- /dev/null
+++ b/src/components/Icon/index.ts
@@ -0,0 +1,4 @@
+import Icon from './src/Icon.vue'
+import IconSelect from './src/IconSelect.vue'
+
+export { Icon, IconSelect }
diff --git a/src/components/Icon/src/Icon.vue b/src/components/Icon/src/Icon.vue
new file mode 100644
index 0000000..4246539
--- /dev/null
+++ b/src/components/Icon/src/Icon.vue
@@ -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>
diff --git a/src/components/Icon/src/IconSelect.vue b/src/components/Icon/src/IconSelect.vue
new file mode 100644
index 0000000..d4a5b07
--- /dev/null
+++ b/src/components/Icon/src/IconSelect.vue
@@ -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>
diff --git a/src/components/Icon/src/data.ts b/src/components/Icon/src/data.ts
new file mode 100644
index 0000000..2a4ed5a
--- /dev/null
+++ b/src/components/Icon/src/data.ts
@@ -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'
+ ]
+}
diff --git a/src/hooks/event/useScrollTo.ts b/src/hooks/event/useScrollTo.ts
new file mode 100644
index 0000000..92aec87
--- /dev/null
+++ b/src/hooks/event/useScrollTo.ts
@@ -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 }
+}
diff --git a/src/hooks/web/useCache.ts b/src/hooks/web/useCache.ts
new file mode 100644
index 0000000..4f39f30
--- /dev/null
+++ b/src/hooks/web/useCache.ts
@@ -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 登录表单
+}
diff --git a/src/hooks/web/useConfigGlobal.ts b/src/hooks/web/useConfigGlobal.ts
new file mode 100644
index 0000000..afb3db3
--- /dev/null
+++ b/src/hooks/web/useConfigGlobal.ts
@@ -0,0 +1,9 @@
+import { ConfigGlobalTypes } from '@/types/configGlobal'
+
+export const useConfigGlobal = () => {
+ const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes
+
+ return {
+ configGlobal
+ }
+}
diff --git a/src/hooks/web/useCrudSchemas.ts b/src/hooks/web/useCrudSchemas.ts
new file mode 100644
index 0000000..458b57e
--- /dev/null
+++ b/src/hooks/web/useCrudSchemas.ts
@@ -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)
+}
diff --git a/src/hooks/web/useDesign.ts b/src/hooks/web/useDesign.ts
new file mode 100644
index 0000000..5029680
--- /dev/null
+++ b/src/hooks/web/useDesign.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useEmitt.ts b/src/hooks/web/useEmitt.ts
new file mode 100644
index 0000000..d4efea7
--- /dev/null
+++ b/src/hooks/web/useEmitt.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useForm.ts b/src/hooks/web/useForm.ts
new file mode 100644
index 0000000..53a8a94
--- /dev/null
+++ b/src/hooks/web/useForm.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useGuide.ts b/src/hooks/web/useGuide.ts
new file mode 100644
index 0000000..7fd2fb0
--- /dev/null
+++ b/src/hooks/web/useGuide.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useI18n.ts b/src/hooks/web/useI18n.ts
new file mode 100644
index 0000000..d1ab70f
--- /dev/null
+++ b/src/hooks/web/useI18n.ts
@@ -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
diff --git a/src/hooks/web/useIcon.ts b/src/hooks/web/useIcon.ts
new file mode 100644
index 0000000..3500204
--- /dev/null
+++ b/src/hooks/web/useIcon.ts
@@ -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)
+}
diff --git a/src/hooks/web/useLocale.ts b/src/hooks/web/useLocale.ts
new file mode 100644
index 0000000..c65070e
--- /dev/null
+++ b/src/hooks/web/useLocale.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useMessage.ts b/src/hooks/web/useMessage.ts
new file mode 100644
index 0000000..ac2b552
--- /dev/null
+++ b/src/hooks/web/useMessage.ts
@@ -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'
+ })
+ }
+ }
+}
diff --git a/src/hooks/web/useNProgress.ts b/src/hooks/web/useNProgress.ts
new file mode 100644
index 0000000..6d8c0b9
--- /dev/null
+++ b/src/hooks/web/useNProgress.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useNetwork.ts b/src/hooks/web/useNetwork.ts
new file mode 100644
index 0000000..66fa446
--- /dev/null
+++ b/src/hooks/web/useNetwork.ts
@@ -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 }
diff --git a/src/hooks/web/useNow.ts b/src/hooks/web/useNow.ts
new file mode 100644
index 0000000..09d3176
--- /dev/null
+++ b/src/hooks/web/useNow.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/usePageLoading.ts b/src/hooks/web/usePageLoading.ts
new file mode 100644
index 0000000..bb89457
--- /dev/null
+++ b/src/hooks/web/usePageLoading.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useTable.ts b/src/hooks/web/useTable.ts
new file mode 100644
index 0000000..361dd67
--- /dev/null
+++ b/src/hooks/web/useTable.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useTagsView.ts b/src/hooks/web/useTagsView.ts
new file mode 100644
index 0000000..31eadb0
--- /dev/null
+++ b/src/hooks/web/useTagsView.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useTimeAgo.ts b/src/hooks/web/useTimeAgo.ts
new file mode 100644
index 0000000..a6da281
--- /dev/null
+++ b/src/hooks/web/useTimeAgo.ts
@@ -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
+}
diff --git a/src/hooks/web/useTitle.ts b/src/hooks/web/useTitle.ts
new file mode 100644
index 0000000..020a9b7
--- /dev/null
+++ b/src/hooks/web/useTitle.ts
@@ -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
+}
diff --git a/src/hooks/web/useValidator.ts b/src/hooks/web/useValidator.ts
new file mode 100644
index 0000000..151e35b
--- /dev/null
+++ b/src/hooks/web/useValidator.ts
@@ -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
+ }
+}
diff --git a/src/hooks/web/useWatermark.ts b/src/hooks/web/useWatermark.ts
new file mode 100644
index 0000000..4a31359
--- /dev/null
+++ b/src/hooks/web/useWatermark.ts
@@ -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 }
+}
diff --git a/src/main.ts b/src/main.ts
index 0ad939a..c0297f2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -21,6 +21,7 @@
// svg图标
import 'virtual:svg-icons-register';
+import '@purge-icons/generated'
import ElementIcons from '@/plugins/svgicon';
// permission control
diff --git a/src/utils/is.ts b/src/utils/is.ts
new file mode 100644
index 0000000..eec86a9
--- /dev/null
+++ b/src/utils/is.ts
@@ -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
+}
diff --git a/src/views/basis/sysParam/config/DetailForm.vue b/src/views/basis/sysParam/config/DetailForm.vue
index f1346c6..f10580d 100644
--- a/src/views/basis/sysParam/config/DetailForm.vue
+++ b/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>
- <el-button @click="dialog.visible = false">取 消</el-button>
- </slot>
- </div>
- </el-dialog>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+ <el-button @click="dialog.visible = false">取 消</el-button>
+ </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 - 修改
diff --git a/src/views/basis/sysParam/config/index.vue b/src/views/basis/sysParam/config/index.vue
index 193550f..eab1c24 100644
--- a/src/views/basis/sysParam/config/index.vue
+++ b/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>
--
Gitblit v1.8.0