mirror of
https://github.com/fugary/simple-element-plus-template.git
synced 2025-12-31 03:17:49 +00:00
1. 控件优化
2. 增加monaco-editor、echarts等
This commit is contained in:
@@ -70,6 +70,22 @@ const allMenus = [
|
|||||||
{
|
{
|
||||||
id: 24,
|
id: 24,
|
||||||
parentId: 2,
|
parentId: 2,
|
||||||
|
iconCls: 'Edit',
|
||||||
|
nameCn: '编辑器示例',
|
||||||
|
nameEn: 'Editors',
|
||||||
|
menuUrl: '/editors'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 25,
|
||||||
|
parentId: 2,
|
||||||
|
iconCls: 'PieChartSharp',
|
||||||
|
nameCn: '图表示例',
|
||||||
|
nameEn: 'Charts',
|
||||||
|
menuUrl: '/charts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30,
|
||||||
|
parentId: 2,
|
||||||
iconCls: 'TipsAndUpdatesOutlined',
|
iconCls: 'TipsAndUpdatesOutlined',
|
||||||
nameCn: '其他示例',
|
nameCn: '其他示例',
|
||||||
nameEn: 'Others',
|
nameEn: 'Others',
|
||||||
|
|||||||
6318
package-lock.json
generated
6318
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -1,39 +1,52 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-element-plus-template",
|
"name": "simple-element-plus-template",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
|
||||||
|
"hmr": "vite --debug hmr"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@guolao/vue-monaco-editor": "^1.5.1",
|
||||||
|
"@howiefh/ant-path-matcher": "^0.0.4",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"axios": "^1.6.8",
|
"async-validator": "^4.2.5",
|
||||||
"dayjs": "^1.11.10",
|
"axios": "^1.7.2",
|
||||||
"element-plus": "^2.6.3",
|
"dayjs": "^1.11.11",
|
||||||
"lodash": "^4.17.21",
|
"echarts": "^5.5.0",
|
||||||
"mockjs": "^1.1.0",
|
"element-plus": "^2.7.5",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"monaco-editor": "^0.47.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"numeral": "^2.0.6",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
|
"ua-parser-js": "^1.0.38",
|
||||||
"vite-plugin-mock": "^3.0.1",
|
"vite-plugin-mock": "^3.0.1",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.29",
|
||||||
"vue-i18n": "^9.10.2",
|
"vue-echarts": "^6.7.3",
|
||||||
"vue-router": "^4.3.0",
|
"vue-i18n": "^9.13.1",
|
||||||
|
"vue-router": "^4.3.3",
|
||||||
"vue-virtual-scroller": "^2.0.0-beta.8"
|
"vue-virtual-scroller": "^2.0.0-beta.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.10.1",
|
"@rollup/rollup-win32-x64-msvc": "^4.18.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
"@rushstack/eslint-patch": "^1.10.3",
|
||||||
"@typescript-eslint/parser": "^7.4.0",
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@typescript-eslint/parser": "^7.13.1",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
"@vue/eslint-config-standard": "^8.0.1",
|
"@vue/eslint-config-standard": "^8.0.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.24.0",
|
"eslint-plugin-vue": "^9.26.0",
|
||||||
"typescript": "^5.4.3",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"vite": "^5.2.7"
|
"typescript": "^5.4.5",
|
||||||
|
"vite": "^5.3.1",
|
||||||
|
"vite-plugin-eslint": "^1.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
import { $changeLocale, elementLocale, $i18nBundle } from '@/messages'
|
import { $changeLocale, elementLocale } from '@/messages'
|
||||||
import { useTitle } from '@vueuse/core'
|
import { useTitle } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { calcRouteTitle, useRoutePopStateEvent } from '@/route/RouteUtils'
|
||||||
|
|
||||||
const globalConfigStore = useGlobalConfigStore()
|
const globalConfigStore = useGlobalConfigStore()
|
||||||
$changeLocale(globalConfigStore.currentLocale)
|
$changeLocale(globalConfigStore.currentLocale)
|
||||||
const title = computed(() => $i18nBundle('common.label.title'))
|
const route = useRoute()
|
||||||
|
const title = computed(() => calcRouteTitle(route))
|
||||||
useTitle(title)
|
useTitle(title)
|
||||||
|
useRoutePopStateEvent()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -145,6 +145,17 @@ html, body, #app, .index-container {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-center{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center-col{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
--el-dialog-border-radius: var(--el-border-radius-large);
|
--el-dialog-border-radius: var(--el-border-radius-large);
|
||||||
}
|
}
|
||||||
@@ -153,6 +164,24 @@ html, body, #app, .index-container {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-dialog.is-fullscreen .el-dialog__header,
|
||||||
|
.el-dialog.is-fullscreen .el-dialog__footer{
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right:0;
|
||||||
|
z-index: 999;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.el-dialog.is-fullscreen .el-dialog__footer{
|
||||||
|
bottom: var(--el-dialog-padding-primary);
|
||||||
|
}
|
||||||
|
.el-dialog.is-fullscreen .dialog-footer{
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.el-dialog.is-fullscreen .el-dialog__body{
|
||||||
|
padding: 48px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-list::-webkit-scrollbar {
|
.icon-list::-webkit-scrollbar {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@@ -184,6 +213,10 @@ html, body, #app, .index-container {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-main {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.container-center {
|
.container-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,4 +1,38 @@
|
|||||||
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
||||||
|
import { $coreHideLoading, $coreShowLoading } from '@/utils'
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
import 'nprogress/nprogress.css'
|
||||||
|
import { GLOBAL_ROUTE_LOADING, GLOBAL_ROUTE_NEW_LOADING } from '@/config'
|
||||||
|
import { $changeLocale } from '@/messages'
|
||||||
|
import { GlobalLocales } from '@/consts/GlobalConstants'
|
||||||
|
import { useBreadcrumbConfigStore } from '@/stores/BreadcrumbConfigStore'
|
||||||
|
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||||
|
|
||||||
|
NProgress.configure({ showSpinner: false, trickleSpeed: 500 })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否开启路由的loading
|
||||||
|
* @param route
|
||||||
|
* @return {*|boolean}
|
||||||
|
*/
|
||||||
|
const checkRouteLoading = route => route?.meta?.loading ?? GLOBAL_ROUTE_LOADING
|
||||||
|
|
||||||
|
const startRouteLoading = (route) => {
|
||||||
|
if (checkRouteLoading(route)) {
|
||||||
|
NProgress.start()
|
||||||
|
if (GLOBAL_ROUTE_NEW_LOADING) {
|
||||||
|
$coreShowLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const endRouteLoading = (route) => {
|
||||||
|
if (checkRouteLoading(route)) {
|
||||||
|
NProgress.done()
|
||||||
|
if (GLOBAL_ROUTE_NEW_LOADING) {
|
||||||
|
$coreHideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查路有权限
|
* 检查路有权限
|
||||||
@@ -6,14 +40,32 @@ import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
|||||||
* @param from 出事路由
|
* @param from 出事路由
|
||||||
* @returns {{name: string}|boolean}
|
* @returns {{name: string}|boolean}
|
||||||
*/
|
*/
|
||||||
export const checkRouteAuthority = (to, from) => {
|
export const checkRouteAuthority = async (to) => {
|
||||||
|
startRouteLoading(to)
|
||||||
const loginConfigStore = useLoginConfigStore()
|
const loginConfigStore = useLoginConfigStore()
|
||||||
|
if (to.meta?.beforeLogin) { // 登录前的路由添加meta信息:beforeLogin: true
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (loginConfigStore.isLoginIn()) {
|
if (loginConfigStore.isLoginIn()) {
|
||||||
// check权限
|
// check权限
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (to.meta?.beforeLogin) { // 登录前的路由添加meta信息:beforeLogin: true
|
endRouteLoading(to)
|
||||||
return true
|
|
||||||
}
|
|
||||||
return { name: 'Login' }
|
return { name: 'Login' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processRouteSavedParam = (to, from) => {
|
||||||
|
useGlobalSearchParamStore().savedParamRouteInfo = { // 路由后退处理
|
||||||
|
to, from
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const processRouteLoading = (to, from) => {
|
||||||
|
endRouteLoading(to)
|
||||||
|
if (to.query?.language && Object.values(GlobalLocales).includes(to.query.language)) {
|
||||||
|
$changeLocale(to.query?.language)
|
||||||
|
}
|
||||||
|
const { clearBreadcrumbConfig } = useBreadcrumbConfigStore()
|
||||||
|
clearBreadcrumbConfig() // 清理面包屑label
|
||||||
|
processRouteSavedParam(to, from)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { debounce, isEmpty, isObject, cloneDeep, chunk } from 'lodash'
|
import { debounce, isEmpty, isObject, cloneDeep, chunk } from 'lodash-es'
|
||||||
import { onClickOutside, onKeyStroke, useVModel } from '@vueuse/core'
|
import { onClickOutside, onKeyStroke, useVModel } from '@vueuse/core'
|
||||||
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus'
|
import { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus'
|
||||||
|
|
||||||
@@ -76,6 +76,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
inputAsValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null
|
default: null
|
||||||
@@ -127,6 +131,7 @@ const selectPageData = ref({})
|
|||||||
const selectPageTab = ref(null)
|
const selectPageTab = ref(null)
|
||||||
const popoverVisible = ref(false)
|
const popoverVisible = ref(false)
|
||||||
const autocompletePopover = ref()
|
const autocompletePopover = ref()
|
||||||
|
const inputRef = ref()
|
||||||
const defaultAutoPage = {
|
const defaultAutoPage = {
|
||||||
pageSize: props.autocompleteConfig?.pageSize || 8,
|
pageSize: props.autocompleteConfig?.pageSize || 8,
|
||||||
pageNumber: 1
|
pageNumber: 1
|
||||||
@@ -192,6 +197,8 @@ const onInputKeywords = debounce((input) => {
|
|||||||
}
|
}
|
||||||
if (!val && input) {
|
if (!val && input) {
|
||||||
onSelectData()
|
onSelectData()
|
||||||
|
} else if (input && props.inputAsValue && props.useIdModel) {
|
||||||
|
vModel.value = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, props.debounceTime)
|
}, props.debounceTime)
|
||||||
@@ -211,6 +218,16 @@ onMounted(() => {
|
|||||||
popoverVisible.value = false
|
popoverVisible.value = false
|
||||||
})
|
})
|
||||||
setAutocompleteLabel(calcDefaultLabel.value)
|
setAutocompleteLabel(calcDefaultLabel.value)
|
||||||
|
// 向下按键移动元素
|
||||||
|
onKeyStroke('ArrowDown', () => moveSelection(true), { target: inputRef.value })
|
||||||
|
// 向上按键移动元素
|
||||||
|
onKeyStroke('ArrowUp', () => moveSelection(false), { target: inputRef.value })
|
||||||
|
// 选中回车
|
||||||
|
onKeyStroke('Enter', (event) => {
|
||||||
|
onSelectData(currentOnRow.value)
|
||||||
|
event?.stopImmediatePropagation()
|
||||||
|
event?.stopPropagation()
|
||||||
|
}, { target: inputRef.value })
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => popoverVisible.value, (val) => {
|
watch(() => popoverVisible.value, (val) => {
|
||||||
@@ -229,6 +246,9 @@ watch(() => props.modelValue, (value) => {
|
|||||||
if (isEmpty(value)) {
|
if (isEmpty(value)) {
|
||||||
vModel.value = null
|
vModel.value = null
|
||||||
}
|
}
|
||||||
|
} else if (!value) {
|
||||||
|
setAutocompleteLabel('')
|
||||||
|
vModel.value = value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -293,18 +313,9 @@ const moveSelection = function (down) {
|
|||||||
currentOnIndex.value = -1
|
currentOnIndex.value = -1
|
||||||
currentOnRow.value = null
|
currentOnRow.value = null
|
||||||
}
|
}
|
||||||
tableRef.value.table?.setCurrentRow(currentOnRow.value)
|
tableRef.value?.table?.setCurrentRow(currentOnRow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向下按键移动元素
|
|
||||||
onKeyStroke('ArrowDown', () => moveSelection(true))
|
|
||||||
// 向上按键移动元素
|
|
||||||
onKeyStroke('ArrowUp', () => moveSelection(false))
|
|
||||||
// 选中回车
|
|
||||||
onKeyStroke('Enter', () => {
|
|
||||||
onSelectData(currentOnRow.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
//= ===============selectPage处理=================//
|
//= ===============selectPage处理=================//
|
||||||
const selectPagePageConfig = ref({})
|
const selectPagePageConfig = ref({})
|
||||||
const parsedSelectPageData = computed(() => {
|
const parsedSelectPageData = computed(() => {
|
||||||
@@ -376,6 +387,7 @@ watch(() => props.autocompleteConfig, (autocompleteConfig) => {
|
|||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-input
|
<el-input
|
||||||
|
ref="inputRef"
|
||||||
v-model="keywords"
|
v-model="keywords"
|
||||||
:clearable="clearable"
|
:clearable="clearable"
|
||||||
:placeholder="placeholder||title"
|
:placeholder="placeholder||title"
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export interface CommonAutocompleteProps {
|
|||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
// input自定义属性
|
// input自定义属性
|
||||||
inputAttrs?: InputProps;
|
inputAttrs?: InputProps;
|
||||||
|
// 输入当做值的特殊模式
|
||||||
|
inputAsValue?: boolean;
|
||||||
// 验证事件
|
// 验证事件
|
||||||
validateEvent?: boolean;
|
validateEvent?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { parsePathParams, useMenuInfo, useMenuName } from '@/components/utils'
|
import { calcMatchedRoutes } from '@/route/RouteUtils'
|
||||||
|
|
||||||
const tabsViewStore = useTabsViewStore()
|
const props = defineProps({
|
||||||
|
labelConfig: {
|
||||||
|
type: [Object, Array],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
showIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
const exists = []
|
return calcMatchedRoutes(route, props.labelConfig)
|
||||||
return route.matched.map((item, index) => {
|
|
||||||
item = index === route.matched.length - 1 ? route : item
|
|
||||||
const menuInfo = useMenuInfo(item)
|
|
||||||
let icon = ''
|
|
||||||
if (menuInfo && menuInfo.icon) {
|
|
||||||
icon = menuInfo.icon
|
|
||||||
} else if (item.meta && item.meta.icon) {
|
|
||||||
icon = item.meta.icon
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
path: parsePathParams(item.path, route.params),
|
|
||||||
menuName: useMenuName(item),
|
|
||||||
icon
|
|
||||||
}
|
|
||||||
}).filter(item => {
|
|
||||||
const notExist = !exists.includes(item.menuName)
|
|
||||||
if (notExist) {
|
|
||||||
exists.push(item.menuName)
|
|
||||||
}
|
|
||||||
return notExist && !item.menuName.endsWith('Base')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -40,12 +26,12 @@ const breadcrumbs = computed(() => {
|
|||||||
class="common-breadcrumb"
|
class="common-breadcrumb"
|
||||||
>
|
>
|
||||||
<el-breadcrumb-item
|
<el-breadcrumb-item
|
||||||
v-for="item in breadcrumbs"
|
v-for="(item, index) in breadcrumbs"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
:to="{ path: item.path }"
|
:to="index!==breadcrumbs.length-1?{ path: item.path }:undefined"
|
||||||
>
|
>
|
||||||
<common-icon
|
<common-icon
|
||||||
v-if="tabsViewStore.isShowTabIcon&&item.icon"
|
v-if="showIcon&&item.icon"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
/>
|
/>
|
||||||
{{ item.menuName }}
|
{{ item.menuName }}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { get, isArray, isObject, set } from 'lodash'
|
import { get, isArray, isFunction, isObject, set, cloneDeep } from 'lodash-es'
|
||||||
import { computed, onMounted, useSlots } from 'vue'
|
import { computed, onMounted, useSlots } from 'vue'
|
||||||
import cloneDeep from 'lodash/cloneDeep'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +51,13 @@ const isUnlimited = computed(() => {
|
|||||||
})
|
})
|
||||||
return unlimited
|
return unlimited
|
||||||
}
|
}
|
||||||
|
} else if (filterType === 'slider') {
|
||||||
|
if (!value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (option?.attrs?.range) {
|
||||||
|
return value?.[0] === (option.attrs?.min || 0) && value?.[1] === (option.attrs?.max || 10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return !value || !value.length
|
return !value || !value.length
|
||||||
})
|
})
|
||||||
@@ -64,8 +70,16 @@ const setUnlimited = () => {
|
|||||||
value = []
|
value = []
|
||||||
} else if (filterType === 'common-tab-filter') {
|
} else if (filterType === 'common-tab-filter') {
|
||||||
value = {}
|
value = {}
|
||||||
|
} else if (filterType === 'slider') {
|
||||||
|
// slider range 默认值是[0,100]
|
||||||
|
if (option?.attrs?.range) {
|
||||||
|
value = [option.attrs?.min || 0, option.attrs?.max || 100]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
set(props.model, option.prop, value)
|
set(props.model, option.prop, value)
|
||||||
|
if (isFunction(option.change)) {
|
||||||
|
option.change(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initFilterModel = () => {
|
const initFilterModel = () => {
|
||||||
|
|||||||
@@ -1,23 +1,5 @@
|
|||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
component: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="common-form-label">
|
<div class="common-form-label">
|
||||||
<slot />
|
<slot />
|
||||||
<component
|
|
||||||
:is="component"
|
|
||||||
v-if="component"
|
|
||||||
v-bind="$attrs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const props = defineProps({
|
|||||||
default: undefined
|
default: undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
const vModel = useVModel(props, 'modelValue', emit)
|
const vModel = useVModel(props, 'modelValue', emit)
|
||||||
|
|
||||||
const childTypeMapping = { // 自动映射子元素类型,配置的时候可以不写type
|
const childTypeMapping = { // 自动映射子元素类型,配置的时候可以不写type
|
||||||
@@ -72,7 +72,7 @@ const inputType = computed(() => useInputType({ type: props.type }))
|
|||||||
:is="inputType"
|
:is="inputType"
|
||||||
v-if="vModel"
|
v-if="vModel"
|
||||||
v-model="vModel[tab.prop]"
|
v-model="vModel[tab.prop]"
|
||||||
@change="vModel._tabFilter=true"
|
@change="vModel.isTabFilter=true;$emit('change', vModel)"
|
||||||
>
|
>
|
||||||
<control-child
|
<control-child
|
||||||
v-for="(childItem, childIdx) in tab.children"
|
v-for="(childItem, childIdx) in tab.children"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||||
|
import { isFunction } from 'lodash-es'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{option:CommonFormOption}}
|
* @type {{option:CommonFormOption}}
|
||||||
@@ -24,6 +25,15 @@ const label = computed(() => {
|
|||||||
}
|
}
|
||||||
return option.label
|
return option.label
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tooltipFunc = ($event) => {
|
||||||
|
$event.preventDefault()
|
||||||
|
$event.stopImmediatePropagation()
|
||||||
|
if (isFunction(props.option.tooltipFunc)) {
|
||||||
|
props.option.tooltipFunc($event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -31,9 +41,30 @@ const label = computed(() => {
|
|||||||
:is="inputType"
|
:is="inputType"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
:disabled="option.disabled"
|
||||||
|
:readonly="option.readonly"
|
||||||
v-bind="option.attrs"
|
v-bind="option.attrs"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
<el-tooltip
|
||||||
|
v-if="option.tooltip||option.tooltipFunc"
|
||||||
|
class="box-item"
|
||||||
|
effect="dark"
|
||||||
|
:disabled="!option.tooltip"
|
||||||
|
:content="option.tooltip"
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<el-link
|
||||||
|
:underline="false"
|
||||||
|
@click="tooltipFunc($event)"
|
||||||
|
>
|
||||||
|
<common-icon
|
||||||
|
icon="QuestionFilled"
|
||||||
|
/>
|
||||||
|
</el-link>
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { computed, isVNode, ref, watch } from 'vue'
|
|||||||
import { $i18nBundle } from '@/messages'
|
import { $i18nBundle } from '@/messages'
|
||||||
import ControlChild from '@/components/common-form-control/control-child.vue'
|
import ControlChild from '@/components/common-form-control/control-child.vue'
|
||||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||||
import cloneDeep from 'lodash/cloneDeep'
|
import { cloneDeep, get, isFunction, set, isArray, isString } from 'lodash-es'
|
||||||
import { get, isFunction, set } from 'lodash'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,13 +32,31 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const needProcessValue = option => option.trim || option.upperCase || option.lowerCase
|
||||||
|
const processValue = (value, option) => {
|
||||||
|
if (value && isString(value)) {
|
||||||
|
value = option.trim ? value.trim() : value
|
||||||
|
value = option.upperCase ? value.toUpperCase() : value
|
||||||
|
value = option.lowerCase ? value.toLowerCase() : value
|
||||||
|
modelValue.value = value
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
const calcOption = computed(() => {
|
const calcOption = computed(() => {
|
||||||
let option = props.option
|
let option = { ...props.option }
|
||||||
if (isFunction(option.dynamicOption)) {
|
if (isFunction(option.dynamicOption)) {
|
||||||
option = { ...option, ...option.dynamicOption(props.model, option, props.addInfo) }
|
option = { ...option, ...option.dynamicOption(props.model, option, props.addInfo) }
|
||||||
} else if (isFunction(option.dynamicAttrs)) {
|
} else if (isFunction(option.dynamicAttrs)) {
|
||||||
option = { ...option, attrs: { ...option.attrs, ...option.dynamicAttrs(props.model, option, props.addInfo) } }
|
option = { ...option, attrs: { ...option.attrs, ...option.dynamicAttrs(props.model, option, props.addInfo) } }
|
||||||
}
|
}
|
||||||
|
if (needProcessValue(option)) {
|
||||||
|
const change = option.change
|
||||||
|
option.change = value => {
|
||||||
|
value = processValue(value, option)
|
||||||
|
isFunction(change) && change(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
return option
|
return option
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -47,14 +65,14 @@ const inputType = computed(() => useInputType(calcOption.value))
|
|||||||
const modelAttrs = computed(() => {
|
const modelAttrs = computed(() => {
|
||||||
const option = calcOption.value
|
const option = calcOption.value
|
||||||
const attrs = { ...option.attrs }
|
const attrs = { ...option.attrs }
|
||||||
if (attrs.clearable === undefined && ['el-input', 'el-select', 'el-select-v2', 'common-autocomplete', 'el-autocomplete', 'el-cascader', 'el-tree-select'].includes(inputType.value)) {
|
if (['el-input', 'el-select', 'el-select-v2', 'common-autocomplete', 'el-autocomplete', 'el-cascader', 'el-tree-select'].includes(inputType.value)) {
|
||||||
attrs.clearable = true
|
attrs.clearable = attrs.clearable ?? true
|
||||||
}
|
}
|
||||||
if (inputType.value === 'common-autocomplete' && option.getAutocompleteLabel) {
|
if (inputType.value === 'common-autocomplete' && option.getAutocompleteLabel) {
|
||||||
attrs.defaultLabel = option.getAutocompleteLabel(props.model, option)
|
attrs.defaultLabel = option.getAutocompleteLabel(props.model, option)
|
||||||
}
|
}
|
||||||
if (inputType.value === 'el-date-picker') {
|
if (inputType.value === 'el-date-picker') {
|
||||||
attrs.disabledDate = (date) => {
|
attrs.disabledDate = attrs.disabledDate || ((date) => {
|
||||||
const option = calcOption.value
|
const option = calcOption.value
|
||||||
let result = false
|
let result = false
|
||||||
if (option.minDate) {
|
if (option.minDate) {
|
||||||
@@ -64,25 +82,22 @@ const modelAttrs = computed(() => {
|
|||||||
result = date.getTime() > dayjs(option.maxDate).startOf('d').toDate().getTime()
|
result = date.getTime() > dayjs(option.maxDate).startOf('d').toDate().getTime()
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
})
|
||||||
|
const defaultValue = attrs.defaultValue || modelValue.value || option.minDate
|
||||||
|
if (defaultValue && !isArray(defaultValue)) {
|
||||||
|
attrs.defaultValue = dayjs(defaultValue).toDate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const defaultValue = modelValue.value || option.minDate
|
|
||||||
if (defaultValue) {
|
|
||||||
attrs.defaultValue = dayjs(defaultValue).toDate()
|
|
||||||
}
|
|
||||||
return attrs
|
return attrs
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => [inputType.value, calcOption.value.minDate, calcOption.value.maxDate], ([type, minDate, maxDate]) => {
|
watch([inputType, () => calcOption.value.minDate, () => calcOption.value.maxDate], ([type]) => {
|
||||||
const option = calcOption.value
|
const option = calcOption.value
|
||||||
const date = modelValue.value
|
const date = modelValue.value
|
||||||
if (type === 'el-date-picker' && date && !option.disabled && option.clearInvalidDate !== false) {
|
if (type === 'el-date-picker' && date && !option.disabled && option.clearInvalidDate !== false) {
|
||||||
let invalid = false
|
let invalid = false
|
||||||
if (minDate) {
|
if (isFunction(modelAttrs.value.disabledDate)) {
|
||||||
invalid = dayjs(date).isBefore(dayjs(option.minDate).startOf('d'))
|
invalid = modelAttrs.value.disabledDate(dayjs(date).toDate())
|
||||||
}
|
|
||||||
if (invalid && maxDate) {
|
|
||||||
invalid = dayjs(date).isAfter(dayjs(option.maxDate).startOf('d'))
|
|
||||||
}
|
}
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
modelValue.value = undefined
|
modelValue.value = undefined
|
||||||
@@ -126,7 +141,8 @@ const childTypeMapping = { // 自动映射子元素类型,配置的时候可
|
|||||||
|
|
||||||
const children = computed(() => {
|
const children = computed(() => {
|
||||||
const option = calcOption.value
|
const option = calcOption.value
|
||||||
const result = option.children || [] // 初始化一些默认值
|
let result = option.children || [] // 初始化一些默认值
|
||||||
|
result = result.filter(childItem => childItem.enabled !== false)
|
||||||
result.forEach(childItem => {
|
result.forEach(childItem => {
|
||||||
if (!childItem.type) {
|
if (!childItem.type) {
|
||||||
childItem.type = childTypeMapping[option.type]
|
childItem.type = childTypeMapping[option.type]
|
||||||
@@ -141,7 +157,7 @@ const rules = computed(() => {
|
|||||||
const option = calcOption.value
|
const option = calcOption.value
|
||||||
let _rules = cloneDeep(option.rules || [])
|
let _rules = cloneDeep(option.rules || [])
|
||||||
if (option.prop) {
|
if (option.prop) {
|
||||||
if (option.required !== undefined) {
|
if (option.required) {
|
||||||
const label = option.label || toLabelByKey(option.labelKey)
|
const label = option.label || toLabelByKey(option.labelKey)
|
||||||
_rules = [{
|
_rules = [{
|
||||||
trigger: option.trigger,
|
trigger: option.trigger,
|
||||||
@@ -149,7 +165,7 @@ const rules = computed(() => {
|
|||||||
message: $i18nBundle('common.msg.nonNull', [label])
|
message: $i18nBundle('common.msg.nonNull', [label])
|
||||||
}, ..._rules]
|
}, ..._rules]
|
||||||
}
|
}
|
||||||
if (option.pattern !== undefined) {
|
if (option.pattern) {
|
||||||
const label = option.label || toLabelByKey(option.labelKey)
|
const label = option.label || toLabelByKey(option.labelKey)
|
||||||
_rules = [{
|
_rules = [{
|
||||||
pattern: option.pattern,
|
pattern: option.pattern,
|
||||||
@@ -175,9 +191,7 @@ initFormModel()
|
|||||||
|
|
||||||
watch(() => calcOption.value, initFormModel, { deep: true })
|
watch(() => calcOption.value, initFormModel, { deep: true })
|
||||||
|
|
||||||
const hasModelText = computed(() => {
|
const hasModelText = computed(() => isFunction(calcOption.value.formatter))
|
||||||
return modelAttrs.value.modelText || calcOption.value.formatter
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['change'])
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
@@ -199,18 +213,10 @@ const controlLabelWidth = computed(() => {
|
|||||||
|
|
||||||
const formatResult = computed(() => {
|
const formatResult = computed(() => {
|
||||||
if (hasModelText.value) {
|
if (hasModelText.value) {
|
||||||
if (modelAttrs.value.modelText) {
|
const result = calcOption.value.formatter(modelValue.value, calcOption.value)
|
||||||
return {
|
return {
|
||||||
modelText: modelAttrs.value.modelText
|
modelText: result,
|
||||||
}
|
vnode: isVNode(result)
|
||||||
}
|
|
||||||
const option = calcOption.value
|
|
||||||
if (option.formatter) {
|
|
||||||
const result = option.formatter(modelValue.value, calcOption.value)
|
|
||||||
return {
|
|
||||||
modelText: result,
|
|
||||||
vnode: isVNode(result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -233,7 +239,16 @@ const formatResult = computed(() => {
|
|||||||
#label
|
#label
|
||||||
>
|
>
|
||||||
<slot name="beforeLabel" />
|
<slot name="beforeLabel" />
|
||||||
<span :class="calcOption.labelCls">{{ label }}</span>
|
<span
|
||||||
|
v-if="!$slots.label"
|
||||||
|
:class="calcOption.labelCls"
|
||||||
|
>{{ label }}</span>
|
||||||
|
<slot
|
||||||
|
v-else
|
||||||
|
name="label"
|
||||||
|
:option="calcOption"
|
||||||
|
:model="formModel"
|
||||||
|
/>
|
||||||
<slot name="afterLabel" />
|
<slot name="afterLabel" />
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
v-if="calcOption.tooltip||calcOption.tooltipFunc"
|
v-if="calcOption.tooltip||calcOption.tooltipFunc"
|
||||||
@@ -255,40 +270,42 @@ const formatResult = computed(() => {
|
|||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<component
|
<slot>
|
||||||
:is="inputType"
|
<component
|
||||||
v-model="modelValue"
|
:is="inputType"
|
||||||
v-bind="modelAttrs"
|
v-model="modelValue"
|
||||||
:placeholder="calcOption.placeholder"
|
v-bind="modelAttrs"
|
||||||
:disabled="calcOption.disabled"
|
:placeholder="calcOption.placeholder"
|
||||||
:readonly="calcOption.readonly"
|
:disabled="calcOption.disabled"
|
||||||
@change="controlChange"
|
:readonly="calcOption.readonly"
|
||||||
>
|
@change="controlChange"
|
||||||
<template
|
|
||||||
v-if="hasModelText&&formatResult"
|
|
||||||
#default
|
|
||||||
>
|
>
|
||||||
<span
|
<template
|
||||||
v-if="formatResult.modelText&&!formatResult.vnode"
|
v-if="hasModelText&&formatResult"
|
||||||
class="common-form-label-text"
|
#default
|
||||||
v-html="formatResult.modelText"
|
>
|
||||||
/>
|
<span
|
||||||
<component
|
v-if="formatResult.modelText&&!formatResult.vnode"
|
||||||
:is="formatResult.modelText"
|
class="common-form-label-text"
|
||||||
v-if="formatResult.vnode"
|
v-html="formatResult.modelText"
|
||||||
class="common-form-label-text"
|
/>
|
||||||
/>
|
<component
|
||||||
</template>
|
:is="formatResult.modelText"
|
||||||
<slot name="childBefore" />
|
v-if="formatResult.vnode"
|
||||||
<template v-if="children&&children.length">
|
class="common-form-label-text"
|
||||||
<control-child
|
/>
|
||||||
v-for="(childItem, index) in children"
|
</template>
|
||||||
:key="index"
|
<slot name="childBefore" />
|
||||||
:option="childItem"
|
<template v-if="children&&children.length">
|
||||||
/>
|
<control-child
|
||||||
</template>
|
v-for="(childItem, index) in children"
|
||||||
<slot name="childAfter" />
|
:key="index"
|
||||||
</component>
|
:option="childItem"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<slot name="childAfter" />
|
||||||
|
</component>
|
||||||
|
</slot>
|
||||||
<slot name="after" />
|
<slot name="after" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { inject, ref, onMounted, isRef, watchEffect } from 'vue'
|
import { inject, ref, onMounted, isRef, watchEffect, onUnmounted } from 'vue'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel, onKeyStroke } from '@vueuse/core'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { isFunction } from 'lodash-es'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {CommonFormProps}
|
* @type {CommonFormProps}
|
||||||
@@ -42,6 +44,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
disableButtons: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
showSubmit: {
|
showSubmit: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
@@ -71,11 +77,25 @@ const props = defineProps({
|
|||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
backUrl: {
|
backUrl: {
|
||||||
type: String,
|
type: [String, Function],
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
submitByEnter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
scrollToError: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['submitForm', 'update:model'])
|
const emit = defineEmits(['submitForm', 'update:model'])
|
||||||
|
|
||||||
const formModel = useVModel(props, 'model', emit)
|
const formModel = useVModel(props, 'model', emit)
|
||||||
@@ -86,10 +106,31 @@ const form = ref()
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
form
|
form
|
||||||
})
|
})
|
||||||
|
const formDiv = ref()
|
||||||
|
const removeEnterFn = ref()
|
||||||
|
const commonWindowRef = inject('commonWindow', null)
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const commonWindowRef = inject('commonWindow', null)
|
|
||||||
if (isRef(commonWindowRef)) {
|
if (isRef(commonWindowRef)) {
|
||||||
commonWindowRef.value.addForm(form)
|
commonWindowRef.value?.addForm(form)
|
||||||
|
}
|
||||||
|
if (props.submitByEnter) {
|
||||||
|
removeEnterFn.value = onKeyStroke('Enter', (event) => {
|
||||||
|
event?.stopImmediatePropagation()
|
||||||
|
if (form.value) {
|
||||||
|
console.info('=========================submitByEnter', formDiv.value)
|
||||||
|
emit('submitForm', form.value)
|
||||||
|
}
|
||||||
|
}, { target: formDiv.value })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (isRef(commonWindowRef)) {
|
||||||
|
commonWindowRef.value?.removeForm(form)
|
||||||
|
}
|
||||||
|
if (removeEnterFn.value) {
|
||||||
|
removeEnterFn.value()
|
||||||
|
removeEnterFn.value = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -103,75 +144,109 @@ watchEffect(async () => {
|
|||||||
await form.value.validate((ok) => { disableSubmit.value = !ok })
|
await form.value.validate((ok) => { disableSubmit.value = !ok })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const goBack = (...args) => {
|
||||||
|
if (isFunction(props.backUrl)) {
|
||||||
|
return props.backUrl(...args)
|
||||||
|
} else if (props.backUrl) {
|
||||||
|
router.push(props.backUrl)
|
||||||
|
} else {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-form
|
<div
|
||||||
ref="form"
|
ref="formDiv"
|
||||||
:inline="inline"
|
class="common-form-div"
|
||||||
:class="className"
|
:class="$attrs.class"
|
||||||
:model="formModel"
|
|
||||||
:label-width="labelWidth"
|
|
||||||
:validate-on-rule-change="validateOnRuleChange"
|
|
||||||
>
|
>
|
||||||
<template
|
<el-form
|
||||||
v-for="(option,index) in options"
|
ref="form"
|
||||||
:key="index"
|
:inline="inline"
|
||||||
>
|
:class="className"
|
||||||
<slot
|
|
||||||
v-if="option.slot"
|
|
||||||
:name="option.slot"
|
|
||||||
:option="option"
|
|
||||||
:form="form"
|
|
||||||
:model="formModel"
|
|
||||||
/>
|
|
||||||
<common-form-control
|
|
||||||
v-if="!option.slot&&option.enabled!==false"
|
|
||||||
:model="formModel"
|
|
||||||
:option="option"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<slot
|
|
||||||
:form="form"
|
|
||||||
:model="formModel"
|
:model="formModel"
|
||||||
name="default"
|
:label-width="labelWidth"
|
||||||
/>
|
:scroll-to-error="scrollToError"
|
||||||
<el-form-item
|
:validate-on-rule-change="validateOnRuleChange"
|
||||||
v-if="showButtons"
|
v-bind="{...$attrs, 'class':undefined}"
|
||||||
:style="buttonStyle"
|
@submit.prevent
|
||||||
>
|
>
|
||||||
<el-button
|
<template
|
||||||
v-if="showSubmit"
|
v-for="(option,index) in options"
|
||||||
:disabled="disableSubmit"
|
:key="index"
|
||||||
type="primary"
|
|
||||||
@click="$emit('submitForm', form)"
|
|
||||||
>
|
>
|
||||||
{{ submitLabel||$t('common.label.submit') }}
|
<slot
|
||||||
</el-button>
|
v-if="option.slot"
|
||||||
<el-button
|
:name="option.slot"
|
||||||
v-if="showReset"
|
:option="option"
|
||||||
@click="form.resetFields()"
|
:form="form"
|
||||||
>
|
:model="formModel"
|
||||||
{{ resetLabel||$t('common.label.reset') }}
|
/>
|
||||||
</el-button>
|
<common-form-control
|
||||||
<el-button
|
v-if="!option.slot&&option.enabled!==false"
|
||||||
v-if="showBack||backUrl"
|
:model="formModel"
|
||||||
@click="backUrl?$router.push(backUrl):$router.go(-1)"
|
:option="option"
|
||||||
>
|
>
|
||||||
{{ backLabel||$t('common.label.back') }}
|
<template
|
||||||
</el-button>
|
v-if="option.labelSlot"
|
||||||
|
#label="scope"
|
||||||
|
>
|
||||||
|
<slot
|
||||||
|
v-if="option.labelSlot"
|
||||||
|
:name="option.labelSlot"
|
||||||
|
:form="form"
|
||||||
|
v-bind="scope"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</common-form-control>
|
||||||
|
</template>
|
||||||
<slot
|
<slot
|
||||||
:form="form"
|
:form="form"
|
||||||
:model="formModel"
|
:model="formModel"
|
||||||
name="buttons"
|
name="default"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
<el-form-item
|
||||||
<slot
|
v-if="showButtons"
|
||||||
:form="form"
|
:style="buttonStyle"
|
||||||
:model="formModel"
|
class="buttonsDiv"
|
||||||
name="after-buttons"
|
>
|
||||||
/>
|
<el-button
|
||||||
</el-form>
|
v-if="showSubmit"
|
||||||
|
:disabled="disableSubmit || disableButtons"
|
||||||
|
type="primary"
|
||||||
|
@click="$emit('submitForm', form)"
|
||||||
|
>
|
||||||
|
{{ submitLabel||$t('common.label.submit') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="showReset"
|
||||||
|
:disabled="disableButtons"
|
||||||
|
@click="form.resetFields()"
|
||||||
|
>
|
||||||
|
{{ resetLabel||$t('common.label.reset') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="showBack||backUrl"
|
||||||
|
:disabled="disableButtons"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
{{ backLabel||$t('common.label.back') }}
|
||||||
|
</el-button>
|
||||||
|
<slot
|
||||||
|
:form="form"
|
||||||
|
:model="formModel"
|
||||||
|
name="buttons"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<slot
|
||||||
|
:form="form"
|
||||||
|
:model="formModel"
|
||||||
|
name="after-buttons"
|
||||||
|
/>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
16
src/components/common-form/public.d.ts
vendored
16
src/components/common-form/public.d.ts
vendored
@@ -123,6 +123,12 @@ export interface CommonFormOption extends FormControlTypeOption {
|
|||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
/** 占位提示符 */
|
/** 占位提示符 */
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
/** 日期最小值**/
|
||||||
|
minDate: string|Date;
|
||||||
|
/** 日期最大值**/
|
||||||
|
maxDate: string|Date;
|
||||||
|
/** 是否清理不可用日期值**/
|
||||||
|
clearInvalidDate: boolean;
|
||||||
/** 有些控件柚子节点 */
|
/** 有些控件柚子节点 */
|
||||||
children?: Array<CommonFormOption>;
|
children?: Array<CommonFormOption>;
|
||||||
/** async-validator验证器 */
|
/** async-validator验证器 */
|
||||||
@@ -133,6 +139,12 @@ export interface CommonFormOption extends FormControlTypeOption {
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
/** 提示函数 */
|
/** 提示函数 */
|
||||||
tooltipFunc?: () => void;
|
tooltipFunc?: () => void;
|
||||||
|
/** 自动trim,默认false**/
|
||||||
|
trim?: boolean,
|
||||||
|
/** 自动upperCase,默认false**/
|
||||||
|
upperCase?: boolean,
|
||||||
|
/** 自动lowerCase,默认false**/
|
||||||
|
lowerCase?: boolean,
|
||||||
/**
|
/**
|
||||||
* common-form-label格式化
|
* common-form-label格式化
|
||||||
* @param modelValue 数据
|
* @param modelValue 数据
|
||||||
@@ -141,6 +153,8 @@ export interface CommonFormOption extends FormControlTypeOption {
|
|||||||
formatter?: (modelValue:any, option: CommonFormOption) => string|VNode;
|
formatter?: (modelValue:any, option: CommonFormOption) => string|VNode;
|
||||||
/** 自定义slot名称 */
|
/** 自定义slot名称 */
|
||||||
slot?: string;
|
slot?: string;
|
||||||
|
/** 自定义label slot名称 */
|
||||||
|
labelSlot?: string;
|
||||||
/**
|
/**
|
||||||
* 根据model数据动态计算Option值
|
* 根据model数据动态计算Option值
|
||||||
* @param model 表单model
|
* @param model 表单model
|
||||||
@@ -184,4 +198,6 @@ export interface CommonFormProps extends FormProps {
|
|||||||
backUrl: string;
|
backUrl: string;
|
||||||
/** 行级排列 */
|
/** 行级排列 */
|
||||||
inline: boolean;
|
inline: boolean;
|
||||||
|
/** 回车提交 */
|
||||||
|
submitByEnter: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,13 +102,14 @@ const selectIcon = icon => {
|
|||||||
>
|
>
|
||||||
{{ $t('common.label.clear') }}
|
{{ $t('common.label.clear') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-dialog
|
<common-window
|
||||||
v-model="iconSelectVisible"
|
v-model="iconSelectVisible"
|
||||||
:width="dialogWidth"
|
:width="dialogWidth"
|
||||||
v-bind="dialogAttrs"
|
v-bind="dialogAttrs"
|
||||||
draggable
|
draggable
|
||||||
class="icon-dialog"
|
class="icon-dialog"
|
||||||
:title="$t('common.msg.pleaseSelectIcon')"
|
:title="$t('common.msg.pleaseSelectIcon')"
|
||||||
|
:show-buttons="false"
|
||||||
>
|
>
|
||||||
<el-container
|
<el-container
|
||||||
style="overflow: auto;"
|
style="overflow: auto;"
|
||||||
@@ -155,9 +156,15 @@ const selectIcon = icon => {
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</recycle-scroller>
|
</recycle-scroller>
|
||||||
|
<el-backtop
|
||||||
|
v-common-tooltip="$t('common.label.backtop')"
|
||||||
|
target=".scroller"
|
||||||
|
:right="10"
|
||||||
|
:bottom="10"
|
||||||
|
/>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-dialog>
|
</common-window>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -179,6 +186,10 @@ const selectIcon = icon => {
|
|||||||
}
|
}
|
||||||
.icon-area {
|
.icon-area {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.icon-area .el-backtop {
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
.icon-a {
|
.icon-a {
|
||||||
height:80px;
|
height:80px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { ICON_PREFIX } from '@/icons'
|
import { ICON_PREFIX } from '@/icons'
|
||||||
import kebabCase from 'lodash/kebabCase'
|
import { kebabCase } from 'lodash-es'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
icon: {
|
icon: {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const props = defineProps({
|
|||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
index: {
|
index: {
|
||||||
type: [Number, String],
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -87,7 +87,7 @@ const showMenuIcon = computed(() => {
|
|||||||
<common-menu-item
|
<common-menu-item
|
||||||
v-for="(childMenu, childIdx) in menuItem.children"
|
v-for="(childMenu, childIdx) in menuItem.children"
|
||||||
:key="childMenu.index||childIdx"
|
:key="childMenu.index||childIdx"
|
||||||
:index="childIdx"
|
:index="`${menuItem.index||index}_${childIdx}`"
|
||||||
:menu-item="childMenu"
|
:menu-item="childMenu"
|
||||||
/>
|
/>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
@@ -128,6 +128,7 @@ const showMenuIcon = computed(() => {
|
|||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item
|
<el-menu-item
|
||||||
v-else
|
v-else
|
||||||
|
v-open-new-window="menuItem.index"
|
||||||
:class="menuCls"
|
:class="menuCls"
|
||||||
:disabled="menuItem.disabled"
|
:disabled="menuItem.disabled"
|
||||||
:route="menuItem.route"
|
:route="menuItem.route"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const activeRoutePath = computed(() => {
|
|||||||
return props.defaultActivePath
|
return props.defaultActivePath
|
||||||
}
|
}
|
||||||
const current = useParentRoute(route)
|
const current = useParentRoute(route)
|
||||||
return current && current.path !== '/' ? current.path : ''
|
return current && current.path !== '/' ? current.path : '--'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ const activeRoutePath = computed(() => {
|
|||||||
>
|
>
|
||||||
<common-menu-item
|
<common-menu-item
|
||||||
:menu-item="menuItem"
|
:menu-item="menuItem"
|
||||||
:index="index"
|
:index="`${index}`"
|
||||||
>
|
>
|
||||||
<template #split>
|
<template #split>
|
||||||
<slot name="split" />
|
<slot name="split" />
|
||||||
|
|||||||
2
src/components/common-sort/public.d.ts
vendored
2
src/components/common-sort/public.d.ts
vendored
@@ -3,7 +3,7 @@ export interface SortOption {
|
|||||||
labelKey?: string;// 国际化资源key,首选该属性,不存在才使用label
|
labelKey?: string;// 国际化资源key,首选该属性,不存在才使用label
|
||||||
label?: string;
|
label?: string;
|
||||||
showIcon?: boolean; // 控制某些排序不显示图标
|
showIcon?: boolean; // 控制某些排序不显示图标
|
||||||
fixedValue?: 'ASC' | 'DESC' // 固定排序模式
|
fixedValue?: 'ASC' | 'DESC'; // 固定排序模式
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import TableFormControl from '@/components/common-table-form/table-form-control.vue'
|
|
||||||
import { toLabelByKey } from '@/components/utils'
|
import { toLabelByKey } from '@/components/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -19,6 +18,10 @@ const props = defineProps({
|
|||||||
showOperation: {
|
showOperation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
operationWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '110px'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -55,13 +58,11 @@ const options = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<el-table
|
<el-table
|
||||||
:data="dataList"
|
:data="dataList"
|
||||||
border
|
|
||||||
class="common-table-form"
|
class="common-table-form"
|
||||||
>
|
>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="(option, index) in options"
|
v-for="(option, index) in options"
|
||||||
:key="index"
|
:key="`${option.prop}__${index}`"
|
||||||
:label="option.label||toLabelByKey(option.labelKey)"
|
|
||||||
:width="option.width"
|
:width="option.width"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
@@ -73,6 +74,16 @@ const options = computed(() => {
|
|||||||
:name="option.headerSlot"
|
:name="option.headerSlot"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template
|
||||||
|
v-else
|
||||||
|
#header
|
||||||
|
>
|
||||||
|
<el-form-item
|
||||||
|
:label="option.label||toLabelByKey(option.labelKey)"
|
||||||
|
class="common-table-form-label"
|
||||||
|
:required="option.required"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<!--用于自定义显示属性-->
|
<!--用于自定义显示属性-->
|
||||||
<template
|
<template
|
||||||
v-if="option.slot"
|
v-if="option.slot"
|
||||||
@@ -88,7 +99,7 @@ const options = computed(() => {
|
|||||||
v-else
|
v-else
|
||||||
#default="{row, $index}"
|
#default="{row, $index}"
|
||||||
>
|
>
|
||||||
<table-form-control
|
<common-form-control
|
||||||
:model="row"
|
:model="row"
|
||||||
label-width="0"
|
label-width="0"
|
||||||
:option="option"
|
:option="option"
|
||||||
@@ -99,11 +110,13 @@ const options = computed(() => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="showOperation"
|
v-if="showOperation"
|
||||||
|
:width="operationWidth"
|
||||||
:label="$t('common.label.operation')"
|
:label="$t('common.label.operation')"
|
||||||
>
|
>
|
||||||
<template #default="{row, $index}">
|
<template #default="{row, $index}">
|
||||||
<div class="el-form-item">
|
<div class="el-form-item">
|
||||||
<el-button
|
<el-button
|
||||||
|
circle
|
||||||
type="danger"
|
type="danger"
|
||||||
size="small"
|
size="small"
|
||||||
:underline="false"
|
:underline="false"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { formatDate, toLabelByKey } from '@/components/utils'
|
import { formatDate } from '@/utils'
|
||||||
import { computed, useSlots } from 'vue'
|
import { computed, useSlots } from 'vue'
|
||||||
import { get } from 'lodash'
|
import { get } from 'lodash-es'
|
||||||
|
import { toLabelByKey } from '@/components/utils'
|
||||||
|
import TableDynamicButton from '@/components/common-table/table-dynamic-button.vue'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置信息
|
* 配置信息
|
||||||
@@ -49,6 +51,7 @@ const slots = useSlots()
|
|||||||
:label="column.label || toLabelByKey(column.labelKey)"
|
:label="column.label || toLabelByKey(column.labelKey)"
|
||||||
:prop="column.prop||column.property"
|
:prop="column.prop||column.property"
|
||||||
:width="column.width"
|
:width="column.width"
|
||||||
|
:min-width="column.minWidth"
|
||||||
v-bind="column.attrs"
|
v-bind="column.attrs"
|
||||||
:formatter="formatter"
|
:formatter="formatter"
|
||||||
>
|
>
|
||||||
@@ -86,20 +89,16 @@ const slots = useSlots()
|
|||||||
<template
|
<template
|
||||||
#default="scope"
|
#default="scope"
|
||||||
>
|
>
|
||||||
<template v-for="(button, index) in column.buttons">
|
<template
|
||||||
<el-button
|
v-for="(button, index) in column.buttons"
|
||||||
v-if="(!button.buttonIf||button.buttonIf(scope.row, scope))&&button.enabled!==false"
|
:key="index"
|
||||||
:key="index"
|
>
|
||||||
:type="button.type"
|
<table-dynamic-button
|
||||||
:icon="button.icon"
|
:button-config="button"
|
||||||
:size="button.size||buttonSize"
|
:item="scope.row"
|
||||||
:disabled="button.disabled"
|
:button-size="buttonSize"
|
||||||
:round="button.round"
|
:scope="{...scope,item:scope.row}"
|
||||||
:circle="button.circle"
|
/>
|
||||||
@click="button.click&&button.click(scope.row, scope)"
|
|
||||||
>
|
|
||||||
{{ button.label || toLabelByKey(button.labelKey) }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
<slot
|
<slot
|
||||||
name="default"
|
name="default"
|
||||||
|
|||||||
261
src/components/common-table/common-table-v2.vue
Normal file
261
src/components/common-table/common-table-v2.vue
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<script setup lang="jsx">
|
||||||
|
import { computed, useSlots, ref, onMounted } from 'vue'
|
||||||
|
import { toLabelByKey } from '@/components/utils'
|
||||||
|
import { get } from 'lodash-es'
|
||||||
|
import { $i18nBundle } from '@/messages'
|
||||||
|
import TableDynamicButton from '@/components/common-table/table-dynamic-button.vue'
|
||||||
|
import { formatDate } from '@/utils'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type CommonTableProps
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
/**
|
||||||
|
* @type {[CommonTableColumn]}
|
||||||
|
*/
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 显示数据
|
||||||
|
*/
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highlightCurrentRow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
stripe: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
rowHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 50
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '500px'
|
||||||
|
},
|
||||||
|
autoColWidth: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
defaultColWidth: {
|
||||||
|
type: String,
|
||||||
|
default: '150px'
|
||||||
|
},
|
||||||
|
expandColumnKey: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
rowKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'id'
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* el-button
|
||||||
|
* @type [TableButtonProps]
|
||||||
|
*/
|
||||||
|
buttons: {
|
||||||
|
type: Array,
|
||||||
|
default () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buttonsSlot: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
buttonSize: {
|
||||||
|
type: String,
|
||||||
|
default: 'small'
|
||||||
|
},
|
||||||
|
buttonsColumnAttrs: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const expandRowKeys = defineModel('expandRowKeys', {
|
||||||
|
type: Array, default: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const calcExpandColumnKey = computed(() => {
|
||||||
|
if (!props.expandColumnKey && props.columns.length && props.expandRowKeys) {
|
||||||
|
return getColumnKey(props.columns[0])
|
||||||
|
}
|
||||||
|
return props.expandColumnKey
|
||||||
|
})
|
||||||
|
|
||||||
|
const getColumnKey = (column) => {
|
||||||
|
return column.prop || column.property || column.slot
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPropertyData = (row, column) => {
|
||||||
|
return get(row, column.prop || column.property)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1版本formatter转换成CellRenderer,方便统一配置
|
||||||
|
* @param column
|
||||||
|
* @return {function(*): *}
|
||||||
|
*/
|
||||||
|
const formatter2Render = (column) => {
|
||||||
|
let formatter = column.formatter
|
||||||
|
if (column.dateFormat && !formatter) {
|
||||||
|
formatter = row => {
|
||||||
|
const data = getPropertyData(row, column)
|
||||||
|
return formatDate(data, column.dateFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (formatter) {
|
||||||
|
return (data) => {
|
||||||
|
const value = getPropertyData(data.rowData, column)
|
||||||
|
return formatter(data.rowData, data.cellData, value, data.rowIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
|
const data2Scope = (data) => {
|
||||||
|
return {
|
||||||
|
row: data.rowData,
|
||||||
|
item: data.rowData,
|
||||||
|
column: data.column,
|
||||||
|
$index: data.rowIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1版本slot信息转换成CellRenderer,方便统一配置
|
||||||
|
* @param slotName 槽的名称
|
||||||
|
* @return {function(*): VNode[]}
|
||||||
|
*/
|
||||||
|
const slot2Render = (slotName) => {
|
||||||
|
if (slotName && slots[slotName]) {
|
||||||
|
return (data) => {
|
||||||
|
// row: any, column: any, $index: number
|
||||||
|
console.log('===================================slot', data)
|
||||||
|
return slots[slotName](data2Scope(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultWidth = computed(() => {
|
||||||
|
if (props.autoColWidth) {
|
||||||
|
const columnCount = (props.buttons?.length ? props.columns.length + 1 : props.columns.length) || 1
|
||||||
|
return 100 / columnCount + '%'
|
||||||
|
}
|
||||||
|
return defaultWidth
|
||||||
|
})
|
||||||
|
|
||||||
|
const calcColumns = computed(() => {
|
||||||
|
const tmpColumns = props.columns.filter(column => column.enabled !== false)
|
||||||
|
.map((column) => {
|
||||||
|
return Object.assign({
|
||||||
|
title: column.label || toLabelByKey(column.labelKey),
|
||||||
|
headerCellRenderer: column.headerCellRenderer || slot2Render(column.headerSlot),
|
||||||
|
cellRenderer: column.cellRenderer || formatter2Render(column) || slot2Render(column.slot),
|
||||||
|
dataKey: column.prop || column.property,
|
||||||
|
key: getColumnKey(column),
|
||||||
|
width: column.width || defaultWidth.value
|
||||||
|
}, column.attrs || {})
|
||||||
|
})
|
||||||
|
if (props.buttons?.length) {
|
||||||
|
tmpColumns.push(Object.assign({
|
||||||
|
title: $i18nBundle('common.label.operation'),
|
||||||
|
cellRenderer: (data) => {
|
||||||
|
return props.buttons
|
||||||
|
.map((button) => {
|
||||||
|
return <TableDynamicButton
|
||||||
|
buttonConfig={button}
|
||||||
|
buttonSize={props.buttonSize}
|
||||||
|
item={data.rowData}
|
||||||
|
scope={data2Scope(data)} />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, props.buttonsColumnAttrs || {}))
|
||||||
|
}
|
||||||
|
return tmpColumns
|
||||||
|
})
|
||||||
|
|
||||||
|
const calcStyle = computed(() => {
|
||||||
|
return { height: props.height }
|
||||||
|
})
|
||||||
|
|
||||||
|
const table = ref()
|
||||||
|
const tableContainerRef = ref()
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
table
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (table.value && props.expandColumnKey) {
|
||||||
|
table.value.toggleRowExpansion = (rowData) => {
|
||||||
|
const rowKeyValue = get(rowData, props.rowKey)
|
||||||
|
const $row = tableContainerRef.value?.$el?.querySelector(`div[rowkey="${rowKeyValue}"]`)
|
||||||
|
const $expandIcon = $row?.querySelector('.el-table-v2__expand-icon')
|
||||||
|
if ($expandIcon) {
|
||||||
|
$expandIcon.click()
|
||||||
|
}
|
||||||
|
console.log('===================toggleRowExpansion', rowData, rowKeyValue, $row, $expandIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-container
|
||||||
|
ref="tableContainerRef"
|
||||||
|
v-loading="loading"
|
||||||
|
class="common-table-v2"
|
||||||
|
:element-loading-text="loadingText"
|
||||||
|
:style="calcStyle"
|
||||||
|
>
|
||||||
|
<el-auto-resizer>
|
||||||
|
<template #default="{ height: tableHeight, width }">
|
||||||
|
<el-table-v2
|
||||||
|
ref="table"
|
||||||
|
v-model:expanded-row-keys="expandRowKeys"
|
||||||
|
:row-height="rowHeight"
|
||||||
|
:columns="calcColumns"
|
||||||
|
:data="data"
|
||||||
|
:width="width"
|
||||||
|
:height="tableHeight"
|
||||||
|
:row-key="rowKey"
|
||||||
|
:expand-column-key="calcExpandColumnKey"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-auto-resizer>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import CommonTableColumn from '@/components/common-table/common-table-column.vue'
|
import CommonTableColumn from '@/components/common-table/common-table-column.vue'
|
||||||
import { computed, ref, onMounted, watch } from 'vue'
|
import { computed, ref, onMounted, watch, onUnmounted } from 'vue'
|
||||||
import { useIntersectionObserver } from '@vueuse/core'
|
import { useIntersectionObserver } from '@vueuse/core'
|
||||||
import { getFrontendPage } from '@/components/utils'
|
import { getFrontendPage } from '@/components/utils'
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: false
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* el-button
|
* el-button
|
||||||
@@ -83,6 +83,10 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showPageSizes: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -158,7 +162,7 @@ const isInfiniteEnd = ref(false)
|
|||||||
|
|
||||||
function checkInfiniteEnd (pageVal) {
|
function checkInfiniteEnd (pageVal) {
|
||||||
if (props.infinitePaging) {
|
if (props.infinitePaging) {
|
||||||
isInfiniteEnd.value = pageVal ? pageVal.pageNumber >= pageVal.pageCount : true
|
isInfiniteEnd.value = pageVal ? pageVal.pageNumber >= (pageVal.pageCount || 0) : true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,10 +202,10 @@ watch(() => frontendPage, () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.infinitePaging) {
|
if (props.infinitePaging) {
|
||||||
console.info('================================mounted', infiniteRef.value)
|
const { stop } = useIntersectionObserver(infiniteRef, onInfiniteLoad, {
|
||||||
useIntersectionObserver(infiniteRef, onInfiniteLoad, {
|
threshold: 0.5
|
||||||
threshold: 1
|
|
||||||
})
|
})
|
||||||
|
onUnmounted(stop)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -219,6 +223,24 @@ const onInfiniteLoad = (args) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calcPageAttrs = computed(() => {
|
||||||
|
let newPageAttrs = props.pageAttrs
|
||||||
|
if (!props.showPageSizes && newPageAttrs.layout) {
|
||||||
|
newPageAttrs = {
|
||||||
|
...newPageAttrs,
|
||||||
|
layout: newPageAttrs.layout.split(/\s*,\s*/).filter(item => item !== 'sizes').join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newPageAttrs
|
||||||
|
})
|
||||||
|
|
||||||
|
const calcBorder = computed(() => {
|
||||||
|
if (props.infinitePaging) {
|
||||||
|
return true // 这里有个bug,infinitePaging时border为false显示将会有问题
|
||||||
|
}
|
||||||
|
return props.border
|
||||||
|
})
|
||||||
|
|
||||||
const table = ref()
|
const table = ref()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -240,7 +262,7 @@ defineExpose({
|
|||||||
:stripe="stripe"
|
:stripe="stripe"
|
||||||
:data="calcData"
|
:data="calcData"
|
||||||
:class="{'common-hide-expand': hideExpandBtn}"
|
:class="{'common-hide-expand': hideExpandBtn}"
|
||||||
:border="border"
|
:border="calcBorder"
|
||||||
>
|
>
|
||||||
<common-table-column
|
<common-table-column
|
||||||
v-for="(column, index) in calcColumns"
|
v-for="(column, index) in calcColumns"
|
||||||
@@ -293,7 +315,7 @@ defineExpose({
|
|||||||
<el-pagination
|
<el-pagination
|
||||||
v-if="!infinitePaging&&!frontendPaging&&page&&page.pageCount"
|
v-if="!infinitePaging&&!frontendPaging&&page&&page.pageCount"
|
||||||
class="common-pagination"
|
class="common-pagination"
|
||||||
v-bind="pageAttrs"
|
v-bind="calcPageAttrs"
|
||||||
:total="page.totalCount"
|
:total="page.totalCount"
|
||||||
:page-size="page.pageSize"
|
:page-size="page.pageSize"
|
||||||
:current-page="page.pageNumber"
|
:current-page="page.pageNumber"
|
||||||
@@ -303,7 +325,7 @@ defineExpose({
|
|||||||
<el-pagination
|
<el-pagination
|
||||||
v-if="!infinitePaging&&frontendPaging&&frontendPage&&frontendPage.pageCount"
|
v-if="!infinitePaging&&frontendPaging&&frontendPage&&frontendPage.pageCount"
|
||||||
class="common-pagination"
|
class="common-pagination"
|
||||||
v-bind="pageAttrs"
|
v-bind="calcPageAttrs"
|
||||||
:total="frontendPage.totalCount"
|
:total="frontendPage.totalCount"
|
||||||
:page-size="frontendPage.pageSize"
|
:page-size="frontendPage.pageSize"
|
||||||
:current-page="frontendPage.pageNumber"
|
:current-page="frontendPage.pageNumber"
|
||||||
|
|||||||
10
src/components/common-table/public.d.ts
vendored
10
src/components/common-table/public.d.ts
vendored
@@ -11,6 +11,14 @@ export type TableButtonProps = {
|
|||||||
* @param data 表格数据
|
* @param data 表格数据
|
||||||
*/
|
*/
|
||||||
buttonIf: (data: any) => boolean
|
buttonIf: (data: any) => boolean
|
||||||
|
/**
|
||||||
|
* 动态计算按钮属性
|
||||||
|
*/
|
||||||
|
dynamicButton: (data: any) => any
|
||||||
|
/**
|
||||||
|
* 按钮其他选项
|
||||||
|
*/
|
||||||
|
attrs: ButtonProps
|
||||||
} & ButtonProps
|
} & ButtonProps
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +85,8 @@ export interface CommonTableProps extends TableProps<any> {
|
|||||||
pageAlign?: 'left' | 'center' | 'right';
|
pageAlign?: 'left' | 'center' | 'right';
|
||||||
/** 其他分页配置项 */
|
/** 其他分页配置项 */
|
||||||
pageAttrs?: PaginationProps;
|
pageAttrs?: PaginationProps;
|
||||||
|
/** 是否显示分页数量选择 **/
|
||||||
|
showPageSizes?: boolean;
|
||||||
/** loading状态 */
|
/** loading状态 */
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
/** loading显示消息 */
|
/** loading显示消息 */
|
||||||
|
|||||||
64
src/components/common-table/table-dynamic-button.vue
Normal file
64
src/components/common-table/table-dynamic-button.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { toLabelByKey } from '@/components/utils'
|
||||||
|
import { isFunction, cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
buttonConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
buttonSize: {
|
||||||
|
type: String,
|
||||||
|
default: 'small'
|
||||||
|
},
|
||||||
|
scope: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const button = computed(() => {
|
||||||
|
let buttonConfig = cloneDeep(props.buttonConfig)
|
||||||
|
if (isFunction(buttonConfig.buttonIf) && props.item && props.scope.$index > -1) {
|
||||||
|
buttonConfig.enabled = !!buttonConfig.buttonIf(props.item)
|
||||||
|
}
|
||||||
|
if (isFunction(buttonConfig.dynamicButton) && props.item && props.scope.$index > -1) {
|
||||||
|
buttonConfig = { ...buttonConfig, ...buttonConfig.dynamicButton(props.item) }
|
||||||
|
}
|
||||||
|
return buttonConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-button
|
||||||
|
v-if="button.enabled!==false"
|
||||||
|
:type="button.type"
|
||||||
|
:size="button.size||buttonSize"
|
||||||
|
:disabled="button.disabled"
|
||||||
|
:round="button.round"
|
||||||
|
:circle="button.circle"
|
||||||
|
v-bind="button.attrs"
|
||||||
|
@click="button.click&&button.click(item, scope)"
|
||||||
|
>
|
||||||
|
{{ button.label || toLabelByKey(button.labelKey) }}
|
||||||
|
<template
|
||||||
|
v-if="!!button.icon"
|
||||||
|
#icon
|
||||||
|
>
|
||||||
|
<common-icon
|
||||||
|
:icon="button.icon"
|
||||||
|
:size="button.iconSize"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import isString from 'lodash/isString'
|
import { isString } from 'lodash-es'
|
||||||
import TabsViewItem from '@/components/common-tabs-view/tabs-view-item.vue'
|
import TabsViewItem from '@/components/common-tabs-view/tabs-view-item.vue'
|
||||||
|
import { toGetParams } from '@/utils'
|
||||||
|
import { isNestedRoute } from '@/route/RouteUtils'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -13,7 +15,7 @@ watch(route, () => {
|
|||||||
tabsViewStore.addHistoryTab(route)
|
tabsViewStore.addHistoryTab(route)
|
||||||
tabsViewStore.currentTab = route.path
|
tabsViewStore.currentTab = route.path
|
||||||
}
|
}
|
||||||
})
|
}, { immediate: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!tabsViewStore.historyTabs.length) {
|
if (!tabsViewStore.historyTabs.length) {
|
||||||
@@ -26,7 +28,9 @@ const selectHistoryTab = path => {
|
|||||||
const tab = isString(path) ? tabsViewStore.findHistoryTab(path) : path
|
const tab = isString(path) ? tabsViewStore.findHistoryTab(path) : path
|
||||||
if (tab) {
|
if (tab) {
|
||||||
router.push(tab)
|
router.push(tab)
|
||||||
tabsViewStore.addCachedTab(tab)
|
if (!isNestedRoute(tab)) {
|
||||||
|
tabsViewStore.addCachedTab(tab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +43,11 @@ const removeHistoryTab = path => {
|
|||||||
|
|
||||||
const refreshHistoryTab = tab => {
|
const refreshHistoryTab = tab => {
|
||||||
const time = new Date().getTime()
|
const time = new Date().getTime()
|
||||||
router.push(`${tab.path}?${time}`)
|
const query = Object.assign({}, tab.query, { _t: time })
|
||||||
tabsViewStore.addCachedTab(tab)
|
router.replace(`${tab.path}?${toGetParams(query)}`)
|
||||||
|
if (!isNestedRoute(tab)) {
|
||||||
|
tabsViewStore.addCachedTab(tab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeOtherHistoryTabs = tab => {
|
const removeOtherHistoryTabs = tab => {
|
||||||
@@ -82,6 +89,7 @@ const onDropdownVisibleChange = (visible, tab) => {
|
|||||||
ref="tabItems"
|
ref="tabItems"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
:tab-item="item"
|
:tab-item="item"
|
||||||
|
:label-config="item.labelConfig"
|
||||||
@refresh-history-tab="refreshHistoryTab"
|
@refresh-history-tab="refreshHistoryTab"
|
||||||
@remove-other-history-tabs="removeOtherHistoryTabs"
|
@remove-other-history-tabs="removeOtherHistoryTabs"
|
||||||
@remove-history-tabs="removeHistoryTabs"
|
@remove-history-tabs="removeHistoryTabs"
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
import { useMenuInfo, useMenuName } from '@/components/utils'
|
import { useMenuInfo, useMenuName } from '@/components/utils'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
|
import { $i18nKey } from '@/messages'
|
||||||
|
|
||||||
const tabsViewStore = useTabsViewStore()
|
const tabsViewStore = useTabsViewStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
labelConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @type RouteRecordRaw
|
* @type RouteRecordRaw
|
||||||
*/
|
*/
|
||||||
@@ -18,6 +23,11 @@ const props = defineProps({
|
|||||||
defineEmits(['removeHistoryTab', 'removeOtherHistoryTabs', 'removeHistoryTabs', 'refreshHistoryTab', 'onDropdownVisibleChange'])
|
defineEmits(['removeHistoryTab', 'removeOtherHistoryTabs', 'removeHistoryTabs', 'refreshHistoryTab', 'onDropdownVisibleChange'])
|
||||||
|
|
||||||
const menuName = computed(() => {
|
const menuName = computed(() => {
|
||||||
|
const labelConfig = props.labelConfig
|
||||||
|
if (labelConfig) {
|
||||||
|
const label = labelConfig.label || $i18nKey(labelConfig.labelKey)
|
||||||
|
if (label) return label
|
||||||
|
}
|
||||||
return useMenuName(props.tabItem)
|
return useMenuName(props.tabItem)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -26,6 +36,11 @@ const menuInfo = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const menuIcon = computed(() => {
|
const menuIcon = computed(() => {
|
||||||
|
const labelConfig = props.labelConfig
|
||||||
|
if (labelConfig) {
|
||||||
|
const icon = labelConfig.icon
|
||||||
|
if (icon) return icon
|
||||||
|
}
|
||||||
if (menuInfo.value && menuInfo.value.icon) {
|
if (menuInfo.value && menuInfo.value.icon) {
|
||||||
return menuInfo.value.icon
|
return menuInfo.value.icon
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { computed, ref, provide, unref } from 'vue'
|
import { computed, ref, provide, unref, watch, onBeforeUnmount } from 'vue'
|
||||||
import { UPDATE_MODEL_EVENT } from 'element-plus'
|
import { UPDATE_MODEL_EVENT } from 'element-plus'
|
||||||
import { proxyMethod } from '@/components/utils'
|
import { proxyMethod } from '@/components/utils'
|
||||||
|
|
||||||
@@ -13,6 +13,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
header: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@@ -33,6 +37,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
showFullscreen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
dblclickToFullscreen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
beforeClose: {
|
beforeClose: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
@@ -65,6 +77,10 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
showButtons: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
destroyOnClose: {
|
destroyOnClose: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -99,6 +115,11 @@ const commonWindow = ref({
|
|||||||
if (form && !windowForms.value.includes(form)) {
|
if (form && !windowForms.value.includes(form)) {
|
||||||
windowForms.value.push(form)
|
windowForms.value.push(form)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
removeForm (form) {
|
||||||
|
if (form && windowForms.value.includes(form)) {
|
||||||
|
windowForms.value.splice(windowForms.value.indexOf(form), 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
provide('commonWindow', commonWindow)
|
provide('commonWindow', commonWindow)
|
||||||
@@ -138,21 +159,61 @@ const calcBeforeClose = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isFullscreen = defineModel('fullscreen', { type: Boolean, default: false })
|
||||||
|
|
||||||
|
const fullscreenRef = ref()
|
||||||
|
if (props.showFullscreen && props.dblclickToFullscreen) {
|
||||||
|
watch(fullscreenRef, (fullscreenRefVal) => {
|
||||||
|
const headerElement = fullscreenRefVal?.parentElement
|
||||||
|
if (headerElement) {
|
||||||
|
const dblclickHandler = () => { isFullscreen.value = !isFullscreen.value }
|
||||||
|
headerElement.addEventListener('dblclick', dblclickHandler)
|
||||||
|
onBeforeUnmount(() => headerElement.removeEventListener('dblclick', dblclickHandler))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="showDialog"
|
v-model="showDialog"
|
||||||
:title="title"
|
class="common-window"
|
||||||
|
:header="header||title"
|
||||||
:before-close="calcBeforeClose"
|
:before-close="calcBeforeClose"
|
||||||
:width="width"
|
:width="width"
|
||||||
:draggable="draggable"
|
:draggable="draggable"
|
||||||
:overflow="true"
|
:overflow="overflow"
|
||||||
:destroy-on-close="destroyOnClose"
|
:destroy-on-close="destroyOnClose"
|
||||||
:close-on-click-modal="closeOnClickModal"
|
:close-on-click-modal="closeOnClickModal"
|
||||||
:close-on-press-escape="closeOnPressEscape"
|
:close-on-press-escape="closeOnPressEscape"
|
||||||
:append-to-body="appendToBody"
|
:append-to-body="appendToBody"
|
||||||
|
:show-close="showClose"
|
||||||
|
:fullscreen="isFullscreen"
|
||||||
>
|
>
|
||||||
|
<template #header="{ titleId, titleClass}">
|
||||||
|
<slot name="header">
|
||||||
|
<span
|
||||||
|
:id="titleId"
|
||||||
|
class="el-dialog__title"
|
||||||
|
:class="titleClass"
|
||||||
|
>
|
||||||
|
{{ header||title }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
<button
|
||||||
|
v-if="showFullscreen"
|
||||||
|
ref="fullscreenRef"
|
||||||
|
class="el-dialog__headerbtn dialog-fullscreen-btn"
|
||||||
|
style="right: 30px;"
|
||||||
|
@click="isFullscreen = !isFullscreen"
|
||||||
|
>
|
||||||
|
<common-icon
|
||||||
|
class="el-dialog__close"
|
||||||
|
:icon="isFullscreen?'FullscreenExitFilled':'FullscreenFilled'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
<el-container
|
<el-container
|
||||||
:class="defaultCls"
|
:class="defaultCls"
|
||||||
:style="{ height:height }"
|
:style="{ height:height }"
|
||||||
@@ -161,7 +222,10 @@ const calcBeforeClose = computed(() => {
|
|||||||
name="default"
|
name="default"
|
||||||
/>
|
/>
|
||||||
</el-container>
|
</el-container>
|
||||||
<template #footer>
|
<template
|
||||||
|
v-if="showButtons"
|
||||||
|
#footer
|
||||||
|
>
|
||||||
<span class="dialog-footer container-center">
|
<span class="dialog-footer container-center">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="showOk"
|
v-if="showOk"
|
||||||
@@ -177,7 +241,7 @@ const calcBeforeClose = computed(() => {
|
|||||||
</el-button>
|
</el-button>
|
||||||
<template v-for="(button, index) in buttons">
|
<template v-for="(button, index) in buttons">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!button.buttonIf||button.buttonIf()"
|
v-if="button.enabled!==false&&(!button.buttonIf||button.buttonIf())"
|
||||||
:key="index"
|
:key="index"
|
||||||
:type="button.type"
|
:type="button.type"
|
||||||
:icon="button.icon"
|
:icon="button.icon"
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ defineProps({
|
|||||||
|
|
||||||
const propConfig = ref({
|
const propConfig = ref({
|
||||||
placement: 'top-start',
|
placement: 'top-start',
|
||||||
rawContent: true
|
rawContent: true,
|
||||||
|
effect: 'dark'
|
||||||
})
|
})
|
||||||
|
|
||||||
const setConfig = (config) => {
|
const setConfig = (config) => {
|
||||||
@@ -42,6 +43,7 @@ defineExpose({
|
|||||||
v-bind="propConfig"
|
v-bind="propConfig"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
<!--eslint-disable-next-line vue/no-v-html-->
|
||||||
<div v-html="propConfig.content" />
|
<div v-html="propConfig.content" />
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isObject, isString } from 'lodash'
|
import { isObject, isString } from 'lodash-es'
|
||||||
import CommonTooltip from '@/components/directives/CommonTooltip.vue'
|
import CommonTooltip from '@/components/directives/CommonTooltip.vue'
|
||||||
import { DynamicHelper } from '@/components/directives/index'
|
import { DynamicHelper } from '@/components/directives/index'
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ const calcTooltipConfig = (binding) => {
|
|||||||
} else if (isString(binding.value)) {
|
} else if (isString(binding.value)) {
|
||||||
config.content = binding.value
|
config.content = binding.value
|
||||||
}
|
}
|
||||||
if (binding.arg) {
|
if (!config.placement && binding.arg) {
|
||||||
config.placement = binding.arg
|
config.placement = binding.arg
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|||||||
36
src/components/directives/SimpleDirectives.js
Normal file
36
src/components/directives/SimpleDirectives.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { $openNewWin } from '@/utils'
|
||||||
|
|
||||||
|
export const DisableAffixDirective = (el, binding) => {
|
||||||
|
if (binding.value) {
|
||||||
|
el.classList.add('disable-affix')
|
||||||
|
} else {
|
||||||
|
el.classList.remove('disable-affix')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标中键或者Ctrl+鼠标左键实现新窗口中打开
|
||||||
|
* @param el
|
||||||
|
* @param binding
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export const OpenNewWindowDirective = (el, binding) => {
|
||||||
|
if (binding.value) {
|
||||||
|
const config = {
|
||||||
|
click: event => event.button === 0 && event.ctrlKey,
|
||||||
|
mouseup: event => event.button === 1
|
||||||
|
}
|
||||||
|
for (const key in config) {
|
||||||
|
const handlerKey = `__newWindow__${key}`
|
||||||
|
el[handlerKey] && el.removeEventListener(key, el[handlerKey], true)
|
||||||
|
const handler = el[handlerKey] = event => {
|
||||||
|
if (config[key](event)) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
$openNewWin(binding.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
el.addEventListener(key, handler, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { CommonPopoverDirective, CommonTooltipDirective } from '@/components/directives/CommonTooltipDirective'
|
import { CommonPopoverDirective, CommonTooltipDirective } from '@/components/directives/CommonTooltipDirective'
|
||||||
|
import { OpenNewWindowDirective, DisableAffixDirective } from '@/components/directives/SimpleDirectives'
|
||||||
import { h, render } from 'vue'
|
import { h, render } from 'vue'
|
||||||
|
import { isFunction } from 'lodash-es'
|
||||||
|
import { ElConfigProvider } from 'element-plus'
|
||||||
|
import { elementLocale } from '@/messages'
|
||||||
|
|
||||||
export class DynamicHelper {
|
export class DynamicHelper {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -21,11 +25,21 @@ export class DynamicHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createAndRender (...args) {
|
createAndRender (fn, ...args) {
|
||||||
|
if (isFunction(fn)) { // 处理异步模式:()=>import('@/xxx.vue')
|
||||||
|
return fn().then(fnResult => this.createAndRender0(fnResult.default, ...args))
|
||||||
|
}
|
||||||
|
return this.createAndRender0(fn, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
createAndRender0 (...args) {
|
||||||
const container = this.container
|
const container = this.container
|
||||||
const vnode = h(...args)
|
const vnode = h(...args)
|
||||||
vnode.appContext = this.context
|
const configVNode = h(ElConfigProvider, { // 处理动态调用组件页面中element控件语言不正确问题
|
||||||
render(vnode, container)
|
locale: elementLocale.value.localeData
|
||||||
|
}, () => [vnode])
|
||||||
|
configVNode.appContext = this.context
|
||||||
|
render(configVNode, container)
|
||||||
const appDiv = document.getElementById(this.appDivId)
|
const appDiv = document.getElementById(this.appDivId)
|
||||||
if (appDiv && container.firstElementChild) {
|
if (appDiv && container.firstElementChild) {
|
||||||
appDiv.appendChild(container.firstElementChild)
|
appDiv.appendChild(container.firstElementChild)
|
||||||
@@ -39,5 +53,7 @@ export default {
|
|||||||
DynamicHelper.app = Vue
|
DynamicHelper.app = Vue
|
||||||
Vue.directive('common-tooltip', CommonTooltipDirective)
|
Vue.directive('common-tooltip', CommonTooltipDirective)
|
||||||
Vue.directive('common-popover', CommonPopoverDirective)
|
Vue.directive('common-popover', CommonPopoverDirective)
|
||||||
|
Vue.directive('disable-affix', DisableAffixDirective)
|
||||||
|
Vue.directive('open-new-window', OpenNewWindowDirective)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import CommonMenu from '@/components/common-menu/index.vue'
|
|||||||
import CommonMenuItem from '@/components/common-menu-item/index.vue'
|
import CommonMenuItem from '@/components/common-menu-item/index.vue'
|
||||||
import CommonTabsView from '@/components/common-tabs-view/index.vue'
|
import CommonTabsView from '@/components/common-tabs-view/index.vue'
|
||||||
import CommonTable from '@/components/common-table/index.vue'
|
import CommonTable from '@/components/common-table/index.vue'
|
||||||
|
import CommonTableV2 from '@/components/common-table/common-table-v2.vue'
|
||||||
import CommonTableForm from '@/components/common-table-form/index.vue'
|
import CommonTableForm from '@/components/common-table-form/index.vue'
|
||||||
import CommonBreadcrumb from '@/components/common-breadcrumb/index.vue'
|
import CommonBreadcrumb from '@/components/common-breadcrumb/index.vue'
|
||||||
import CommonWindow from '@/components/common-window/index.vue'
|
import CommonWindow from '@/components/common-window/index.vue'
|
||||||
@@ -35,6 +36,7 @@ export default {
|
|||||||
Vue.component('CommonMenuItem', CommonMenuItem)
|
Vue.component('CommonMenuItem', CommonMenuItem)
|
||||||
Vue.component('CommonTabsView', CommonTabsView)
|
Vue.component('CommonTabsView', CommonTabsView)
|
||||||
Vue.component('CommonTable', CommonTable)
|
Vue.component('CommonTable', CommonTable)
|
||||||
|
Vue.component('CommonTableV2', CommonTableV2)
|
||||||
Vue.component('CommonTableForm', CommonTableForm)
|
Vue.component('CommonTableForm', CommonTableForm)
|
||||||
Vue.component('CommonBreadcrumb', CommonBreadcrumb)
|
Vue.component('CommonBreadcrumb', CommonBreadcrumb)
|
||||||
Vue.component('CommonWindow', CommonWindow)
|
Vue.component('CommonWindow', CommonWindow)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ref, unref } from 'vue'
|
import { ref, unref } from 'vue'
|
||||||
import { $i18nBundle, $i18nKey } from '@/messages'
|
import { $i18nBundle, $i18nKey } from '@/messages'
|
||||||
import { isArray, isObject, isFunction } from 'lodash'
|
import { isArray, isFunction, isObject } from 'lodash-es'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
export const getFrontendPage = (totalCount, pageSize, pageNumber = 1) => {
|
export const getFrontendPage = (totalCount, pageSize, pageNumber = 1) => {
|
||||||
const pageCount = Math.floor((totalCount + pageSize - 1) / pageSize)
|
const pageCount = Math.floor((totalCount + pageSize - 1) / pageSize)
|
||||||
@@ -138,10 +137,27 @@ export const parsePathParams = (path, params) => {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const proxyMethod = (targets = [], methodName) => {
|
||||||
|
return (...args) => {
|
||||||
|
const results = []
|
||||||
|
for (let target of targets.filter(target => !!unref(target))) {
|
||||||
|
target = unref(target)
|
||||||
|
const method = target[methodName]
|
||||||
|
if (isFunction(method)) {
|
||||||
|
results.push(method.call(target, ...args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isObject(results[0]) && isFunction(results[0]?.then)) {
|
||||||
|
return Promise.all(results)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义表单选项,带有jsdoc注解,方便代码提示
|
* 定义表单选项,带有jsdoc注解,方便代码提示
|
||||||
* @param {CommonFormOption[]} formOptions 表单选项
|
* @param {CommonFormOption|CommonFormOption[]} formOptions 表单选项
|
||||||
* @return {CommonFormOption[]} 表单选项配置
|
* @return {CommonFormOption|CommonFormOption[]} 表单选项配置
|
||||||
*/
|
*/
|
||||||
export const defineFormOptions = (formOptions) => {
|
export const defineFormOptions = (formOptions) => {
|
||||||
return formOptions
|
return formOptions
|
||||||
@@ -171,31 +187,3 @@ export const defineTableButtons = (tableButtons) => {
|
|||||||
export const defineMenuItems = (menuItems) => {
|
export const defineMenuItems = (menuItems) => {
|
||||||
return menuItems
|
return menuItems
|
||||||
}
|
}
|
||||||
export const formatDate = (date, format) => {
|
|
||||||
if (date) {
|
|
||||||
return dayjs(date).format(format || 'YYYY-MM-DD HH:mm:ss')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatDay = (date, format) => {
|
|
||||||
if (date) {
|
|
||||||
return dayjs(date).format(format || 'YYYY-MM-DD')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const proxyMethod = (targets = [], methodName) => {
|
|
||||||
return (...args) => {
|
|
||||||
const results = []
|
|
||||||
for (let target of targets) {
|
|
||||||
target = unref(target)
|
|
||||||
const method = target[methodName]
|
|
||||||
if (isFunction(method)) {
|
|
||||||
results.push(method.call(target, ...args))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isObject(results[0]) && isFunction(results[0]?.then)) {
|
|
||||||
return Promise.all(results)
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,6 +4,76 @@
|
|||||||
*/
|
*/
|
||||||
export const PAGE_SIZE = 10
|
export const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数保存过期时间,单位分钟
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
export const SEARCH_PARAM_TIMEOUT = 10
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认是否用全局loading
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const GLOBAL_LOADING = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新自定义loading用于route加载
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const GLOBAL_ROUTE_NEW_LOADING = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认是否有全局错误消息
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const GLOBAL_ERROR_MESSAGE = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认是否启用路由Loading
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const GLOBAL_ROUTE_LOADING = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loading延迟,单位毫秒
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
export const LOADING_DELAY = 200
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 国际化开关,国际化未开发完成前可能需要关闭
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const I18N_ENABLED = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黑色主题开关
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const THEME_ENABLED = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动layout开关
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const AUTO_LAYOUT_ENABLED = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大缓存页面数量
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
export const TAB_MODE_MAX_CACHES = 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有记住参数功能
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
export const REMEMBER_SEARCH_PARAM_ENABLED = true
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认分页数据
|
* 默认分页数据
|
||||||
*
|
*
|
||||||
@@ -16,3 +86,9 @@ export const useDefaultPage = (pageSize = PAGE_SIZE) => {
|
|||||||
pageNumber: 1
|
pageNumber: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL
|
||||||
|
|
||||||
|
export const SYSTEM_KEY = import.meta.env.VITE_APP_SYSTEM_KEY
|
||||||
|
|
||||||
|
export const APP_VERSION = import.meta.env.VITE_APP_VERSION
|
||||||
|
|||||||
@@ -17,3 +17,11 @@ export const GlobalLocales = {
|
|||||||
CN: 'zh-CN',
|
CN: 'zh-CN',
|
||||||
EN: 'en-US'
|
EN: 'en-US'
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 搜索调条件记住
|
||||||
|
*/
|
||||||
|
export const LoadSaveParamMode = {
|
||||||
|
ALL: 'all',
|
||||||
|
BACK: 'back',
|
||||||
|
NEVER: 'never'
|
||||||
|
}
|
||||||
|
|||||||
70
src/hooks/CommonHooks.js
Normal file
70
src/hooks/CommonHooks.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { isFunction, isNumber } from 'lodash-es'
|
||||||
|
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用搜索表格表单封装
|
||||||
|
* @param {CommonTableAndSearchForm} param 参数
|
||||||
|
* @return {CommonTableAndSearchResult} 返回数据
|
||||||
|
*/
|
||||||
|
export const useTableAndSearchForm = ({
|
||||||
|
searchMethod,
|
||||||
|
defaultParam = {},
|
||||||
|
dataProcessor,
|
||||||
|
pageProcessor,
|
||||||
|
saveParam = true
|
||||||
|
}) => {
|
||||||
|
const globalSearchParamStore = useGlobalSearchParamStore()
|
||||||
|
const tableData = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const searchParam = ref(saveParam ? globalSearchParamStore.getCurrentParam(defaultParam) : defaultParam)
|
||||||
|
const searchTableItems = async (pageNumber, newParams = {}) => {
|
||||||
|
if (isNumber(pageNumber)) {
|
||||||
|
searchParam.value?.pageSetting && (searchParam.value.pageSetting.pageNumber = pageNumber)
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
saveParam && globalSearchParamStore.saveCurrentParam(searchParam.value)
|
||||||
|
const searchResult = await searchMethod({ ...searchParam.value, ...newParams })
|
||||||
|
.finally(() => { loading.value = false })
|
||||||
|
loading.value = false
|
||||||
|
if (searchResult.success && searchResult.resultData) {
|
||||||
|
const resultData = searchResult.resultData
|
||||||
|
tableData.value = isFunction(dataProcessor) && dataProcessor?.(resultData, searchParam)
|
||||||
|
pageProcessor = pageProcessor || ((resultData, searchParam) => {
|
||||||
|
searchParam.value?.pageSetting && resultData.pageSetting && Object.assign(searchParam.value.pageSetting, resultData.pageSetting || {})
|
||||||
|
})
|
||||||
|
pageProcessor?.(resultData, searchParam)
|
||||||
|
}
|
||||||
|
return searchResult
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tableData,
|
||||||
|
loading,
|
||||||
|
searchParam,
|
||||||
|
searchMethod: searchTableItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDateStr = (watchFn) => {
|
||||||
|
const dateStr = ref(new Date().getTime())
|
||||||
|
watchFn && watch(watchFn, (update) => {
|
||||||
|
if (update) {
|
||||||
|
dateStr.value = new Date().getTime()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { dateStr }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGlobalSaveSearchParam = (defaultParam) => {
|
||||||
|
const globalSearchParamStore = useGlobalSearchParamStore()
|
||||||
|
const searchParam = ref(globalSearchParamStore.getCurrentParam(defaultParam))
|
||||||
|
return {
|
||||||
|
searchParam,
|
||||||
|
/**
|
||||||
|
* @param [path]
|
||||||
|
*/
|
||||||
|
saveSearchParam: (path) => {
|
||||||
|
globalSearchParamStore.saveCurrentParam(searchParam.value, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/hooks/public.d.ts
vendored
Normal file
16
src/hooks/public.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
export interface CommonTableAndSearchForm {
|
||||||
|
defaultParam?: any;
|
||||||
|
searchMethod: (searchParam: any) => Promise<any>;
|
||||||
|
dataProcessor?: (resultData: any, searchParam: any) => any[];
|
||||||
|
pageProcessor?: (resultData: any, searchParam: any) => void;
|
||||||
|
saveParam?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommonTableAndSearchResult {
|
||||||
|
tableData: Ref<Array<any>>;
|
||||||
|
loading: Ref<boolean>;
|
||||||
|
searchParam: Ref<any>;
|
||||||
|
searchMethod: (pageNumber?: number) => Promise<any>;
|
||||||
|
}
|
||||||
53
src/hooks/useDisableAffix.js
Normal file
53
src/hooks/useDisableAffix.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { ref, defineComponent, h, watch, computed } from 'vue'
|
||||||
|
import { ElButton } from 'element-plus'
|
||||||
|
import CommonIcon from '@/components/common-icon/index.vue'
|
||||||
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
|
import { useElementSize } from '@vueuse/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制浮窗是否固定
|
||||||
|
* @param scrollSelector
|
||||||
|
* @param config
|
||||||
|
* @return {{affixChangeButton: DefineSetupFnComponent<Record<string, any>, {}, {}>, disableAffix: Ref<UnwrapRef<boolean>>}}
|
||||||
|
*/
|
||||||
|
export const useDisableAffix = (scrollSelector = '.home-main', config = {}) => {
|
||||||
|
const disableAffix = ref(false)
|
||||||
|
const toggleDisableAffix = () => {
|
||||||
|
disableAffix.value = !disableAffix.value
|
||||||
|
}
|
||||||
|
const AffixToggleButton = defineComponent(() => {
|
||||||
|
return () => h(ElButton, Object.assign({
|
||||||
|
style: 'float:right',
|
||||||
|
size: 'small',
|
||||||
|
type: disableAffix.value ? 'warning' : 'primary',
|
||||||
|
onClick: toggleDisableAffix
|
||||||
|
}, config), () => [h(CommonIcon, {
|
||||||
|
size: 18,
|
||||||
|
icon: disableAffix.value ? 'PinOffFilled' : 'PushPinFilled'
|
||||||
|
})])
|
||||||
|
})
|
||||||
|
watch([() => useGlobalConfigStore().layoutMode, () => useGlobalConfigStore().isCollapseLeft], () => {
|
||||||
|
const container = document.querySelector(scrollSelector)
|
||||||
|
container?.scrollTo({ top: 0 })
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
disableAffix,
|
||||||
|
AffixToggleButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useElementAffixOffset = (disableAffix, targetOffset = 20, offset = 10) => {
|
||||||
|
const targetElementRef = ref(null)
|
||||||
|
const { height } = useElementSize(targetElementRef)
|
||||||
|
|
||||||
|
const targetAffixOffset = computed(() => {
|
||||||
|
if (height.value && !disableAffix.value) {
|
||||||
|
return height.value + targetOffset
|
||||||
|
}
|
||||||
|
return offset
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
targetElementRef,
|
||||||
|
targetAffixOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
import * as MaterialIconsVue from '@vicons/material'
|
import * as MaterialIconsVue from '@vicons/material'
|
||||||
import kebabCase from 'lodash/kebabCase'
|
import { kebabCase } from 'lodash-es'
|
||||||
|
|
||||||
export const INSTALL_ICONS = []
|
export const INSTALL_ICONS = []
|
||||||
export const ICON_PREFIX = 'icon-'
|
export const ICON_PREFIX = 'icon-'
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
import 'element-plus/dist/index.css'
|
|
||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
|
||||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||||
|
import '@/vendors/dayjs.js'
|
||||||
|
import ElementPlus from '@/vendors/element-plus'
|
||||||
import stores from '@/stores'
|
import stores from '@/stores'
|
||||||
import icons from '@/icons'
|
import icons from '@/icons'
|
||||||
import messages from '@/messages'
|
import messages from '@/messages'
|
||||||
@@ -13,6 +12,7 @@ import App from '@/App.vue'
|
|||||||
import router from '@/route/routes'
|
import router from '@/route/routes'
|
||||||
|
|
||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
import MonacoEditor from '@/vendors/monaco-editor'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
@@ -23,5 +23,6 @@ app.use(ElementPlus)
|
|||||||
app.use(icons)
|
app.use(icons)
|
||||||
app.use(messages)
|
app.use(messages)
|
||||||
app.use(commons)
|
app.use(commons)
|
||||||
|
app.use(MonacoEditor)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import cloneDeep from 'lodash/cloneDeep'
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
const base = { // 预定义几种属性
|
const base = { // 预定义几种属性
|
||||||
label: {},
|
label: {},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ common.label.settings = '设置'
|
|||||||
common.label.confirm = '确认'
|
common.label.confirm = '确认'
|
||||||
common.label.save = '保存'
|
common.label.save = '保存'
|
||||||
common.label.close = '关闭'
|
common.label.close = '关闭'
|
||||||
|
common.label.copy = '复制'
|
||||||
common.label.cancel = '取消'
|
common.label.cancel = '取消'
|
||||||
common.label.refresh = '刷新'
|
common.label.refresh = '刷新'
|
||||||
common.label.closeOther = '关闭其他'
|
common.label.closeOther = '关闭其他'
|
||||||
@@ -46,6 +47,23 @@ common.label.keywords = '关键字'
|
|||||||
common.label.breadcrumb = '面包屑导航'
|
common.label.breadcrumb = '面包屑导航'
|
||||||
common.label.username = '用户名'
|
common.label.username = '用户名'
|
||||||
common.label.password = '密码'
|
common.label.password = '密码'
|
||||||
|
common.label.backtop = '回到顶部'
|
||||||
|
common.label.format = '格式化'
|
||||||
|
common.label.saveParamMode = '记住搜索条件'
|
||||||
|
common.label.allSaveParamMode = '自动记住'
|
||||||
|
common.label.backSaveParamMode = '仅返回时记住'
|
||||||
|
common.label.neverSaveParamMode = '不记住'
|
||||||
|
|
||||||
|
//= ============通用============
|
||||||
|
common.label.commonCode = '{0}代码'
|
||||||
|
common.label.commonConfig = '配置{0}'
|
||||||
|
common.label.commonEdit = '{0}编辑'
|
||||||
|
common.label.commonAdd = '新增{0}'
|
||||||
|
common.label.commonDelete = '删除{0}'
|
||||||
|
common.label.commonParent = '上级{0}'
|
||||||
|
common.label.commonAdd1 = '添加{0}'
|
||||||
|
common.label.commonCopy = '复制{0}'
|
||||||
|
common.label.commonSwap = '交换{0}'
|
||||||
|
|
||||||
//* =======================msg=====================//
|
//* =======================msg=====================//
|
||||||
common.msg.nonNull = '{0}不能为空'
|
common.msg.nonNull = '{0}不能为空'
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ common.label.settings = 'Settings'
|
|||||||
common.label.confirm = 'Confirm'
|
common.label.confirm = 'Confirm'
|
||||||
common.label.save = 'Save'
|
common.label.save = 'Save'
|
||||||
common.label.close = 'Close'
|
common.label.close = 'Close'
|
||||||
|
common.label.copy = 'Copy'
|
||||||
common.label.cancel = 'Cancel'
|
common.label.cancel = 'Cancel'
|
||||||
common.label.refresh = 'Refresh'
|
common.label.refresh = 'Refresh'
|
||||||
common.label.closeOther = 'Close Others'
|
common.label.closeOther = 'Close Others'
|
||||||
@@ -46,6 +47,23 @@ common.label.keywords = 'Keywords'
|
|||||||
common.label.breadcrumb = 'Breadcrumb'
|
common.label.breadcrumb = 'Breadcrumb'
|
||||||
common.label.username = 'User Name'
|
common.label.username = 'User Name'
|
||||||
common.label.password = 'Password'
|
common.label.password = 'Password'
|
||||||
|
common.label.backtop = 'Back to top'
|
||||||
|
common.label.format = 'Format'
|
||||||
|
common.label.saveParamMode = 'Remember Search Mode'
|
||||||
|
common.label.allSaveParamMode = 'Remember All'
|
||||||
|
common.label.backSaveParamMode = 'Remember Search on Back'
|
||||||
|
common.label.neverSaveParamMode = 'Never Remember'
|
||||||
|
|
||||||
|
//= ============通用============
|
||||||
|
common.label.commonConfig = 'Config {0}'
|
||||||
|
common.label.commonCode = '{0} Code'
|
||||||
|
common.label.commonEdit = '{0} Edit'
|
||||||
|
common.label.commonAdd = 'Add {0}'
|
||||||
|
common.label.commonDelete = 'Delete {0}'
|
||||||
|
common.label.commonParent = 'Parent {0}'
|
||||||
|
common.label.commonAdd1 = 'Add {0}'
|
||||||
|
common.label.commonCopy = 'Copy {0}'
|
||||||
|
common.label.commonSwap = 'Swap {0}'
|
||||||
|
|
||||||
//* =======================msg=====================//
|
//* =======================msg=====================//
|
||||||
common.msg.nonNull = '{0} is required.'
|
common.msg.nonNull = '{0} is required.'
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'dayjs/locale/zh-cn'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
import { GlobalLocales } from '@/consts/GlobalConstants'
|
import { GlobalLocales } from '@/consts/GlobalConstants'
|
||||||
|
import { isArray, isString } from 'lodash-es'
|
||||||
|
|
||||||
const DEFAULT_LOCALE = 'zh-CN'
|
const DEFAULT_LOCALE = 'zh-CN'
|
||||||
dayjs.locale(DEFAULT_LOCALE) // dayjs的语言配置
|
dayjs.locale(DEFAULT_LOCALE) // dayjs的语言配置
|
||||||
@@ -57,7 +58,12 @@ export const $i18nMsg = (cn, en, replaceEmpty = true) => {
|
|||||||
* @param {String[]=} params 可选参数
|
* @param {String[]=} params 可选参数
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export const $i18nBundle = i18n.global.t
|
export const $i18nBundle = (key, params) => {
|
||||||
|
if (!key || !isString(key) || !key.includes('.')) { // 仅处理含有.的key
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return i18n.global.t(key, params)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据key和locale返回数据<br>
|
* 根据key和locale返回数据<br>
|
||||||
@@ -78,14 +84,22 @@ export const $i18nByLocale = (key, locale, args) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 方便多个资源key解析
|
* 方便多个资源key解析
|
||||||
* @param {String} key 国际化资源key
|
* @param {String|[String]} key 国际化资源key
|
||||||
* @param {String} args 可选参数,也是资源key,方便多个资源key解析
|
* @param {String} args 可选参数,也是资源key,方便多个资源key解析
|
||||||
*/
|
*/
|
||||||
export const $i18nKey = (key, ...args) => {
|
export const $i18nKey = (key, ...args) => {
|
||||||
|
if (isArray(key)) {
|
||||||
|
args = key.slice(1)
|
||||||
|
key = key[0]
|
||||||
|
}
|
||||||
args = args.map(argKey => $i18nBundle(argKey))
|
args = args.map(argKey => $i18nBundle(argKey))
|
||||||
return $i18nBundle(key, args)
|
return $i18nBundle(key, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const $i18nConcat = (...items) => {
|
||||||
|
return items.map(item => (item ?? '')).filter(item => !!item).join($i18nMsg('', ' ', false))
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install (app) {
|
install (app) {
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
@@ -95,6 +109,7 @@ export default {
|
|||||||
$i18nKey,
|
$i18nKey,
|
||||||
$i18nBundle,
|
$i18nBundle,
|
||||||
$isLocale,
|
$isLocale,
|
||||||
|
$i18nConcat,
|
||||||
$i18nByLocale
|
$i18nByLocale
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
155
src/route/RouteUtils.js
Normal file
155
src/route/RouteUtils.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { parsePathParams, toLabelByKey, useMenuInfo, useMenuName } from '@/components/utils'
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import { useScroll, useEventListener } from '@vueuse/core'
|
||||||
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
|
import { useBreadcrumbConfigStore } from '@/stores/BreadcrumbConfigStore'
|
||||||
|
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||||
|
import { isArray } from 'lodash-es'
|
||||||
|
import { $i18nBundle } from '@/messages'
|
||||||
|
|
||||||
|
const labelConfig2MenuInfo = (labelConfig, existItem = {}) => {
|
||||||
|
if (labelConfig) {
|
||||||
|
existItem = { ...existItem }
|
||||||
|
existItem.menuName = labelConfig.label || toLabelByKey(labelConfig.labelKey) || labelConfig.menuName || existItem.menuName
|
||||||
|
existItem.icon = labelConfig.icon || existItem.icon
|
||||||
|
existItem.path = labelConfig.path || existItem.path
|
||||||
|
}
|
||||||
|
return existItem
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算匹配路由列表,用于生成面包屑
|
||||||
|
* @param route
|
||||||
|
* @param labelConfig
|
||||||
|
* @return {[{path: string, menuName: string, icon: string}]}
|
||||||
|
*/
|
||||||
|
export const calcMatchedRoutes = (route, labelConfig) => {
|
||||||
|
const exists = []
|
||||||
|
/**
|
||||||
|
* @type {[{path: string, menuName: string, icon: string}]}
|
||||||
|
*/
|
||||||
|
const results = route.matched.filter(item => item.meta?.breadcrumb !== false).map((item, index) => {
|
||||||
|
item = index === route.matched.length - 1 ? route : item
|
||||||
|
const menuInfo = useMenuInfo(item)
|
||||||
|
let icon = ''
|
||||||
|
if (menuInfo && menuInfo.icon) {
|
||||||
|
icon = menuInfo.icon
|
||||||
|
} else if (item.meta && item.meta.icon) {
|
||||||
|
icon = item.meta.icon
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
path: parsePathParams(item.path, route.params),
|
||||||
|
menuName: useMenuName(item),
|
||||||
|
icon
|
||||||
|
}
|
||||||
|
}).filter(item => {
|
||||||
|
const notExist = !exists.includes(item.menuName)
|
||||||
|
if (notExist) {
|
||||||
|
exists.push(item.menuName)
|
||||||
|
}
|
||||||
|
return notExist && !item.menuName.endsWith('Base')
|
||||||
|
})
|
||||||
|
if (labelConfig && results.length) {
|
||||||
|
const lastItem = results.pop()
|
||||||
|
let appendItems = []
|
||||||
|
if (isArray(labelConfig)) {
|
||||||
|
appendItems = labelConfig.slice(0, labelConfig.length - 1)
|
||||||
|
labelConfig = labelConfig[labelConfig.length - 1]
|
||||||
|
}
|
||||||
|
results.push(...appendItems.map(config => labelConfig2MenuInfo(config)))
|
||||||
|
results.push(labelConfig2MenuInfo(labelConfig, lastItem))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题计算
|
||||||
|
* @param route
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const calcRouteTitle = (route) => {
|
||||||
|
const labelConfig = useBreadcrumbConfigStore.breadcrumbConfig
|
||||||
|
let title = $i18nBundle('common.label.title')
|
||||||
|
const routes = calcMatchedRoutes(route, labelConfig)
|
||||||
|
const item = routes?.[routes?.length - 1]
|
||||||
|
if (item?.menuName && item.path !== '/' && item.path !== '/login') {
|
||||||
|
title = `${item.menuName}`
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkMataReplaceHistory = (historyTab, tab) => {
|
||||||
|
// 如果meta中配置有replaceTabHistory,默认替换相关的tab
|
||||||
|
return historyTab?.meta?.replaceTabHistory && historyTab.meta.replaceTabHistory === tab?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isSameReplaceHistory = (historyTab, tab) => {
|
||||||
|
return historyTab?.meta?.replaceTabHistory && tab?.meta?.replaceTabHistory &&
|
||||||
|
historyTab.meta.replaceTabHistory === tab.meta.replaceTabHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkReplaceHistoryShouldReplace = (historyTab, tab) => {
|
||||||
|
return checkMataReplaceHistory(historyTab, tab) || checkMataReplaceHistory(tab, historyTab) || isSameReplaceHistory(historyTab, tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记住滚动位置
|
||||||
|
*/
|
||||||
|
export const useTabModeScrollSaver = () => {
|
||||||
|
onMounted(() => {
|
||||||
|
const tabsViewStore = useTabsViewStore()
|
||||||
|
const { y } = useScroll(document.querySelector('.home-main'))
|
||||||
|
watch(y, val => {
|
||||||
|
if (tabsViewStore.isTabMode && tabsViewStore.isCachedTabMode && tabsViewStore.currentTabItem) { // tab模式下监控滚动位置并保存
|
||||||
|
tabsViewStore.currentTabItem.scroll = {
|
||||||
|
top: val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getParentRootKey = (route) => {
|
||||||
|
if (isNestedRoute(route) && route.meta?.replaceTabHistory && route.meta?.cache !== false) {
|
||||||
|
return route.meta.replaceTabHistory
|
||||||
|
}
|
||||||
|
return route.fullPath
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isNestedRoute = (route) => {
|
||||||
|
return !!route?.meta?.nested
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取动画key,用于父子组件动画问题
|
||||||
|
* @param route {import('vue-router').Route}
|
||||||
|
* @param matcher {Function}
|
||||||
|
* @return {Ref<*>}
|
||||||
|
*/
|
||||||
|
export const useTransitionKey = (route, matcher) => {
|
||||||
|
let lastTransitionKey = route.fullPath
|
||||||
|
const transitionKey = ref('')
|
||||||
|
const calcTransitionKey = () => {
|
||||||
|
if (matcher(route) && lastTransitionKey !== route.fullPath) {
|
||||||
|
console.log('=========================', lastTransitionKey, route.fullPath)
|
||||||
|
lastTransitionKey = route.fullPath
|
||||||
|
}
|
||||||
|
return lastTransitionKey
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
matcher(route) && (transitionKey.value = calcTransitionKey())
|
||||||
|
})
|
||||||
|
watch(() => route.fullPath, () => {
|
||||||
|
matcher(route) && (transitionKey.value = calcTransitionKey())
|
||||||
|
})
|
||||||
|
return transitionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRoutePopStateEvent = () => {
|
||||||
|
useEventListener(window, 'popstate', () => {
|
||||||
|
const { from, to } = useGlobalSearchParamStore().savedParamRouteInfo
|
||||||
|
if (!useTabsViewStore().isTabMode || checkReplaceHistoryShouldReplace(from, to)) {
|
||||||
|
useGlobalSearchParamStore().setSaveParamBack(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -10,6 +10,14 @@ export default [{
|
|||||||
path: '/tests',
|
path: '/tests',
|
||||||
name: 'TestPage',
|
name: 'TestPage',
|
||||||
component: () => import('@/views/tools/TestPage.vue')
|
component: () => import('@/views/tools/TestPage.vue')
|
||||||
|
}, {
|
||||||
|
path: '/editors',
|
||||||
|
name: 'Editors',
|
||||||
|
component: () => import('@/views/tools/Editors.vue')
|
||||||
|
}, {
|
||||||
|
path: '/charts',
|
||||||
|
name: 'Charts',
|
||||||
|
component: () => import('@/views/tools/Charts.vue')
|
||||||
}, {
|
}, {
|
||||||
path: '/window-forms',
|
path: '/window-forms',
|
||||||
name: 'WindowForms',
|
name: 'WindowForms',
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
|||||||
import Login from '@/views/Login.vue'
|
import Login from '@/views/Login.vue'
|
||||||
import AdminRoutes from '@/route/AdminRoutes'
|
import AdminRoutes from '@/route/AdminRoutes'
|
||||||
import ToolsRoutes from '@/route/ToolsRoutes'
|
import ToolsRoutes from '@/route/ToolsRoutes'
|
||||||
import { checkRouteAuthority } from '@/authority'
|
import { checkRouteAuthority, processRouteLoading } from '@/authority'
|
||||||
|
import { checkReplaceHistoryShouldReplace } from '@/route/RouteUtils'
|
||||||
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
@@ -55,6 +57,29 @@ const router = createRouter({
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const scrollMain = (to, scrollOption) => {
|
||||||
|
setTimeout(() => { // 因为有0.3s动画需要延迟到动画之后
|
||||||
|
scrollOption = scrollOption || to?.meta?.scroll || { top: 0 }
|
||||||
|
console.log('======================scrollTo', scrollOption.top)
|
||||||
|
document.querySelector('.home-main')?.scrollTo(scrollOption)
|
||||||
|
}, 350)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义路由滚动行为,在home-main容器中滚动顶部
|
||||||
|
* @param to
|
||||||
|
* @param from
|
||||||
|
*/
|
||||||
|
export const routeScrollBehavior = (to, from) => {
|
||||||
|
const tabsViewStore = useTabsViewStore()
|
||||||
|
const scrollOption = !checkReplaceHistoryShouldReplace(to, from) ? tabsViewStore.currentTabItem?.scroll : undefined
|
||||||
|
scrollMain(to, scrollOption)
|
||||||
|
}
|
||||||
|
|
||||||
router.beforeEach(checkRouteAuthority)
|
router.beforeEach(checkRouteAuthority)
|
||||||
|
router.afterEach((...args) => {
|
||||||
|
processRouteLoading(...args)
|
||||||
|
routeScrollBehavior(...args)
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const useCityAutocompleteConfig = () => {
|
|||||||
label: $i18nMsg('英文名', 'EN Name'),
|
label: $i18nMsg('英文名', 'EN Name'),
|
||||||
property: 'nameEn'
|
property: 'nameEn'
|
||||||
}],
|
}],
|
||||||
searchMethod ({ query, page }, cb) {
|
searchMethod ({ page }, cb) {
|
||||||
loadAutoCities({ page }) // {query, page}
|
loadAutoCities({ page }) // {query, page}
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const data = {
|
const data = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { INSTALL_ICONS } from '@/icons'
|
import { INSTALL_ICONS } from '@/icons'
|
||||||
import chunk from 'lodash/chunk'
|
import { chunk } from 'lodash-es'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param keywords {string}
|
* @param keywords {string}
|
||||||
@@ -9,7 +9,14 @@ import chunk from 'lodash/chunk'
|
|||||||
export const filterIconsByKeywords = (keywords, colSize) => {
|
export const filterIconsByKeywords = (keywords, colSize) => {
|
||||||
let installIcons = INSTALL_ICONS
|
let installIcons = INSTALL_ICONS
|
||||||
if (keywords) {
|
if (keywords) {
|
||||||
installIcons = installIcons.filter(icon => icon.toLowerCase().includes(keywords.toLowerCase()))
|
installIcons = installIcons.filter(icon => {
|
||||||
|
keywords = keywords.trim()
|
||||||
|
if (keywords.includes(' ')) {
|
||||||
|
return keywords.split(/\s+/).every(k => icon.toLowerCase().includes(k.toLowerCase()))
|
||||||
|
} else {
|
||||||
|
return icon.toLowerCase().includes(keywords.toLowerCase())
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return chunk(installIcons, colSize).map((arr, idx) => {
|
return chunk(installIcons, colSize).map((arr, idx) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { $i18nMsg } from '@/messages'
|
|||||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
import { GlobalLocales } from '@/consts/GlobalConstants'
|
import { GlobalLocales } from '@/consts/GlobalConstants'
|
||||||
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
||||||
|
import { I18N_ENABLED, THEME_ENABLED } from '@/config'
|
||||||
|
import { $logout } from '@/utils'
|
||||||
|
|
||||||
export const searchMenusResult = (queryParam, config) => {
|
export const searchMenusResult = (queryParam, config) => {
|
||||||
return $httpPost('/api/searchMenus', queryParam, config)
|
return $httpPost('/api/searchMenus', queryParam, config)
|
||||||
@@ -136,6 +138,7 @@ export const useThemeAndLocaleMenus = () => {
|
|||||||
return [{
|
return [{
|
||||||
icon: 'LanguageFilled',
|
icon: 'LanguageFilled',
|
||||||
isDropdown: true,
|
isDropdown: true,
|
||||||
|
enabled: I18N_ENABLED,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
iconIf: () => GlobalLocales.CN === globalConfigStore.currentLocale ? 'check' : '',
|
iconIf: () => GlobalLocales.CN === globalConfigStore.currentLocale ? 'check' : '',
|
||||||
@@ -151,6 +154,7 @@ export const useThemeAndLocaleMenus = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
isDropdown: true,
|
isDropdown: true,
|
||||||
|
enabled: THEME_ENABLED,
|
||||||
iconIf: () => !globalConfigStore.isDarkTheme ? 'moon' : 'sunny',
|
iconIf: () => !globalConfigStore.isDarkTheme ? 'moon' : 'sunny',
|
||||||
click: () => globalConfigStore.changeTheme(!globalConfigStore.isDarkTheme)
|
click: () => globalConfigStore.changeTheme(!globalConfigStore.isDarkTheme)
|
||||||
}]
|
}]
|
||||||
@@ -189,9 +193,8 @@ export const useBaseTopMenus = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
labelKey: 'common.label.logout',
|
labelKey: 'common.label.logout',
|
||||||
click (router) {
|
click () {
|
||||||
loginConfigStore.logout()
|
$logout()
|
||||||
router.push('/login')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const useUserAutocompleteConfig = () => {
|
|||||||
property: 'address',
|
property: 'address',
|
||||||
width: '300px'
|
width: '300px'
|
||||||
}],
|
}],
|
||||||
searchMethod ({ query, page }, cb) {
|
searchMethod ({ page }, cb) {
|
||||||
loadUsersResult({ page })
|
loadUsersResult({ page })
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const data = {
|
const data = {
|
||||||
|
|||||||
28
src/stores/BreadcrumbConfigStore.js
Normal file
28
src/stores/BreadcrumbConfigStore.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { computed, ref, nextTick } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
|
export const useBreadcrumbConfigStore = defineStore('breadcrumbConfig', () => {
|
||||||
|
const internalBreadcrumbConfig = ref()
|
||||||
|
const tabViewStore = useTabsViewStore()
|
||||||
|
const setBreadcrumbConfig = (config) => {
|
||||||
|
internalBreadcrumbConfig.value = config
|
||||||
|
if (tabViewStore.isTabMode && tabViewStore.currentTabItem && config) {
|
||||||
|
nextTick(() => {
|
||||||
|
tabViewStore.currentTabItem.labelConfig = config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const breadcrumbConfig = computed(() => {
|
||||||
|
if (tabViewStore.isTabMode && tabViewStore.currentTabItem) {
|
||||||
|
return tabViewStore.currentTabItem.labelConfig
|
||||||
|
}
|
||||||
|
return internalBreadcrumbConfig.value
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
breadcrumbConfig,
|
||||||
|
setBreadcrumbConfig,
|
||||||
|
clearBreadcrumbConfig: () => {
|
||||||
|
setBreadcrumbConfig(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark, useMediaQuery } from '@vueuse/core'
|
||||||
import { GlobalLayoutMode, GlobalLocales } from '@/consts/GlobalConstants'
|
import { GlobalLayoutMode, GlobalLocales, LoadSaveParamMode } from '@/consts/GlobalConstants'
|
||||||
import { changeMessages } from '@/messages'
|
import { changeMessages } from '@/messages'
|
||||||
|
import { useSystemKey } from '@/utils'
|
||||||
|
import { AUTO_LAYOUT_ENABLED, I18N_ENABLED, THEME_ENABLED } from '@/config'
|
||||||
|
|
||||||
export const useGlobalConfigStore = defineStore('globalConfig', () => {
|
export const useGlobalConfigStore = defineStore('globalConfig', () => {
|
||||||
const currentLocale = ref(GlobalLocales.CN)
|
const currentLocale = ref(GlobalLocales.CN)
|
||||||
const systemKey = import.meta.env.VITE_APP_SYSTEM_KEY
|
const systemKey = useSystemKey()
|
||||||
const isDarkTheme = useDark({
|
const isDarkTheme = THEME_ENABLED
|
||||||
storageKey: `__${systemKey}__vueuse-color-scheme`
|
? useDark({
|
||||||
})
|
storageKey: `__${systemKey}__vueuse-color-scheme`
|
||||||
|
})
|
||||||
|
: ref(false)
|
||||||
const isCollapseLeft = ref(false)
|
const isCollapseLeft = ref(false)
|
||||||
const isShowSettings = ref(false)
|
const isShowSettings = ref(false)
|
||||||
const isShowBreadcrumb = ref(true)
|
const isShowBreadcrumb = ref(true)
|
||||||
const showMenuIcon = ref(true)
|
const showMenuIcon = ref(true)
|
||||||
const layoutMode = ref(GlobalLayoutMode.TOP)
|
const isLargeScreen = useMediaQuery('(min-width: 1440px)')
|
||||||
|
const layoutMode = !isLargeScreen.value && AUTO_LAYOUT_ENABLED ? ref(GlobalLayoutMode.TOP) : ref(GlobalLayoutMode.LEFT)
|
||||||
|
const loadSaveParamMode = ref(LoadSaveParamMode.BACK)
|
||||||
return {
|
return {
|
||||||
currentLocale,
|
currentLocale,
|
||||||
isDarkTheme,
|
isDarkTheme,
|
||||||
@@ -22,8 +28,10 @@ export const useGlobalConfigStore = defineStore('globalConfig', () => {
|
|||||||
isShowSettings,
|
isShowSettings,
|
||||||
isShowBreadcrumb,
|
isShowBreadcrumb,
|
||||||
layoutMode,
|
layoutMode,
|
||||||
|
loadSaveParamMode,
|
||||||
showMenuIcon,
|
showMenuIcon,
|
||||||
changeLocale (locale) {
|
changeLocale (locale) {
|
||||||
|
if (!I18N_ENABLED) return
|
||||||
if (Object.values(GlobalLocales).includes(locale)) {
|
if (Object.values(GlobalLocales).includes(locale)) {
|
||||||
currentLocale.value = locale
|
currentLocale.value = locale
|
||||||
} else {
|
} else {
|
||||||
@@ -32,6 +40,7 @@ export const useGlobalConfigStore = defineStore('globalConfig', () => {
|
|||||||
changeMessages(locale)
|
changeMessages(locale)
|
||||||
},
|
},
|
||||||
changeTheme (dark) {
|
changeTheme (dark) {
|
||||||
|
if (!THEME_ENABLED) return
|
||||||
isDarkTheme.value = dark
|
isDarkTheme.value = dark
|
||||||
},
|
},
|
||||||
changeShowSettings (val) {
|
changeShowSettings (val) {
|
||||||
|
|||||||
86
src/stores/GlobalSearchParamStore.js
Normal file
86
src/stores/GlobalSearchParamStore.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { isObject, merge } from 'lodash-es'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { REMEMBER_SEARCH_PARAM_ENABLED, SEARCH_PARAM_TIMEOUT } from '@/config'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
|
import { LoadSaveParamMode } from '@/consts/GlobalConstants'
|
||||||
|
|
||||||
|
export const useGlobalSearchParamStore = defineStore('globalSearchParam', () => {
|
||||||
|
const globalParams = ref({})
|
||||||
|
const route = useRoute()
|
||||||
|
const rememberSearchParam = computed(() => REMEMBER_SEARCH_PARAM_ENABLED &&
|
||||||
|
useGlobalConfigStore().loadSaveParamMode !== LoadSaveParamMode.NEVER)
|
||||||
|
|
||||||
|
const getCurrentParamByPath = (defaultParam, path) => {
|
||||||
|
isSaveParamBack.value = false
|
||||||
|
if (rememberSearchParam.value && path && globalParams.value[path]) {
|
||||||
|
/**
|
||||||
|
* @type {SaveParam}
|
||||||
|
*/
|
||||||
|
const saveParam = globalParams.value[path]
|
||||||
|
if (isParamValid(saveParam)) {
|
||||||
|
return merge({}, defaultParam, saveParam.formParam)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultParam
|
||||||
|
}
|
||||||
|
const getCurrentParam = (defaultParam) => {
|
||||||
|
if (!rememberSearchParam.value || (useGlobalConfigStore().loadSaveParamMode === LoadSaveParamMode.BACK && !isSaveParamBack.value)) {
|
||||||
|
return { ...defaultParam }
|
||||||
|
}
|
||||||
|
return getCurrentParamByPath(defaultParam, route.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} SaveParam
|
||||||
|
* @property {string} formParam
|
||||||
|
* @property {string} date
|
||||||
|
* @property {number} timeout
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param param {SaveParam}
|
||||||
|
*/
|
||||||
|
const isParamValid = param => {
|
||||||
|
return param.date && param.timeout && dayjs(param.date).add(param.timeout, 'minute').isAfter(dayjs())
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSaveParamBack = ref(false)
|
||||||
|
const savedParamRouteInfo = ref()
|
||||||
|
|
||||||
|
return {
|
||||||
|
rememberSearchParam,
|
||||||
|
globalParams,
|
||||||
|
getCurrentParam,
|
||||||
|
getCurrentParamByPath,
|
||||||
|
isSaveParamBack,
|
||||||
|
savedParamRouteInfo,
|
||||||
|
setSaveParamBack: (value) => {
|
||||||
|
isSaveParamBack.value = !!value
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param value {Object}
|
||||||
|
* @param path {string|{path?:string,timeout?:number}}
|
||||||
|
*/
|
||||||
|
saveCurrentParam (value, path) {
|
||||||
|
if (!rememberSearchParam.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const config = {}
|
||||||
|
Object.assign(config, isObject(path) ? path : { path })
|
||||||
|
path = config.path || route.path
|
||||||
|
if (path) {
|
||||||
|
globalParams.value[path] = {
|
||||||
|
formParam: value,
|
||||||
|
date: new Date(),
|
||||||
|
timeout: config.timeout || SEARCH_PARAM_TIMEOUT
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Path is required')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
persist: true
|
||||||
|
})
|
||||||
@@ -1,44 +1,69 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { login } from '@/services/login/LoginService'
|
import { login } from '@/services/login/LoginService'
|
||||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
import { useMenuConfigStore } from '@/stores/MenuConfigStore'
|
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||||
|
import { SYSTEM_KEY } from '@/config'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
export const useLoginConfigStore = defineStore('loginConfig', () => {
|
export const useLoginConfigStore = defineStore('loginConfig', () => {
|
||||||
|
/**
|
||||||
|
* 登录结果
|
||||||
|
* @type {{value: {accessToken: string, account: Object, systemKey: string, expires: Date}}}
|
||||||
|
*/
|
||||||
|
const loginResult = ref(null)
|
||||||
/**
|
/**
|
||||||
* 登录成功后保存accessToken
|
* 登录成功后保存accessToken
|
||||||
* @type {Ref<string>}
|
* @type {Ref<string>}
|
||||||
*/
|
*/
|
||||||
const accessToken = ref('')
|
const accessToken = computed(() => loginResult.value?.accessToken)
|
||||||
/**
|
/**
|
||||||
* 保存登录用户信息
|
* 保存登录用户信息
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
const accountInfo = ref()
|
const accountInfo = computed(() => loginResult.value?.account)
|
||||||
|
/**
|
||||||
|
* 系统key
|
||||||
|
* @type {Ref<string>}
|
||||||
|
*/
|
||||||
|
const systemKey = computed(() => loginResult.value?.systemKey || SYSTEM_KEY)
|
||||||
|
/**
|
||||||
|
* 记住上次登录名
|
||||||
|
* @type {Ref<UnwrapRef<string>>}
|
||||||
|
*/
|
||||||
|
const lastLoginName = ref('')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
loginResult,
|
||||||
|
lastLoginName,
|
||||||
accessToken,
|
accessToken,
|
||||||
accountInfo,
|
accountInfo,
|
||||||
|
systemKey,
|
||||||
/**
|
/**
|
||||||
* @param {{account: Object, accessToken:string}} loginResult
|
* @param {{account: Object, accessToken:string}} resultData
|
||||||
*/
|
*/
|
||||||
setLoginAccountInfo (loginResult) {
|
setLoginAccountInfo (resultData) {
|
||||||
accountInfo.value = loginResult.account
|
loginResult.value = resultData
|
||||||
accessToken.value = loginResult.accessToken
|
lastLoginName.value = resultData?.account?.actualLoginName
|
||||||
},
|
},
|
||||||
clearLoginInfo () {
|
clearLoginInfo () {
|
||||||
accessToken.value = ''
|
loginResult.value = null
|
||||||
accountInfo.value = null
|
|
||||||
},
|
},
|
||||||
isLoginIn () {
|
isLoginIn () {
|
||||||
|
if (loginResult.value?.expires) {
|
||||||
|
if (dayjs(loginResult.value.expires).isBefore(dayjs())) { // Token过期
|
||||||
|
this.logout()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
return !!accessToken.value
|
return !!accessToken.value
|
||||||
},
|
},
|
||||||
logout () {
|
logout () {
|
||||||
// 清理登录数据
|
// 清理登录数据
|
||||||
this.clearLoginInfo()
|
this.clearLoginInfo()
|
||||||
// 清理TAB数据, $reset似乎不能用
|
// $reset清理数据
|
||||||
useTabsViewStore().clearAllTabs()
|
useTabsViewStore().$reset()
|
||||||
useMenuConfigStore().clearBusinessMenus()
|
useGlobalSearchParamStore().$reset()
|
||||||
},
|
},
|
||||||
async login (loginVo) {
|
async login (loginVo) {
|
||||||
const loginResult = await login(loginVo)
|
const loginResult = await login(loginVo)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { ref } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import {
|
||||||
|
checkReplaceHistoryShouldReplace,
|
||||||
|
isNestedRoute
|
||||||
|
} from '@/route/RouteUtils'
|
||||||
|
import { TAB_MODE_MAX_CACHES } from '@/config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} TabsViewStore
|
* @typedef {Object} TabsViewStore
|
||||||
@@ -14,10 +19,12 @@ import { defineStore } from 'pinia'
|
|||||||
* @return {TabsViewStore}
|
* @return {TabsViewStore}
|
||||||
*/
|
*/
|
||||||
export const useTabsViewStore = defineStore('tabsView', () => {
|
export const useTabsViewStore = defineStore('tabsView', () => {
|
||||||
const isTabMode = ref(true)
|
const isTabMode = ref(false)
|
||||||
const isCachedTabMode = ref(true)
|
const isCachedTabMode = ref(true)
|
||||||
const isShowTabIcon = ref(true)
|
const isShowTabIcon = ref(true)
|
||||||
const currentTab = ref('')
|
const currentTab = ref('')
|
||||||
|
const currentTabItem = ref(null)
|
||||||
|
const maxCacheCount = ref(TAB_MODE_MAX_CACHES)
|
||||||
/**
|
/**
|
||||||
* @type {{value: [import('vue-router').RouteRecordRaw]}}
|
* @type {{value: [import('vue-router').RouteRecordRaw]}}
|
||||||
*/
|
*/
|
||||||
@@ -48,14 +55,15 @@ export const useTabsViewStore = defineStore('tabsView', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkMataReplaceHistory = (historyTab, tab) => {
|
const isHomeTab = tab => !!tab?.meta?.homeFlag
|
||||||
// 如果meta中配置有replaceTabHistory,默认替换相关的tab
|
|
||||||
return historyTab.meta && historyTab.meta.replaceTabHistory && historyTab.meta.replaceTabHistory === tab.name
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSameReplaceHistory = (historyTab, tab) => {
|
const getNestedParentTab = tab => isNestedRoute(tab) && tab.matched?.length >= 2 ? tab.matched[tab.matched.length - 2] : tab
|
||||||
return historyTab.meta && historyTab.meta.replaceTabHistory && tab.meta && tab.meta.replaceTabHistory &&
|
|
||||||
historyTab.meta.replaceTabHistory === tab.meta.replaceTabHistory
|
const addNestedParentTab = (tab, replaceTab) => {
|
||||||
|
if (isCachedTabMode.value && !forceNotCache(tab)) {
|
||||||
|
const parentTab = getNestedParentTab(tab)
|
||||||
|
addCachedTab(parentTab, replaceTab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addHistoryTab = (tab) => {
|
const addHistoryTab = (tab) => {
|
||||||
@@ -63,16 +71,20 @@ export const useTabsViewStore = defineStore('tabsView', () => {
|
|||||||
if (isTabMode.value) {
|
if (isTabMode.value) {
|
||||||
const idx = historyTabs.value.findIndex(v => v.path === tab.path)
|
const idx = historyTabs.value.findIndex(v => v.path === tab.path)
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
const replaceIdx = historyTabs.value.findIndex(v => checkMataReplaceHistory(v, tab) ||
|
const replaceIdx = historyTabs.value.findIndex(v => checkReplaceHistoryShouldReplace(v, tab))
|
||||||
checkMataReplaceHistory(tab, v) || isSameReplaceHistory(v, tab))
|
|
||||||
let replaceTab = null
|
let replaceTab = null
|
||||||
if (replaceIdx > -1) {
|
if (replaceIdx > -1) {
|
||||||
replaceTab = historyTabs.value[replaceIdx]
|
replaceTab = historyTabs.value[replaceIdx]
|
||||||
historyTabs.value.splice(replaceIdx, 1, Object.assign({}, tab))
|
historyTabs.value.splice(replaceIdx, 1, Object.assign({}, tab))
|
||||||
} else {
|
} else {
|
||||||
historyTabs.value.push(Object.assign({}, tab)) // 可能是Proxy,需要解析出来
|
// 可能是Proxy,需要解析出来
|
||||||
|
isHomeTab(tab) ? historyTabs.value.unshift({ ...tab }) : historyTabs.value.push({ ...tab })
|
||||||
|
}
|
||||||
|
if (isNestedRoute(tab)) {
|
||||||
|
addNestedParentTab(tab, replaceTab)
|
||||||
|
} else {
|
||||||
|
addCachedTab(tab, replaceTab)
|
||||||
}
|
}
|
||||||
addCachedTab(tab, replaceTab)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +111,10 @@ export const useTabsViewStore = defineStore('tabsView', () => {
|
|||||||
|
|
||||||
const removeHistoryTabs = (tab, type) => {
|
const removeHistoryTabs = (tab, type) => {
|
||||||
if (tab) {
|
if (tab) {
|
||||||
const idx = cachedTabs.value.findIndex(v => v === tab.name)
|
const idx = historyTabs.value.findIndex(v => v.path === tab.path)
|
||||||
|
if (idx < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
let removeTabs = []
|
let removeTabs = []
|
||||||
if (type === 'right') {
|
if (type === 'right') {
|
||||||
removeTabs = historyTabs.value.splice(idx + 1)
|
removeTabs = historyTabs.value.splice(idx + 1)
|
||||||
@@ -111,17 +126,24 @@ export const useTabsViewStore = defineStore('tabsView', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const forceNotCache = tab => {
|
||||||
|
const noCacheRoute = tab.matched?.find(route => route.meta && route.meta.cache === false)
|
||||||
|
return !!noCacheRoute
|
||||||
|
}
|
||||||
|
|
||||||
const addCachedTab = (tab, replaceTab) => {
|
const addCachedTab = (tab, replaceTab) => {
|
||||||
if (isCachedTabMode.value && tab.name && !tab.name.includes('-')) {
|
if (isCachedTabMode.value && !forceNotCache(tab) && tab.name && !tab.name.includes('-')) {
|
||||||
removeCachedTab(replaceTab)
|
removeCachedTab(replaceTab)
|
||||||
if (!cachedTabs.value.includes(tab.name)) {
|
if (!cachedTabs.value.includes(tab.name)) {
|
||||||
cachedTabs.value.push(tab.name)
|
nextTick(() => { cachedTabs.value.push(tab.name) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeCachedTab = tab => {
|
const removeCachedTab = tab => {
|
||||||
if (tab) {
|
if (tab) {
|
||||||
|
tab = getNestedParentTab(tab)
|
||||||
const idx = cachedTabs.value.findIndex(v => v === tab.name)
|
const idx = cachedTabs.value.findIndex(v => v === tab.name)
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
cachedTabs.value.splice(idx, 1)
|
cachedTabs.value.splice(idx, 1)
|
||||||
@@ -142,17 +164,30 @@ export const useTabsViewStore = defineStore('tabsView', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(currentTab, path => {
|
||||||
|
currentTabItem.value = historyTabs.value.find(v => path && v.path === path)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isTabMode,
|
isTabMode,
|
||||||
isCachedTabMode,
|
isCachedTabMode,
|
||||||
isShowTabIcon,
|
isShowTabIcon,
|
||||||
|
maxCacheCount,
|
||||||
currentTab,
|
currentTab,
|
||||||
|
currentTabItem,
|
||||||
historyTabs,
|
historyTabs,
|
||||||
cachedTabs,
|
cachedTabs,
|
||||||
|
$customReset (initState) {
|
||||||
|
Object.assign(initState, { // 保留部分配置
|
||||||
|
isTabMode: isTabMode.value,
|
||||||
|
isCachedTabMode: isCachedTabMode.value,
|
||||||
|
isShowTabIcon: isShowTabIcon.value
|
||||||
|
})
|
||||||
|
},
|
||||||
changeTabMode (val) {
|
changeTabMode (val) {
|
||||||
isTabMode.value = val
|
isTabMode.value = val
|
||||||
if (!isTabMode.value) {
|
if (!isTabMode.value) {
|
||||||
clearHistoryTabs()
|
clearAllTabs()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changeCachedTabMode (val) {
|
changeCachedTabMode (val) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||||
|
import { useSystemKey } from '@/utils'
|
||||||
|
import { cloneDeep, isFunction } from 'lodash-es'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组合式api的$reset需要自己实现
|
* 组合式api的$reset需要自己实现
|
||||||
@@ -7,9 +9,17 @@ import { createPersistedState } from 'pinia-plugin-persistedstate'
|
|||||||
* @param store
|
* @param store
|
||||||
*/
|
*/
|
||||||
const piniaPluginResetStore = ({ store }) => {
|
const piniaPluginResetStore = ({ store }) => {
|
||||||
const initialState = JSON.parse(JSON.stringify(store.$state)) // deep clone(store.$state)
|
const initialState = cloneDeep(store.$state) // deep clone(store.$state)
|
||||||
store.$reset = () => {
|
store.$reset = () => {
|
||||||
store.$state = JSON.parse(JSON.stringify(initialState))
|
const initState = cloneDeep(initialState)
|
||||||
|
if (isFunction(store.$customReset)) {
|
||||||
|
const newState = store.$customReset(initState)
|
||||||
|
if (newState) {
|
||||||
|
store.$patch(state => Object.assign(state, newState))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store.$patch(state => Object.assign(state, initState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +29,7 @@ export default {
|
|||||||
pinia.use(piniaPluginResetStore)
|
pinia.use(piniaPluginResetStore)
|
||||||
pinia.use(createPersistedState({
|
pinia.use(createPersistedState({
|
||||||
key: key => {
|
key: key => {
|
||||||
const systemKey = import.meta.env.VITE_APP_SYSTEM_KEY
|
const systemKey = useSystemKey()
|
||||||
return `__${systemKey}__${key}`
|
return `__${systemKey}__${key}`
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
305
src/utils/index.js
Normal file
305
src/utils/index.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { isObject, isArray, set, isNumber } from 'lodash-es'
|
||||||
|
import { ElLoading, ElMessageBox, ElMessage } from 'element-plus'
|
||||||
|
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||||
|
import numeral from 'numeral'
|
||||||
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
import { LOADING_DELAY, SYSTEM_KEY } from '@/config'
|
||||||
|
import { $i18nBundle } from '@/messages'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
||||||
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
|
|
||||||
|
export const useSystemKey = () => {
|
||||||
|
return SYSTEM_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDate = (date, format) => {
|
||||||
|
if (date) {
|
||||||
|
return dayjs(date).format(format || 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDay = (date, format) => {
|
||||||
|
if (date) {
|
||||||
|
return dayjs(date).format(format || 'YYYY-MM-DD')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 数组转换成 key:value的对象,可以为a.b.c格式
|
||||||
|
* @param item
|
||||||
|
* @param parentKey
|
||||||
|
*/
|
||||||
|
export const toFlatKeyValue = (item, parentKey = '') => {
|
||||||
|
const result = {}
|
||||||
|
for (const key in item) {
|
||||||
|
if (key === '@class') { // 过滤掉一些不用的属性
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const newKey = parentKey ? `${parentKey}.${key}` : key
|
||||||
|
if (isObject(item[key]) && !isArray(item[key])) {
|
||||||
|
Object.assign(result, toFlatKeyValue(item[key], newKey))
|
||||||
|
} else {
|
||||||
|
result[newKey] = item[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 转换a.b.c形式为对象格式
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
export const toKeyValueObj = (item) => {
|
||||||
|
const result = {}
|
||||||
|
for (const objKey in item) {
|
||||||
|
set(result, objKey, item[objKey])
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 转换成get参数
|
||||||
|
* @param obj
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export const toGetParams = (obj) => {
|
||||||
|
if (isObject(obj)) {
|
||||||
|
obj = toFlatKeyValue(obj)
|
||||||
|
return Object.keys(obj)
|
||||||
|
.map(key => `${key}=${obj[key]}`).join('&')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取GET参数为对象
|
||||||
|
* @param queryStr
|
||||||
|
* @return {{[p: string]: string}}
|
||||||
|
*/
|
||||||
|
export const fromGetParams = (queryStr) => {
|
||||||
|
return Object.fromEntries(new URLSearchParams(queryStr).entries())
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 给指定URL增加GET参数
|
||||||
|
* @param url {String} url地址
|
||||||
|
* @param params {Object} 参数
|
||||||
|
* @return {string} 返回新URL
|
||||||
|
*/
|
||||||
|
export const addParamsToURL = (url, params = {}) => {
|
||||||
|
const queryIndex = url.indexOf('?')
|
||||||
|
const hasParams = queryIndex > -1
|
||||||
|
const getParams = hasParams ? fromGetParams(url.substring(queryIndex + 1)) : {}
|
||||||
|
const baseUrl = hasParams ? url.substring(0, queryIndex) : url
|
||||||
|
return `${baseUrl}?${toGetParams({ ...getParams, ...params })}`
|
||||||
|
}
|
||||||
|
const router = null
|
||||||
|
/**
|
||||||
|
* @param {string|RouteLocationRaw|number} path 路径、路由对象、数字
|
||||||
|
* @param replace 是否用replace方法
|
||||||
|
* @return {*|Promise<T>}
|
||||||
|
*/
|
||||||
|
export const $goto = (path, replace = false) => {
|
||||||
|
path = path || -1
|
||||||
|
if (isNumber(path)) {
|
||||||
|
return Promise.resolve().then(() => router?.go(path))
|
||||||
|
} else {
|
||||||
|
if (replace) {
|
||||||
|
return router?.replace(path)
|
||||||
|
} else {
|
||||||
|
return router?.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 定制窗口打开模式
|
||||||
|
const oriOpen = window.open
|
||||||
|
/**
|
||||||
|
* 通用打开窗口逻辑,统一处理不同模式下窗口打开
|
||||||
|
* @param path 路径:/xxx/xxx1
|
||||||
|
* @param target 目标窗口,仅窗口有效,_blank默认, _self
|
||||||
|
* @param forceNewWin 是否强制新窗口打开
|
||||||
|
*/
|
||||||
|
export const $openWin = (path, target = '_blank', forceNewWin = false) => {
|
||||||
|
const tabsViewStore = useTabsViewStore()
|
||||||
|
if (path) { // path有值才跳转
|
||||||
|
if (path.match(/^\w+?:\/\/.+/)) { // http等协议不拦截跳转逻辑
|
||||||
|
oriOpen(path, target || '_blank')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const hasHash = path.startsWith('#')
|
||||||
|
if (tabsViewStore.isTabMode && !forceNewWin) {
|
||||||
|
path = hasHash ? path.substring(1) : path
|
||||||
|
$goto(path)
|
||||||
|
} else {
|
||||||
|
path = hasHash ? path : `#${path}`
|
||||||
|
oriOpen(path, target || '_blank')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.open = $openWin
|
||||||
|
|
||||||
|
export const $openNewWin = path => $openWin(path, '_blank', true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RouteLocationNormalizedLoaded} route 路由,不可为空
|
||||||
|
*/
|
||||||
|
export const $reload = (route) => {
|
||||||
|
const currentRoute = route
|
||||||
|
if (currentRoute) {
|
||||||
|
const time = new Date().getTime()
|
||||||
|
const query = Object.assign({}, currentRoute.query, { _t: time })
|
||||||
|
$goto(`${currentRoute.path}?${toGetParams(query)}`, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useReload = () => {
|
||||||
|
const route = useRoute()
|
||||||
|
return {
|
||||||
|
reload: () => {
|
||||||
|
$reload(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $logout = () => {
|
||||||
|
useLoginConfigStore().logout()
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
$goto('/login')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const globalLoadingConfig = {
|
||||||
|
delay: LOADING_DELAY,
|
||||||
|
globalLoading: null,
|
||||||
|
delayLoadingId: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $coreShowLoading = (message) => {
|
||||||
|
const globalLoading = globalLoadingConfig.globalLoading
|
||||||
|
if (globalLoading) {
|
||||||
|
globalLoading.close()
|
||||||
|
}
|
||||||
|
const openLoading = () => ElLoading.service(Object.assign({
|
||||||
|
lock: true,
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
text: message
|
||||||
|
}))
|
||||||
|
if (globalLoadingConfig.delay > 0) {
|
||||||
|
globalLoadingConfig.delayLoadingId = setTimeout(() => {
|
||||||
|
globalLoadingConfig.globalLoading = openLoading()
|
||||||
|
}, globalLoadingConfig.delay)
|
||||||
|
} else {
|
||||||
|
globalLoadingConfig.globalLoading = openLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $coreHideLoading = () => {
|
||||||
|
globalLoadingConfig.delayLoadingId && clearTimeout(globalLoadingConfig.delayLoadingId)
|
||||||
|
globalLoadingConfig.delayLoadingId = null
|
||||||
|
globalLoadingConfig.globalLoading?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $coreAlert = (message, title = $i18nBundle('common.label.reminder'), options = undefined) => {
|
||||||
|
if (isObject(title) && !options) {
|
||||||
|
options = title
|
||||||
|
title = null
|
||||||
|
}
|
||||||
|
options = Object.assign({
|
||||||
|
type: 'info',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
draggable: true,
|
||||||
|
customClass: 'common-message-alert'
|
||||||
|
}, options || {})
|
||||||
|
return ElMessageBox.alert(message,
|
||||||
|
title || $i18nBundle('common.label.reminder'),
|
||||||
|
options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $coreError = (message, title = $i18nBundle('common.label.reminder'), options = undefined) => {
|
||||||
|
return $coreAlert(message, title, Object.assign({
|
||||||
|
type: 'error'
|
||||||
|
}, options || {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $coreWarning = (message, title = $i18nBundle('common.label.reminder'), options = undefined) => {
|
||||||
|
return $coreAlert(message, title, Object.assign({
|
||||||
|
type: 'warning'
|
||||||
|
}, options || {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $coreSuccess = (message, title = $i18nBundle('common.label.reminder'), options = undefined) => {
|
||||||
|
return $coreAlert(message, title, Object.assign({
|
||||||
|
type: 'success'
|
||||||
|
}, options || {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $coreConfirm = (message, title = $i18nBundle('common.label.reminder'), options = undefined) => {
|
||||||
|
if (isObject(title) && !options) {
|
||||||
|
options = title
|
||||||
|
title = null
|
||||||
|
}
|
||||||
|
options = Object.assign({
|
||||||
|
icon: markRaw(QuestionFilled),
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
draggable: true,
|
||||||
|
customClass: 'common-message-confirm'
|
||||||
|
}, options || {})
|
||||||
|
return ElMessageBox.confirm(message,
|
||||||
|
title || $i18nBundle('common.label.reminder'),
|
||||||
|
options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $formatNumber = (value, format) => {
|
||||||
|
return numeral(value).format(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $number = (value, size) => {
|
||||||
|
const digits = []
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
digits.push('0')
|
||||||
|
}
|
||||||
|
return $formatNumber(value, '0,0.' + digits.join(''))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $currency = (value, prefix) => {
|
||||||
|
return `${prefix || '¥'} ${$formatNumber(value, '0,0.00')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $currencyShort = (value, prefix) => {
|
||||||
|
return `${prefix || '¥'} ${$formatNumber(value, '0,0.[00]')}`
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @typedef {{text:string, success?:string, error?:string}} CopyTextConfig
|
||||||
|
* @type {CopyTextConfig}
|
||||||
|
*/
|
||||||
|
const defaultCopyConfig = {
|
||||||
|
success: 'Copied Successfully!',
|
||||||
|
error: 'Copy Not supported!'
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param text {string | CopyTextConfig} 需要复制的文本
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
export const $copyText = (text) => {
|
||||||
|
if (text) {
|
||||||
|
let config
|
||||||
|
if (isObject(text)) {
|
||||||
|
config = { ...defaultCopyConfig, ...text }
|
||||||
|
} else {
|
||||||
|
config = { ...defaultCopyConfig, text }
|
||||||
|
}
|
||||||
|
if (config.text) {
|
||||||
|
const { copy, isSupported } = useClipboard({ legacy: true })
|
||||||
|
if (isSupported) {
|
||||||
|
copy(config.text)
|
||||||
|
ElMessage({
|
||||||
|
message: config.success,
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ElMessage({
|
||||||
|
message: config.error,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/vendors/axios.js
vendored
95
src/vendors/axios.js
vendored
@@ -4,62 +4,107 @@ import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
|||||||
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
||||||
import { $i18nBundle } from '@/messages'
|
import { $i18nBundle } from '@/messages'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { debounce } from 'lodash'
|
import { debounce, isString } from 'lodash-es'
|
||||||
|
import { $coreHideLoading, $coreShowLoading, $goto } from '@/utils'
|
||||||
|
|
||||||
|
import { GLOBAL_ERROR_MESSAGE, GLOBAL_LOADING } from '@/config'
|
||||||
|
|
||||||
export const $http = axios.create({
|
export const $http = axios.create({
|
||||||
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
|
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
|
||||||
timeout: import.meta.env.VITE_APP_API_TIMEOUT
|
timeout: import.meta.env.VITE_APP_API_TIMEOUT
|
||||||
})
|
})
|
||||||
|
|
||||||
$http.interceptors.request.use(config => {
|
const hasLoading = config => {
|
||||||
|
return config?.loading ?? GLOBAL_LOADING
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param config {ServiceRequestConfig}
|
||||||
|
* @return {*|boolean}
|
||||||
|
*/
|
||||||
|
const showErrorMessage = config => {
|
||||||
|
return config?.showErrorMessage ?? GLOBAL_ERROR_MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
$http.interceptors.request.use(/** @param config {ServiceRequestConfig} */ config => {
|
||||||
const globalConfigStore = useGlobalConfigStore()
|
const globalConfigStore = useGlobalConfigStore()
|
||||||
const loginConfigStore = useLoginConfigStore()
|
const loginConfigStore = useLoginConfigStore()
|
||||||
config.headers.locale = globalConfigStore.currentLocale
|
config.headers.locale = globalConfigStore.currentLocale
|
||||||
if (config.addToken !== false && loginConfigStore.accessToken) { // 添加token
|
if (config.addToken !== false && loginConfigStore.accessToken) { // 添加token
|
||||||
config.headers.Authorization = `Bearer ${loginConfigStore.accessToken}`
|
config.headers.Authorization = `Bearer ${loginConfigStore.accessToken}`
|
||||||
}
|
}
|
||||||
|
if (config.addToken !== false && !loginConfigStore.accessToken && !config.isLogin) {
|
||||||
|
return false // 处理登出是调用接口出现异常问题
|
||||||
|
}
|
||||||
|
if (hasLoading(config)) {
|
||||||
|
$coreShowLoading(isString(config.loading) ? config.loading : undefined)
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
})
|
})
|
||||||
|
|
||||||
const networkErrorFun = debounce(() => ElMessage.error($i18nBundle('common.msg.networkError')), 300)
|
const networkErrorFun = debounce(() => ElMessage.error($i18nBundle('common.msg.networkError')), 300)
|
||||||
const networkTimeoutFun = debounce(() => ElMessage.error($i18nBundle('common.msg.networkTimeout')), 300)
|
const networkTimeoutFun = debounce(() => ElMessage.error($i18nBundle('common.msg.networkTimeout')), 300)
|
||||||
|
|
||||||
$http.interceptors.response.use(data => {
|
$http.interceptors.response.use(response => {
|
||||||
// todo 其他处理
|
if (hasLoading(response.config)) {
|
||||||
return data
|
$coreHideLoading()
|
||||||
|
}
|
||||||
|
if (response && response.data && !response.data.success && response.data.message) {
|
||||||
|
if (response.config && showErrorMessage(response.config)) {
|
||||||
|
ElMessage.error(response.data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
}, error => {
|
}, error => {
|
||||||
console.info(error.code, error.message)
|
if (hasLoading(error?.config)) {
|
||||||
|
$coreHideLoading()
|
||||||
|
}
|
||||||
|
console.info('=========================axios', error)
|
||||||
if (error.message === 'Network Error') {
|
if (error.message === 'Network Error') {
|
||||||
networkErrorFun()
|
networkErrorFun()
|
||||||
} else if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') > -1) {
|
} else if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') > -1) {
|
||||||
networkTimeoutFun()
|
networkTimeoutFun()
|
||||||
}
|
}
|
||||||
if (error.response.status === 401 && !error.response.config.isLogin) {
|
if (error.response?.status === 401 && !error.response?.config.isLogin) {
|
||||||
// 跳转登录页面
|
// 跳转登录页面
|
||||||
|
$goto('/login')
|
||||||
}
|
}
|
||||||
return error.response
|
return error.response
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {AxiosRequestConfig} ServiceRequestConfig
|
||||||
|
* @property {number} [timeout] 超时时间
|
||||||
|
* @property {boolean|string} [loading] 是否显示loading,默认不显示
|
||||||
|
* @property {boolean} [addToken] 是否添加token
|
||||||
|
* @property {boolean} [showErrorMessage] 是否显示自动错误信息
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @param url URL地址
|
||||||
|
* @param {object} [data] 数据对象
|
||||||
|
* @param [config] {ServiceRequestConfig} 配置对象
|
||||||
|
* @return {Promise<unknown>}
|
||||||
|
*/
|
||||||
export const $httpPost = (url, data, config) => {
|
export const $httpPost = (url, data, config) => {
|
||||||
return new Promise((resolve, reject) => {
|
return $http.post(url, data, config).then(response => {
|
||||||
$http.post(url, data, config).then(response => {
|
if (response?.data) {
|
||||||
if (response.data) {
|
return response.data // 只要有数据就认为成功,内容再解析
|
||||||
resolve(response.data) // 只要有数据就认为成功,内容再解析
|
} else {
|
||||||
} else {
|
throw new Error('No response data')
|
||||||
reject(new Error('No response data'))
|
}
|
||||||
}
|
|
||||||
}, reject)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
export const $httpGet = (url, data, config) => {
|
* @param url URL地址
|
||||||
return new Promise((resolve, reject) => {
|
* @param [config] {ServiceRequestConfig} 配置对象
|
||||||
$http.get(url, config).then(response => {
|
* @return {Promise<unknown>}
|
||||||
if (response.data) {
|
*/
|
||||||
resolve(response.data) // 只要有数据就认为成功,内容再解析
|
export const $httpGet = (url, config) => {
|
||||||
} else {
|
return $http.get(url, config).then(response => {
|
||||||
reject(new Error('No response data'))
|
if (response?.data) {
|
||||||
}
|
return response.data // 只要有数据就认为成功,内容再解析
|
||||||
}, reject)
|
} else {
|
||||||
|
throw new Error('No response data')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/vendors/dayjs.js
vendored
Normal file
11
src/vendors/dayjs.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import duration from 'dayjs/plugin/duration'
|
||||||
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
|
||||||
|
dayjs.extend(utc)
|
||||||
|
dayjs.extend(duration)
|
||||||
|
dayjs.extend(timezone)
|
||||||
|
dayjs.tz.setDefault('Asia/Shanghai')
|
||||||
|
|
||||||
|
export default dayjs
|
||||||
34
src/vendors/echarts.js
vendored
Normal file
34
src/vendors/echarts.js
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { use, Axis } from 'echarts/core'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import {
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
GridComponent
|
||||||
|
} from 'echarts/components'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
|
|
||||||
|
export const useEchartsConfig = () => {
|
||||||
|
const globalConfigStore = useGlobalConfigStore()
|
||||||
|
use([
|
||||||
|
Axis,
|
||||||
|
CanvasRenderer,
|
||||||
|
BarChart,
|
||||||
|
PieChart,
|
||||||
|
LineChart,
|
||||||
|
GridComponent,
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent
|
||||||
|
])
|
||||||
|
const theme = computed(() => {
|
||||||
|
return globalConfigStore.isDarkTheme ? 'dark' : 'light'
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
theme,
|
||||||
|
VChart
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/vendors/element-plus.js
vendored
Normal file
41
src/vendors/element-plus.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import ElementPlus, { ElTag, ElSelect, ElSelectV2, ElTable } from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
// 黑色模式
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改默认值
|
||||||
|
*/
|
||||||
|
const setDefaultProps = () => {
|
||||||
|
ElTag.props.disableTransitions = {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
ElSelect.props.defaultFirstOption = {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
ElSelect.props.emptyValues = {
|
||||||
|
type: Array,
|
||||||
|
default: () => [null, undefined]
|
||||||
|
}
|
||||||
|
ElSelectV2.props.defaultFirstOption = {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
ElSelect.props.emptyValues = {
|
||||||
|
type: Array,
|
||||||
|
default: () => [null, undefined]
|
||||||
|
}
|
||||||
|
ElTable.props.scrollbarAlwaysOn = {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install (app) {
|
||||||
|
app.use(ElementPlus)
|
||||||
|
setDefaultProps()
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/vendors/monaco-editor.js
vendored
Normal file
166
src/vendors/monaco-editor.js
vendored
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import VueMonacoEditor, { loader } from '@guolao/vue-monaco-editor'
|
||||||
|
import { ref, watch, toRaw, h, withDirectives, resolveDirective } from 'vue'
|
||||||
|
const MonacoLoader = () => import('monaco-editor')
|
||||||
|
|
||||||
|
const WorkerImporters = {
|
||||||
|
JsonWorker: () => import('monaco-editor/esm/vs/language/json/json.worker?worker'),
|
||||||
|
CssWorker: () => import('monaco-editor/esm/vs/language/css/css.worker?worker'),
|
||||||
|
HtmlWorker: () => import('monaco-editor/esm/vs/language/html/html.worker?worker'),
|
||||||
|
JsWorker: () => import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
|
||||||
|
EditorWorker: () => import('monaco-editor/esm/vs/editor/editor.worker?worker')
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 默认配置
|
||||||
|
* @type {IStandaloneEditorConstructionOptions}
|
||||||
|
*/
|
||||||
|
const defaultConfig = {
|
||||||
|
automaticLayout: true,
|
||||||
|
autoIndent: 'full',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
theme: 'vs-dark',
|
||||||
|
wordWrap: 'on',
|
||||||
|
readOnly: true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param [config] {IStandaloneEditorConstructionOptions} 配置信息
|
||||||
|
* @return {IStandaloneEditorConstructionOptions}
|
||||||
|
*/
|
||||||
|
export const defineMonacoOptions = (config) => {
|
||||||
|
return {
|
||||||
|
...defaultConfig,
|
||||||
|
...config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMonacoWorker = async (key) => {
|
||||||
|
const workerModule = await WorkerImporters[key]?.()
|
||||||
|
return workerModule?.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* vs路径问题:https://www.npmjs.com/package/@guolao/vue-monaco-editor
|
||||||
|
*/
|
||||||
|
self.MonacoEnvironment = {
|
||||||
|
getWorker: async function (workerId, label) {
|
||||||
|
const JsonWorker = await getMonacoWorker('JsonWorker')
|
||||||
|
const CssWorker = await getMonacoWorker('CssWorker')
|
||||||
|
const HtmlWorker = await getMonacoWorker('HtmlWorker')
|
||||||
|
const JsWorker = await getMonacoWorker('JsWorker')
|
||||||
|
const EditorWorker = await getMonacoWorker('EditorWorker')
|
||||||
|
switch (label) {
|
||||||
|
case 'json':
|
||||||
|
return new JsonWorker()
|
||||||
|
case 'css':
|
||||||
|
case 'scss':
|
||||||
|
case 'less':
|
||||||
|
return new CssWorker()
|
||||||
|
case 'html':
|
||||||
|
case 'handlebars':
|
||||||
|
case 'razor':
|
||||||
|
return new HtmlWorker()
|
||||||
|
case 'typescript':
|
||||||
|
case 'javascript':
|
||||||
|
return new JsWorker()
|
||||||
|
default:
|
||||||
|
return new EditorWorker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const langCheckConfig = {
|
||||||
|
json: /(\{[\s\S]*})|(\[[\s\S]*])/,
|
||||||
|
html: /(<[\s\S]*>)/,
|
||||||
|
sql: /(SELECT\s.*?\bFROM\b)|(INSERT\s.*?\bINTO\b)|(UPDATE\s.*?\bSET\b)|(DELETE\s.*?\bFROM\b)/i
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $checkLang = value => {
|
||||||
|
const val = value?.trim() || ''
|
||||||
|
if (val) {
|
||||||
|
for (const langKey in langCheckConfig) {
|
||||||
|
if (langCheckConfig[langKey].test(val)) {
|
||||||
|
return langKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const $formatDocument = (editor, readOnly, delay = 200) => {
|
||||||
|
setTimeout(function () { // 延迟执行格式化
|
||||||
|
if (readOnly) {
|
||||||
|
editor.updateOptions({ // 只读格式化操作无效,需要先去掉只读状态
|
||||||
|
readOnly: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
editor.getAction('editor.action.formatDocument').run().then(function () {
|
||||||
|
if (readOnly) {
|
||||||
|
editor.updateOptions({
|
||||||
|
readOnly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const processPasteCode = data => {
|
||||||
|
data = data?.replace(/(\\r|\\n|\\t)+/ig, '').replace(/(?!(\\\\\\))[\\]+/ig, '').replace(/^\s+/, '').replace(/\s+$/, '')
|
||||||
|
if (data?.match(/^<.*/)) {
|
||||||
|
data = data.replace(/</ig, '<').replace(/>/ig, '>')
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMonacoEditorOptions = (config) => {
|
||||||
|
const contentRef = ref('')
|
||||||
|
const languageRef = ref('')
|
||||||
|
const editorRef = ref()
|
||||||
|
const monacoEditorOptions = defineMonacoOptions(config)
|
||||||
|
const formatDocument = () => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
self.MonacoEnvironment.getWorker(languageRef.value).then(() => {
|
||||||
|
$formatDocument(editorRef.value, monacoEditorOptions.readOnly)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
watch([contentRef, editorRef], () => {
|
||||||
|
languageRef.value = $checkLang(contentRef.value)
|
||||||
|
if (contentRef.value && editorRef.value && monacoEditorOptions.readOnly) {
|
||||||
|
formatDocument()
|
||||||
|
}
|
||||||
|
if (editorRef.value && !editorRef.value.__internalPasteFunc__) {
|
||||||
|
const editor = toRaw(editorRef.value)
|
||||||
|
editor.__internalPasteFunc__ = () => {
|
||||||
|
const value = editor.getValue()
|
||||||
|
contentRef.value = processPasteCode(value)
|
||||||
|
editor.setValue(contentRef.value)
|
||||||
|
formatDocument()
|
||||||
|
}
|
||||||
|
editorRef.value.onDidPaste(editorRef.value.__internalPasteFunc__)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
contentRef,
|
||||||
|
languageRef,
|
||||||
|
editorRef,
|
||||||
|
monacoEditorOptions,
|
||||||
|
formatDocument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLoadingDiv = (attrs = {}) => {
|
||||||
|
const loadingDirective = [[resolveDirective('loading'), true]]
|
||||||
|
return withDirectives(h('div', { style: 'height:100%', ...attrs }), loadingDirective)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install (app) {
|
||||||
|
app.component(VueMonacoEditor.name, {
|
||||||
|
setup (props) {
|
||||||
|
if (loader.__getMonacoInstance() === null) {
|
||||||
|
MonacoLoader().then(monaco => loader.config({ monaco }))
|
||||||
|
}
|
||||||
|
return () => h(VueMonacoEditor, props, () => [getLoadingDiv()])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,24 +3,29 @@ import LeftMenu from '@/layout/LeftMenu.vue'
|
|||||||
import TopNav from '@/layout/TopNav.vue'
|
import TopNav from '@/layout/TopNav.vue'
|
||||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
import { useMenuConfigStore } from '@/stores/MenuConfigStore'
|
|
||||||
import { GlobalLayoutMode } from '@/consts/GlobalConstants'
|
import { GlobalLayoutMode } from '@/consts/GlobalConstants'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import GlobalSettings from '@/views/components/global/GlobalSettings.vue'
|
import GlobalSettings from '@/views/components/global/GlobalSettings.vue'
|
||||||
|
import { useMenuConfigStore } from '@/stores/MenuConfigStore'
|
||||||
|
import { useBreadcrumbConfigStore } from '@/stores/BreadcrumbConfigStore'
|
||||||
|
import { APP_VERSION } from '@/config'
|
||||||
|
import { useTabModeScrollSaver, getParentRootKey } from '@/route/RouteUtils'
|
||||||
|
|
||||||
const globalConfigStore = useGlobalConfigStore()
|
const globalConfigStore = useGlobalConfigStore()
|
||||||
const menuConfigStore = useMenuConfigStore()
|
|
||||||
const tabsViewStore = useTabsViewStore()
|
const tabsViewStore = useTabsViewStore()
|
||||||
|
const breadcrumbConfigStore = useBreadcrumbConfigStore()
|
||||||
const showLeftMenu = computed(() => {
|
const showLeftMenu = computed(() => {
|
||||||
return globalConfigStore.layoutMode === GlobalLayoutMode.LEFT
|
return globalConfigStore.layoutMode === GlobalLayoutMode.LEFT
|
||||||
})
|
})
|
||||||
menuConfigStore.loadBusinessMenus()
|
useTabModeScrollSaver()
|
||||||
|
useMenuConfigStore().loadBusinessMenus()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-container class="index-container">
|
<el-container class="index-container">
|
||||||
<el-aside
|
<el-aside
|
||||||
v-if="showLeftMenu"
|
v-if="showLeftMenu"
|
||||||
class="index-aside"
|
class="index-aside menu"
|
||||||
width="auto"
|
width="auto"
|
||||||
>
|
>
|
||||||
<left-menu />
|
<left-menu />
|
||||||
@@ -33,39 +38,63 @@ menuConfigStore.loadBusinessMenus()
|
|||||||
v-if="globalConfigStore.layoutMode === GlobalLayoutMode.TOP && globalConfigStore.isShowBreadcrumb"
|
v-if="globalConfigStore.layoutMode === GlobalLayoutMode.TOP && globalConfigStore.isShowBreadcrumb"
|
||||||
class="tabs-header"
|
class="tabs-header"
|
||||||
>
|
>
|
||||||
<common-breadcrumb />
|
<common-breadcrumb
|
||||||
|
:show-icon="tabsViewStore.isShowTabIcon"
|
||||||
|
:label-config="breadcrumbConfigStore.breadcrumbConfig"
|
||||||
|
/>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-header
|
<el-header
|
||||||
v-if="tabsViewStore.isTabMode"
|
v-if="tabsViewStore.isTabMode"
|
||||||
class="tabs-header"
|
class="tabs-header tabMode"
|
||||||
>
|
>
|
||||||
<common-tabs-view />
|
<common-tabs-view />
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main class="home-main">
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<transition
|
<transition
|
||||||
name="slide-fade"
|
:name="route.meta?.transition!==false?'slide-fade':''"
|
||||||
mode="out-in"
|
mode="out-in"
|
||||||
>
|
>
|
||||||
<KeepAlive
|
<KeepAlive
|
||||||
|
v-if="tabsViewStore.isTabMode&&tabsViewStore.isCachedTabMode"
|
||||||
:include="tabsViewStore.cachedTabs"
|
:include="tabsViewStore.cachedTabs"
|
||||||
:max="10"
|
:max="tabsViewStore.maxCacheCount"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="Component"
|
:is="Component"
|
||||||
:key="route.fullPath"
|
:key="getParentRootKey(route)"
|
||||||
/>
|
/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
|
<component
|
||||||
|
:is="Component"
|
||||||
|
v-else
|
||||||
|
:key="route.fullPath"
|
||||||
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
<el-container class="text-center padding-10 flex-center">
|
||||||
|
<span>
|
||||||
|
<el-text>Copyright © 2024 Version: {{ APP_VERSION }}</el-text>
|
||||||
|
<el-link
|
||||||
|
href="https://github.com/fugary/simple-element-plus-template"
|
||||||
|
type="primary"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
https://github.com/fugary/simple-element-plus-template
|
||||||
|
</el-link>
|
||||||
|
</span>
|
||||||
|
</el-container>
|
||||||
|
<el-backtop
|
||||||
|
v-common-tooltip="$t('common.label.backtop')"
|
||||||
|
target=".home-main"
|
||||||
|
:right="50"
|
||||||
|
:bottom="50"
|
||||||
|
/>
|
||||||
</el-main>
|
</el-main>
|
||||||
<global-settings />
|
<global-settings />
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tabs-header {
|
|
||||||
margin-top: 5px;
|
|
||||||
height: 40px
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const searchFormOptions = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
const doSearch = form => {
|
const doSearch = form => {
|
||||||
console.info('=================searchParam', searchParam.value)
|
console.info('=================searchParam', searchParam.value, form)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const searchFormOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
const doSearch = form => {
|
const doSearch = form => {
|
||||||
console.info('=================searchParam', form, searchParam.value)
|
console.info('=================searchParam', form, searchParam.value)
|
||||||
|
loadUsers()
|
||||||
}
|
}
|
||||||
/** *************用户编辑**************/
|
/** *************用户编辑**************/
|
||||||
const currentUser = ref(null)
|
const currentUser = ref(null)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
import { GlobalLayoutMode, GlobalLocales } from '@/consts/GlobalConstants'
|
import { GlobalLayoutMode, GlobalLocales, LoadSaveParamMode } from '@/consts/GlobalConstants'
|
||||||
|
import { I18N_ENABLED, REMEMBER_SEARCH_PARAM_ENABLED, THEME_ENABLED } from '@/config'
|
||||||
const globalConfigStore = useGlobalConfigStore()
|
const globalConfigStore = useGlobalConfigStore()
|
||||||
const tabsViewStore = useTabsViewStore()
|
const tabsViewStore = useTabsViewStore()
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +13,7 @@ const options = [
|
|||||||
labelKey: 'common.label.theme',
|
labelKey: 'common.label.theme',
|
||||||
prop: 'isDarkTheme',
|
prop: 'isDarkTheme',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
model: globalConfigStore,
|
enabled: THEME_ENABLED,
|
||||||
attrs: {
|
attrs: {
|
||||||
activeActionIcon: 'icon-moon',
|
activeActionIcon: 'icon-moon',
|
||||||
inactiveActionIcon: 'icon-sunny'
|
inactiveActionIcon: 'icon-sunny'
|
||||||
@@ -22,7 +23,7 @@ const options = [
|
|||||||
labelKey: 'common.label.language',
|
labelKey: 'common.label.language',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
prop: 'currentLocale',
|
prop: 'currentLocale',
|
||||||
model: globalConfigStore,
|
enabled: I18N_ENABLED,
|
||||||
change (val) {
|
change (val) {
|
||||||
globalConfigStore.changeLocale(val)
|
globalConfigStore.changeLocale(val)
|
||||||
},
|
},
|
||||||
@@ -36,9 +37,7 @@ const options = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
labelKey: 'common.label.layout',
|
labelKey: 'common.label.layout',
|
||||||
type: 'select',
|
slot: 'layout',
|
||||||
prop: 'layoutMode',
|
|
||||||
model: globalConfigStore,
|
|
||||||
change (val) {
|
change (val) {
|
||||||
globalConfigStore.changeLayout(val)
|
globalConfigStore.changeLayout(val)
|
||||||
},
|
},
|
||||||
@@ -53,14 +52,12 @@ const options = [
|
|||||||
{
|
{
|
||||||
labelKey: 'common.label.showMenuIcon',
|
labelKey: 'common.label.showMenuIcon',
|
||||||
prop: 'showMenuIcon',
|
prop: 'showMenuIcon',
|
||||||
type: 'switch',
|
type: 'switch'
|
||||||
model: globalConfigStore
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
labelKey: 'common.label.breadcrumb',
|
labelKey: 'common.label.breadcrumb',
|
||||||
prop: 'isShowBreadcrumb',
|
prop: 'isShowBreadcrumb',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
model: globalConfigStore,
|
|
||||||
change (val) {
|
change (val) {
|
||||||
globalConfigStore.isShowBreadcrumb = val
|
globalConfigStore.isShowBreadcrumb = val
|
||||||
}
|
}
|
||||||
@@ -88,6 +85,25 @@ const options = [
|
|||||||
prop: 'isShowTabIcon',
|
prop: 'isShowTabIcon',
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
model: tabsViewStore
|
model: tabsViewStore
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: 'common.label.saveParamMode',
|
||||||
|
prop: 'loadSaveParamMode',
|
||||||
|
type: 'select',
|
||||||
|
enabled: REMEMBER_SEARCH_PARAM_ENABLED,
|
||||||
|
attrs: {
|
||||||
|
clearable: false
|
||||||
|
},
|
||||||
|
children: [{
|
||||||
|
labelKey: 'common.label.allSaveParamMode',
|
||||||
|
value: LoadSaveParamMode.ALL
|
||||||
|
}, {
|
||||||
|
labelKey: 'common.label.backSaveParamMode',
|
||||||
|
value: LoadSaveParamMode.BACK
|
||||||
|
}, {
|
||||||
|
labelKey: 'common.label.neverSaveParamMode',
|
||||||
|
value: LoadSaveParamMode.NEVER
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
@@ -106,7 +122,39 @@ const options = [
|
|||||||
:show-buttons="false"
|
:show-buttons="false"
|
||||||
:options="options"
|
:options="options"
|
||||||
label-position="left"
|
label-position="left"
|
||||||
/>
|
:model="globalConfigStore"
|
||||||
|
>
|
||||||
|
<template #layout="{option}">
|
||||||
|
<common-form-control
|
||||||
|
:model="globalConfigStore"
|
||||||
|
:option="option"
|
||||||
|
>
|
||||||
|
<el-radio-group
|
||||||
|
v-model="globalConfigStore.layoutMode"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<el-radio-button
|
||||||
|
v-for="item in option.children"
|
||||||
|
:key="item.value"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
<common-icon
|
||||||
|
v-if="item.value==='left'"
|
||||||
|
v-common-tooltip="$t(item.labelKey)"
|
||||||
|
icon="VerticalSplitFilled"
|
||||||
|
:size="16"
|
||||||
|
/>
|
||||||
|
<common-icon
|
||||||
|
v-if="item.value==='top'"
|
||||||
|
v-common-tooltip="$t(item.labelKey)"
|
||||||
|
icon="HorizontalSplitFilled"
|
||||||
|
:size="16"
|
||||||
|
/>
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</common-form-control>
|
||||||
|
</template>
|
||||||
|
</common-form>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div style="flex: auto">
|
<div style="flex: auto">
|
||||||
|
|||||||
60
src/views/tools/Charts.vue
Normal file
60
src/views/tools/Charts.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useEchartsConfig } from '@/vendors/echarts'
|
||||||
|
const { VChart, theme } = useEchartsConfig()
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
title: {
|
||||||
|
text: 'Referer of a Website',
|
||||||
|
subtext: 'Fake Data',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'left'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Access From',
|
||||||
|
type: 'pie',
|
||||||
|
radius: '50%',
|
||||||
|
data: [
|
||||||
|
{ value: 1048, name: 'Search Engine' },
|
||||||
|
{ value: 735, name: 'Direct' },
|
||||||
|
{ value: 580, name: 'Email' },
|
||||||
|
{ value: 484, name: 'Union Ads' },
|
||||||
|
{ value: 300, name: 'Video Ads' }
|
||||||
|
],
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-container class="container-center">
|
||||||
|
<v-chart
|
||||||
|
v-if="chartConfig"
|
||||||
|
class="chart"
|
||||||
|
:theme="theme"
|
||||||
|
:option="chartConfig"
|
||||||
|
autoresize
|
||||||
|
/>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 400px;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
src/views/tools/Editors.vue
Normal file
40
src/views/tools/Editors.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useMonacoEditorOptions } from '@/vendors/monaco-editor'
|
||||||
|
import { $copyText } from '@/utils'
|
||||||
|
const { contentRef, languageRef, editorRef, monacoEditorOptions, formatDocument } = useMonacoEditorOptions({
|
||||||
|
readOnly: false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-container class="flex-column">
|
||||||
|
<vue-monaco-editor
|
||||||
|
v-model:value="contentRef"
|
||||||
|
:language="languageRef"
|
||||||
|
height="400px"
|
||||||
|
:options="monacoEditorOptions"
|
||||||
|
@mount="editorRef=$event"
|
||||||
|
/>
|
||||||
|
<el-footer
|
||||||
|
v-if="contentRef"
|
||||||
|
class="container-center"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
@click="$copyText(contentRef)"
|
||||||
|
>
|
||||||
|
{{ $t('common.label.copy') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
@click="formatDocument()"
|
||||||
|
>
|
||||||
|
{{ $t('common.label.format') }}
|
||||||
|
</el-button>
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -63,6 +63,7 @@ const submitForm = ({ form }) => {
|
|||||||
<common-window
|
<common-window
|
||||||
v-model="showWindow"
|
v-model="showWindow"
|
||||||
:ok-click="submitForm"
|
:ok-click="submitForm"
|
||||||
|
show-fullscreen
|
||||||
>
|
>
|
||||||
<el-container class="flex-column container-center">
|
<el-container class="flex-column container-center">
|
||||||
<common-form
|
<common-form
|
||||||
|
|||||||
@@ -1,20 +1,76 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { viteMockServe } from 'vite-plugin-mock'
|
import { viteMockServe } from 'vite-plugin-mock'
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
import eslint from 'vite-plugin-eslint'
|
||||||
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
import packageJson from './package.json'
|
||||||
|
|
||||||
|
const optionalPlugins = [{
|
||||||
|
plugin: visualizer({ open: true }),
|
||||||
|
enabled: false
|
||||||
|
}, {
|
||||||
|
plugin: viteMockServe({
|
||||||
|
mockPath: './mock'
|
||||||
|
}),
|
||||||
|
enabled: true
|
||||||
|
}, {
|
||||||
|
plugin: splitVendorChunkPlugin(),
|
||||||
|
enabled: true
|
||||||
|
}].filter(p => p.enabled).map(p => p.plugin)
|
||||||
|
|
||||||
|
const JS_FILE_NAMES = 'js/[name]-[hash].js'
|
||||||
|
const CSS_FILE_NAMES = 'css/[name]-[hash].css'
|
||||||
|
const IMG_EXT_LIST = ['.png', '.jpg', '.gif', '.svg', '.bmp', '.webp']
|
||||||
|
const IMG_FILE_NAMES = 'images/[name]-[hash].[ext]'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default ({ mode }) => {
|
||||||
plugins: [
|
const env = loadEnv(mode, process.cwd())
|
||||||
vue(),
|
return defineConfig({
|
||||||
viteMockServe({
|
base: env.VITE_APP_CONTEXT_PATH,
|
||||||
mockPath: './mock'
|
define: {
|
||||||
})
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version)
|
||||||
],
|
},
|
||||||
resolve: {
|
plugins: [vue(), vueJsx(), eslint(), ...optionalPlugins],
|
||||||
alias: {
|
esbuild: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
drop: mode === 'production' ? ['console', 'debugger'] : []
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
chunkFileNames: JS_FILE_NAMES, // 引入文件名的名称
|
||||||
|
entryFileNames: JS_FILE_NAMES, // 包的入口文件名称
|
||||||
|
assetFileNames (assetInfo) {
|
||||||
|
if (assetInfo.name?.endsWith('.css')) { // CSS文件
|
||||||
|
return CSS_FILE_NAMES
|
||||||
|
} else if (IMG_EXT_LIST.some((ext) => assetInfo.name?.endsWith(ext))) { // 图片
|
||||||
|
return IMG_FILE_NAMES
|
||||||
|
}
|
||||||
|
return 'assets/[name]-[hash].[ext]' // 其他资源
|
||||||
|
},
|
||||||
|
manualChunks (id) {
|
||||||
|
if (id.includes('element-plus')) {
|
||||||
|
return 'elp'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
assetFileNames: JS_FILE_NAMES
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user