mirror of
https://github.com/fugary/simple-element-plus-template.git
synced 2025-11-12 14:27:49 +00:00
1. 控件优化
2. 增加monaco-editor、echarts等
This commit is contained in:
@@ -70,6 +70,22 @@ const allMenus = [
|
||||
{
|
||||
id: 24,
|
||||
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',
|
||||
nameCn: '其他示例',
|
||||
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",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"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": {
|
||||
"@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",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"axios": "^1.6.8",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mockjs": "^1.1.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"async-validator": "^4.2.5",
|
||||
"axios": "^1.7.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"echarts": "^5.5.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-plugin-persistedstate": "^3.2.1",
|
||||
"ua-parser-js": "^1.0.38",
|
||||
"vite-plugin-mock": "^3.0.1",
|
||||
"vue": "^3.4.21",
|
||||
"vue-i18n": "^9.10.2",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue": "^3.4.29",
|
||||
"vue-echarts": "^6.7.3",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.3",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.18.0",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@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",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.24.0",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.7"
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-eslint": "^1.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script setup>
|
||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||
import { $changeLocale, elementLocale, $i18nBundle } from '@/messages'
|
||||
import { $changeLocale, elementLocale } from '@/messages'
|
||||
import { useTitle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { calcRouteTitle, useRoutePopStateEvent } from '@/route/RouteUtils'
|
||||
|
||||
const globalConfigStore = useGlobalConfigStore()
|
||||
$changeLocale(globalConfigStore.currentLocale)
|
||||
const title = computed(() => $i18nBundle('common.label.title'))
|
||||
const route = useRoute()
|
||||
const title = computed(() => calcRouteTitle(route))
|
||||
useTitle(title)
|
||||
useRoutePopStateEvent()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -145,6 +145,17 @@ html, body, #app, .index-container {
|
||||
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-border-radius: var(--el-border-radius-large);
|
||||
}
|
||||
@@ -153,6 +164,24 @@ html, body, #app, .index-container {
|
||||
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 {
|
||||
z-index: 10;
|
||||
width: 6px;
|
||||
@@ -184,6 +213,10 @@ html, body, #app, .index-container {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.home-main {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.container-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,4 +1,38 @@
|
||||
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 出事路由
|
||||
* @returns {{name: string}|boolean}
|
||||
*/
|
||||
export const checkRouteAuthority = (to, from) => {
|
||||
export const checkRouteAuthority = async (to) => {
|
||||
startRouteLoading(to)
|
||||
const loginConfigStore = useLoginConfigStore()
|
||||
if (to.meta?.beforeLogin) { // 登录前的路由添加meta信息:beforeLogin: true
|
||||
return true
|
||||
}
|
||||
if (loginConfigStore.isLoginIn()) {
|
||||
// check权限
|
||||
return true
|
||||
}
|
||||
if (to.meta?.beforeLogin) { // 登录前的路由添加meta信息:beforeLogin: true
|
||||
return true
|
||||
}
|
||||
endRouteLoading(to)
|
||||
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>
|
||||
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 { UPDATE_MODEL_EVENT, CHANGE_EVENT, useFormItem } from 'element-plus'
|
||||
|
||||
@@ -76,6 +76,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
inputAsValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inputAttrs: {
|
||||
type: Object,
|
||||
default: null
|
||||
@@ -127,6 +131,7 @@ const selectPageData = ref({})
|
||||
const selectPageTab = ref(null)
|
||||
const popoverVisible = ref(false)
|
||||
const autocompletePopover = ref()
|
||||
const inputRef = ref()
|
||||
const defaultAutoPage = {
|
||||
pageSize: props.autocompleteConfig?.pageSize || 8,
|
||||
pageNumber: 1
|
||||
@@ -192,6 +197,8 @@ const onInputKeywords = debounce((input) => {
|
||||
}
|
||||
if (!val && input) {
|
||||
onSelectData()
|
||||
} else if (input && props.inputAsValue && props.useIdModel) {
|
||||
vModel.value = val
|
||||
}
|
||||
}
|
||||
}, props.debounceTime)
|
||||
@@ -211,6 +218,16 @@ onMounted(() => {
|
||||
popoverVisible.value = false
|
||||
})
|
||||
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) => {
|
||||
@@ -229,6 +246,9 @@ watch(() => props.modelValue, (value) => {
|
||||
if (isEmpty(value)) {
|
||||
vModel.value = null
|
||||
}
|
||||
} else if (!value) {
|
||||
setAutocompleteLabel('')
|
||||
vModel.value = value
|
||||
}
|
||||
})
|
||||
|
||||
@@ -293,18 +313,9 @@ const moveSelection = function (down) {
|
||||
currentOnIndex.value = -1
|
||||
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处理=================//
|
||||
const selectPagePageConfig = ref({})
|
||||
const parsedSelectPageData = computed(() => {
|
||||
@@ -376,6 +387,7 @@ watch(() => props.autocompleteConfig, (autocompleteConfig) => {
|
||||
>
|
||||
<template #reference>
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model="keywords"
|
||||
:clearable="clearable"
|
||||
:placeholder="placeholder||title"
|
||||
|
||||
@@ -90,6 +90,8 @@ export interface CommonAutocompleteProps {
|
||||
minHeight?: string;
|
||||
// input自定义属性
|
||||
inputAttrs?: InputProps;
|
||||
// 输入当做值的特殊模式
|
||||
inputAsValue?: boolean;
|
||||
// 验证事件
|
||||
validateEvent?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||
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 breadcrumbs = computed(() => {
|
||||
const exists = []
|
||||
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')
|
||||
})
|
||||
return calcMatchedRoutes(route, props.labelConfig)
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -40,12 +26,12 @@ const breadcrumbs = computed(() => {
|
||||
class="common-breadcrumb"
|
||||
>
|
||||
<el-breadcrumb-item
|
||||
v-for="item in breadcrumbs"
|
||||
v-for="(item, index) in breadcrumbs"
|
||||
:key="item.path"
|
||||
:to="{ path: item.path }"
|
||||
:to="index!==breadcrumbs.length-1?{ path: item.path }:undefined"
|
||||
>
|
||||
<common-icon
|
||||
v-if="tabsViewStore.isShowTabIcon&&item.icon"
|
||||
v-if="showIcon&&item.icon"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
{{ item.menuName }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<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 cloneDeep from 'lodash/cloneDeep'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
@@ -52,6 +51,13 @@ const isUnlimited = computed(() => {
|
||||
})
|
||||
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
|
||||
})
|
||||
@@ -64,8 +70,16 @@ const setUnlimited = () => {
|
||||
value = []
|
||||
} else if (filterType === 'common-tab-filter') {
|
||||
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)
|
||||
if (isFunction(option.change)) {
|
||||
option.change(value)
|
||||
}
|
||||
}
|
||||
|
||||
const initFilterModel = () => {
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
component: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-form-label">
|
||||
<slot />
|
||||
<component
|
||||
:is="component"
|
||||
v-if="component"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,7 @@ const props = defineProps({
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
const vModel = useVModel(props, 'modelValue', emit)
|
||||
|
||||
const childTypeMapping = { // 自动映射子元素类型,配置的时候可以不写type
|
||||
@@ -72,7 +72,7 @@ const inputType = computed(() => useInputType({ type: props.type }))
|
||||
:is="inputType"
|
||||
v-if="vModel"
|
||||
v-model="vModel[tab.prop]"
|
||||
@change="vModel._tabFilter=true"
|
||||
@change="vModel.isTabFilter=true;$emit('change', vModel)"
|
||||
>
|
||||
<control-child
|
||||
v-for="(childItem, childIdx) in tab.children"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||
import { isFunction } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* @type {{option:CommonFormOption}}
|
||||
@@ -24,6 +25,15 @@ const label = computed(() => {
|
||||
}
|
||||
return option.label
|
||||
})
|
||||
|
||||
const tooltipFunc = ($event) => {
|
||||
$event.preventDefault()
|
||||
$event.stopImmediatePropagation()
|
||||
if (isFunction(props.option.tooltipFunc)) {
|
||||
props.option.tooltipFunc($event)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,9 +41,30 @@ const label = computed(() => {
|
||||
:is="inputType"
|
||||
:value="option.value"
|
||||
:label="label"
|
||||
:disabled="option.disabled"
|
||||
:readonly="option.readonly"
|
||||
v-bind="option.attrs"
|
||||
>
|
||||
{{ 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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { computed, isVNode, ref, watch } from 'vue'
|
||||
import { $i18nBundle } from '@/messages'
|
||||
import ControlChild from '@/components/common-form-control/control-child.vue'
|
||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { get, isFunction, set } from 'lodash'
|
||||
import { cloneDeep, get, isFunction, set, isArray, isString } from 'lodash-es'
|
||||
|
||||
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(() => {
|
||||
let option = props.option
|
||||
let option = { ...props.option }
|
||||
if (isFunction(option.dynamicOption)) {
|
||||
option = { ...option, ...option.dynamicOption(props.model, option, props.addInfo) }
|
||||
} else if (isFunction(option.dynamicAttrs)) {
|
||||
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
|
||||
})
|
||||
|
||||
@@ -47,14 +65,14 @@ const inputType = computed(() => useInputType(calcOption.value))
|
||||
const modelAttrs = computed(() => {
|
||||
const option = calcOption.value
|
||||
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)) {
|
||||
attrs.clearable = true
|
||||
if (['el-input', 'el-select', 'el-select-v2', 'common-autocomplete', 'el-autocomplete', 'el-cascader', 'el-tree-select'].includes(inputType.value)) {
|
||||
attrs.clearable = attrs.clearable ?? true
|
||||
}
|
||||
if (inputType.value === 'common-autocomplete' && option.getAutocompleteLabel) {
|
||||
attrs.defaultLabel = option.getAutocompleteLabel(props.model, option)
|
||||
}
|
||||
if (inputType.value === 'el-date-picker') {
|
||||
attrs.disabledDate = (date) => {
|
||||
attrs.disabledDate = attrs.disabledDate || ((date) => {
|
||||
const option = calcOption.value
|
||||
let result = false
|
||||
if (option.minDate) {
|
||||
@@ -64,25 +82,22 @@ const modelAttrs = computed(() => {
|
||||
result = date.getTime() > dayjs(option.maxDate).startOf('d').toDate().getTime()
|
||||
}
|
||||
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
|
||||
})
|
||||
|
||||
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 date = modelValue.value
|
||||
if (type === 'el-date-picker' && date && !option.disabled && option.clearInvalidDate !== false) {
|
||||
let invalid = false
|
||||
if (minDate) {
|
||||
invalid = dayjs(date).isBefore(dayjs(option.minDate).startOf('d'))
|
||||
}
|
||||
if (invalid && maxDate) {
|
||||
invalid = dayjs(date).isAfter(dayjs(option.maxDate).startOf('d'))
|
||||
if (isFunction(modelAttrs.value.disabledDate)) {
|
||||
invalid = modelAttrs.value.disabledDate(dayjs(date).toDate())
|
||||
}
|
||||
if (invalid) {
|
||||
modelValue.value = undefined
|
||||
@@ -126,7 +141,8 @@ const childTypeMapping = { // 自动映射子元素类型,配置的时候可
|
||||
|
||||
const children = computed(() => {
|
||||
const option = calcOption.value
|
||||
const result = option.children || [] // 初始化一些默认值
|
||||
let result = option.children || [] // 初始化一些默认值
|
||||
result = result.filter(childItem => childItem.enabled !== false)
|
||||
result.forEach(childItem => {
|
||||
if (!childItem.type) {
|
||||
childItem.type = childTypeMapping[option.type]
|
||||
@@ -141,7 +157,7 @@ const rules = computed(() => {
|
||||
const option = calcOption.value
|
||||
let _rules = cloneDeep(option.rules || [])
|
||||
if (option.prop) {
|
||||
if (option.required !== undefined) {
|
||||
if (option.required) {
|
||||
const label = option.label || toLabelByKey(option.labelKey)
|
||||
_rules = [{
|
||||
trigger: option.trigger,
|
||||
@@ -149,7 +165,7 @@ const rules = computed(() => {
|
||||
message: $i18nBundle('common.msg.nonNull', [label])
|
||||
}, ..._rules]
|
||||
}
|
||||
if (option.pattern !== undefined) {
|
||||
if (option.pattern) {
|
||||
const label = option.label || toLabelByKey(option.labelKey)
|
||||
_rules = [{
|
||||
pattern: option.pattern,
|
||||
@@ -175,9 +191,7 @@ initFormModel()
|
||||
|
||||
watch(() => calcOption.value, initFormModel, { deep: true })
|
||||
|
||||
const hasModelText = computed(() => {
|
||||
return modelAttrs.value.modelText || calcOption.value.formatter
|
||||
})
|
||||
const hasModelText = computed(() => isFunction(calcOption.value.formatter))
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
@@ -199,18 +213,10 @@ const controlLabelWidth = computed(() => {
|
||||
|
||||
const formatResult = computed(() => {
|
||||
if (hasModelText.value) {
|
||||
if (modelAttrs.value.modelText) {
|
||||
return {
|
||||
modelText: modelAttrs.value.modelText
|
||||
}
|
||||
}
|
||||
const option = calcOption.value
|
||||
if (option.formatter) {
|
||||
const result = option.formatter(modelValue.value, calcOption.value)
|
||||
return {
|
||||
modelText: result,
|
||||
vnode: isVNode(result)
|
||||
}
|
||||
const result = calcOption.value.formatter(modelValue.value, calcOption.value)
|
||||
return {
|
||||
modelText: result,
|
||||
vnode: isVNode(result)
|
||||
}
|
||||
}
|
||||
return null
|
||||
@@ -233,7 +239,16 @@ const formatResult = computed(() => {
|
||||
#label
|
||||
>
|
||||
<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" />
|
||||
<el-tooltip
|
||||
v-if="calcOption.tooltip||calcOption.tooltipFunc"
|
||||
@@ -255,40 +270,42 @@ const formatResult = computed(() => {
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<component
|
||||
:is="inputType"
|
||||
v-model="modelValue"
|
||||
v-bind="modelAttrs"
|
||||
:placeholder="calcOption.placeholder"
|
||||
:disabled="calcOption.disabled"
|
||||
:readonly="calcOption.readonly"
|
||||
@change="controlChange"
|
||||
>
|
||||
<template
|
||||
v-if="hasModelText&&formatResult"
|
||||
#default
|
||||
<slot>
|
||||
<component
|
||||
:is="inputType"
|
||||
v-model="modelValue"
|
||||
v-bind="modelAttrs"
|
||||
:placeholder="calcOption.placeholder"
|
||||
:disabled="calcOption.disabled"
|
||||
:readonly="calcOption.readonly"
|
||||
@change="controlChange"
|
||||
>
|
||||
<span
|
||||
v-if="formatResult.modelText&&!formatResult.vnode"
|
||||
class="common-form-label-text"
|
||||
v-html="formatResult.modelText"
|
||||
/>
|
||||
<component
|
||||
:is="formatResult.modelText"
|
||||
v-if="formatResult.vnode"
|
||||
class="common-form-label-text"
|
||||
/>
|
||||
</template>
|
||||
<slot name="childBefore" />
|
||||
<template v-if="children&&children.length">
|
||||
<control-child
|
||||
v-for="(childItem, index) in children"
|
||||
:key="index"
|
||||
:option="childItem"
|
||||
/>
|
||||
</template>
|
||||
<slot name="childAfter" />
|
||||
</component>
|
||||
<template
|
||||
v-if="hasModelText&&formatResult"
|
||||
#default
|
||||
>
|
||||
<span
|
||||
v-if="formatResult.modelText&&!formatResult.vnode"
|
||||
class="common-form-label-text"
|
||||
v-html="formatResult.modelText"
|
||||
/>
|
||||
<component
|
||||
:is="formatResult.modelText"
|
||||
v-if="formatResult.vnode"
|
||||
class="common-form-label-text"
|
||||
/>
|
||||
</template>
|
||||
<slot name="childBefore" />
|
||||
<template v-if="children&&children.length">
|
||||
<control-child
|
||||
v-for="(childItem, index) in children"
|
||||
:key="index"
|
||||
:option="childItem"
|
||||
/>
|
||||
</template>
|
||||
<slot name="childAfter" />
|
||||
</component>
|
||||
</slot>
|
||||
<slot name="after" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import { inject, ref, onMounted, isRef, watchEffect } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { inject, ref, onMounted, isRef, watchEffect, onUnmounted } from 'vue'
|
||||
import { useVModel, onKeyStroke } from '@vueuse/core'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isFunction } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* @type {CommonFormProps}
|
||||
@@ -42,6 +44,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disableButtons: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showSubmit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
@@ -71,11 +77,25 @@ const props = defineProps({
|
||||
default: ''
|
||||
},
|
||||
backUrl: {
|
||||
type: String,
|
||||
type: [String, Function],
|
||||
default: ''
|
||||
},
|
||||
submitByEnter: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
scrollToError: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const emit = defineEmits(['submitForm', 'update:model'])
|
||||
|
||||
const formModel = useVModel(props, 'model', emit)
|
||||
@@ -86,10 +106,31 @@ const form = ref()
|
||||
defineExpose({
|
||||
form
|
||||
})
|
||||
const formDiv = ref()
|
||||
const removeEnterFn = ref()
|
||||
const commonWindowRef = inject('commonWindow', null)
|
||||
onMounted(() => {
|
||||
const commonWindowRef = inject('commonWindow', null)
|
||||
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 })
|
||||
})
|
||||
|
||||
const goBack = (...args) => {
|
||||
if (isFunction(props.backUrl)) {
|
||||
return props.backUrl(...args)
|
||||
} else if (props.backUrl) {
|
||||
router.push(props.backUrl)
|
||||
} else {
|
||||
router.go(-1)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-form
|
||||
ref="form"
|
||||
:inline="inline"
|
||||
:class="className"
|
||||
:model="formModel"
|
||||
:label-width="labelWidth"
|
||||
:validate-on-rule-change="validateOnRuleChange"
|
||||
<div
|
||||
ref="formDiv"
|
||||
class="common-form-div"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<template
|
||||
v-for="(option,index) in options"
|
||||
:key="index"
|
||||
>
|
||||
<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"
|
||||
<el-form
|
||||
ref="form"
|
||||
:inline="inline"
|
||||
:class="className"
|
||||
:model="formModel"
|
||||
name="default"
|
||||
/>
|
||||
<el-form-item
|
||||
v-if="showButtons"
|
||||
:style="buttonStyle"
|
||||
:label-width="labelWidth"
|
||||
:scroll-to-error="scrollToError"
|
||||
:validate-on-rule-change="validateOnRuleChange"
|
||||
v-bind="{...$attrs, 'class':undefined}"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-button
|
||||
v-if="showSubmit"
|
||||
:disabled="disableSubmit"
|
||||
type="primary"
|
||||
@click="$emit('submitForm', form)"
|
||||
<template
|
||||
v-for="(option,index) in options"
|
||||
:key="index"
|
||||
>
|
||||
{{ submitLabel||$t('common.label.submit') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showReset"
|
||||
@click="form.resetFields()"
|
||||
>
|
||||
{{ resetLabel||$t('common.label.reset') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showBack||backUrl"
|
||||
@click="backUrl?$router.push(backUrl):$router.go(-1)"
|
||||
>
|
||||
{{ backLabel||$t('common.label.back') }}
|
||||
</el-button>
|
||||
<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
|
||||
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
|
||||
:form="form"
|
||||
:model="formModel"
|
||||
name="buttons"
|
||||
name="default"
|
||||
/>
|
||||
</el-form-item>
|
||||
<slot
|
||||
:form="form"
|
||||
:model="formModel"
|
||||
name="after-buttons"
|
||||
/>
|
||||
</el-form>
|
||||
<el-form-item
|
||||
v-if="showButtons"
|
||||
:style="buttonStyle"
|
||||
class="buttonsDiv"
|
||||
>
|
||||
<el-button
|
||||
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>
|
||||
|
||||
<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;
|
||||
/** 占位提示符 */
|
||||
placeholder?: string;
|
||||
/** 日期最小值**/
|
||||
minDate: string|Date;
|
||||
/** 日期最大值**/
|
||||
maxDate: string|Date;
|
||||
/** 是否清理不可用日期值**/
|
||||
clearInvalidDate: boolean;
|
||||
/** 有些控件柚子节点 */
|
||||
children?: Array<CommonFormOption>;
|
||||
/** async-validator验证器 */
|
||||
@@ -133,6 +139,12 @@ export interface CommonFormOption extends FormControlTypeOption {
|
||||
tooltip?: string;
|
||||
/** 提示函数 */
|
||||
tooltipFunc?: () => void;
|
||||
/** 自动trim,默认false**/
|
||||
trim?: boolean,
|
||||
/** 自动upperCase,默认false**/
|
||||
upperCase?: boolean,
|
||||
/** 自动lowerCase,默认false**/
|
||||
lowerCase?: boolean,
|
||||
/**
|
||||
* common-form-label格式化
|
||||
* @param modelValue 数据
|
||||
@@ -141,6 +153,8 @@ export interface CommonFormOption extends FormControlTypeOption {
|
||||
formatter?: (modelValue:any, option: CommonFormOption) => string|VNode;
|
||||
/** 自定义slot名称 */
|
||||
slot?: string;
|
||||
/** 自定义label slot名称 */
|
||||
labelSlot?: string;
|
||||
/**
|
||||
* 根据model数据动态计算Option值
|
||||
* @param model 表单model
|
||||
@@ -184,4 +198,6 @@ export interface CommonFormProps extends FormProps {
|
||||
backUrl: string;
|
||||
/** 行级排列 */
|
||||
inline: boolean;
|
||||
/** 回车提交 */
|
||||
submitByEnter: boolean;
|
||||
}
|
||||
|
||||
@@ -102,13 +102,14 @@ const selectIcon = icon => {
|
||||
>
|
||||
{{ $t('common.label.clear') }}
|
||||
</el-button>
|
||||
<el-dialog
|
||||
<common-window
|
||||
v-model="iconSelectVisible"
|
||||
:width="dialogWidth"
|
||||
v-bind="dialogAttrs"
|
||||
draggable
|
||||
class="icon-dialog"
|
||||
:title="$t('common.msg.pleaseSelectIcon')"
|
||||
:show-buttons="false"
|
||||
>
|
||||
<el-container
|
||||
style="overflow: auto;"
|
||||
@@ -155,9 +156,15 @@ const selectIcon = icon => {
|
||||
</el-col>
|
||||
</el-row>
|
||||
</recycle-scroller>
|
||||
<el-backtop
|
||||
v-common-tooltip="$t('common.label.backtop')"
|
||||
target=".scroller"
|
||||
:right="10"
|
||||
:bottom="10"
|
||||
/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-dialog>
|
||||
</common-window>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -179,6 +186,10 @@ const selectIcon = icon => {
|
||||
}
|
||||
.icon-area {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
.icon-area .el-backtop {
|
||||
position: absolute;
|
||||
}
|
||||
.icon-a {
|
||||
height:80px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ICON_PREFIX } from '@/icons'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import { kebabCase } from 'lodash-es'
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
|
||||
@@ -18,7 +18,7 @@ const props = defineProps({
|
||||
required: true
|
||||
},
|
||||
index: {
|
||||
type: [Number, String],
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
@@ -87,7 +87,7 @@ const showMenuIcon = computed(() => {
|
||||
<common-menu-item
|
||||
v-for="(childMenu, childIdx) in menuItem.children"
|
||||
:key="childMenu.index||childIdx"
|
||||
:index="childIdx"
|
||||
:index="`${menuItem.index||index}_${childIdx}`"
|
||||
:menu-item="childMenu"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
@@ -128,6 +128,7 @@ const showMenuIcon = computed(() => {
|
||||
</el-menu-item>
|
||||
<el-menu-item
|
||||
v-else
|
||||
v-open-new-window="menuItem.index"
|
||||
:class="menuCls"
|
||||
:disabled="menuItem.disabled"
|
||||
:route="menuItem.route"
|
||||
|
||||
@@ -22,7 +22,7 @@ const activeRoutePath = computed(() => {
|
||||
return props.defaultActivePath
|
||||
}
|
||||
const current = useParentRoute(route)
|
||||
return current && current.path !== '/' ? current.path : ''
|
||||
return current && current.path !== '/' ? current.path : '--'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -39,7 +39,7 @@ const activeRoutePath = computed(() => {
|
||||
>
|
||||
<common-menu-item
|
||||
:menu-item="menuItem"
|
||||
:index="index"
|
||||
:index="`${index}`"
|
||||
>
|
||||
<template #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
|
||||
label?: string;
|
||||
showIcon?: boolean; // 控制某些排序不显示图标
|
||||
fixedValue?: 'ASC' | 'DESC' // 固定排序模式
|
||||
fixedValue?: 'ASC' | 'DESC'; // 固定排序模式
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import TableFormControl from '@/components/common-table-form/table-form-control.vue'
|
||||
import { toLabelByKey } from '@/components/utils'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -19,6 +18,10 @@ const props = defineProps({
|
||||
showOperation: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
operationWidth: {
|
||||
type: String,
|
||||
default: '110px'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -55,13 +58,11 @@ const options = computed(() => {
|
||||
<template>
|
||||
<el-table
|
||||
:data="dataList"
|
||||
border
|
||||
class="common-table-form"
|
||||
>
|
||||
<el-table-column
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
:label="option.label||toLabelByKey(option.labelKey)"
|
||||
:key="`${option.prop}__${index}`"
|
||||
:width="option.width"
|
||||
>
|
||||
<template
|
||||
@@ -73,6 +74,16 @@ const options = computed(() => {
|
||||
:name="option.headerSlot"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
#header
|
||||
>
|
||||
<el-form-item
|
||||
:label="option.label||toLabelByKey(option.labelKey)"
|
||||
class="common-table-form-label"
|
||||
:required="option.required"
|
||||
/>
|
||||
</template>
|
||||
<!--用于自定义显示属性-->
|
||||
<template
|
||||
v-if="option.slot"
|
||||
@@ -88,7 +99,7 @@ const options = computed(() => {
|
||||
v-else
|
||||
#default="{row, $index}"
|
||||
>
|
||||
<table-form-control
|
||||
<common-form-control
|
||||
:model="row"
|
||||
label-width="0"
|
||||
:option="option"
|
||||
@@ -99,11 +110,13 @@ const options = computed(() => {
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="showOperation"
|
||||
:width="operationWidth"
|
||||
:label="$t('common.label.operation')"
|
||||
>
|
||||
<template #default="{row, $index}">
|
||||
<div class="el-form-item">
|
||||
<el-button
|
||||
circle
|
||||
type="danger"
|
||||
size="small"
|
||||
:underline="false"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup>
|
||||
import { formatDate, toLabelByKey } from '@/components/utils'
|
||||
import { formatDate } from '@/utils'
|
||||
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)"
|
||||
:prop="column.prop||column.property"
|
||||
:width="column.width"
|
||||
:min-width="column.minWidth"
|
||||
v-bind="column.attrs"
|
||||
:formatter="formatter"
|
||||
>
|
||||
@@ -86,20 +89,16 @@ const slots = useSlots()
|
||||
<template
|
||||
#default="scope"
|
||||
>
|
||||
<template v-for="(button, index) in column.buttons">
|
||||
<el-button
|
||||
v-if="(!button.buttonIf||button.buttonIf(scope.row, scope))&&button.enabled!==false"
|
||||
:key="index"
|
||||
:type="button.type"
|
||||
:icon="button.icon"
|
||||
:size="button.size||buttonSize"
|
||||
:disabled="button.disabled"
|
||||
:round="button.round"
|
||||
:circle="button.circle"
|
||||
@click="button.click&&button.click(scope.row, scope)"
|
||||
>
|
||||
{{ button.label || toLabelByKey(button.labelKey) }}
|
||||
</el-button>
|
||||
<template
|
||||
v-for="(button, index) in column.buttons"
|
||||
:key="index"
|
||||
>
|
||||
<table-dynamic-button
|
||||
:button-config="button"
|
||||
:item="scope.row"
|
||||
:button-size="buttonSize"
|
||||
:scope="{...scope,item:scope.row}"
|
||||
/>
|
||||
</template>
|
||||
<slot
|
||||
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>
|
||||
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 { getFrontendPage } from '@/components/utils'
|
||||
|
||||
@@ -38,7 +38,7 @@ const props = defineProps({
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* el-button
|
||||
@@ -83,6 +83,10 @@ const props = defineProps({
|
||||
}
|
||||
}
|
||||
},
|
||||
showPageSizes: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -158,7 +162,7 @@ const isInfiniteEnd = ref(false)
|
||||
|
||||
function checkInfiniteEnd (pageVal) {
|
||||
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(() => {
|
||||
if (props.infinitePaging) {
|
||||
console.info('================================mounted', infiniteRef.value)
|
||||
useIntersectionObserver(infiniteRef, onInfiniteLoad, {
|
||||
threshold: 1
|
||||
const { stop } = useIntersectionObserver(infiniteRef, onInfiniteLoad, {
|
||||
threshold: 0.5
|
||||
})
|
||||
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()
|
||||
|
||||
defineExpose({
|
||||
@@ -240,7 +262,7 @@ defineExpose({
|
||||
:stripe="stripe"
|
||||
:data="calcData"
|
||||
:class="{'common-hide-expand': hideExpandBtn}"
|
||||
:border="border"
|
||||
:border="calcBorder"
|
||||
>
|
||||
<common-table-column
|
||||
v-for="(column, index) in calcColumns"
|
||||
@@ -293,7 +315,7 @@ defineExpose({
|
||||
<el-pagination
|
||||
v-if="!infinitePaging&&!frontendPaging&&page&&page.pageCount"
|
||||
class="common-pagination"
|
||||
v-bind="pageAttrs"
|
||||
v-bind="calcPageAttrs"
|
||||
:total="page.totalCount"
|
||||
:page-size="page.pageSize"
|
||||
:current-page="page.pageNumber"
|
||||
@@ -303,7 +325,7 @@ defineExpose({
|
||||
<el-pagination
|
||||
v-if="!infinitePaging&&frontendPaging&&frontendPage&&frontendPage.pageCount"
|
||||
class="common-pagination"
|
||||
v-bind="pageAttrs"
|
||||
v-bind="calcPageAttrs"
|
||||
:total="frontendPage.totalCount"
|
||||
:page-size="frontendPage.pageSize"
|
||||
: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 表格数据
|
||||
*/
|
||||
buttonIf: (data: any) => boolean
|
||||
/**
|
||||
* 动态计算按钮属性
|
||||
*/
|
||||
dynamicButton: (data: any) => any
|
||||
/**
|
||||
* 按钮其他选项
|
||||
*/
|
||||
attrs: ButtonProps
|
||||
} & ButtonProps
|
||||
|
||||
/**
|
||||
@@ -77,6 +85,8 @@ export interface CommonTableProps extends TableProps<any> {
|
||||
pageAlign?: 'left' | 'center' | 'right';
|
||||
/** 其他分页配置项 */
|
||||
pageAttrs?: PaginationProps;
|
||||
/** 是否显示分页数量选择 **/
|
||||
showPageSizes?: boolean;
|
||||
/** loading状态 */
|
||||
loading?: boolean;
|
||||
/** 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 { useRoute, useRouter } from 'vue-router'
|
||||
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 { toGetParams } from '@/utils'
|
||||
import { isNestedRoute } from '@/route/RouteUtils'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -13,7 +15,7 @@ watch(route, () => {
|
||||
tabsViewStore.addHistoryTab(route)
|
||||
tabsViewStore.currentTab = route.path
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
if (!tabsViewStore.historyTabs.length) {
|
||||
@@ -26,7 +28,9 @@ const selectHistoryTab = path => {
|
||||
const tab = isString(path) ? tabsViewStore.findHistoryTab(path) : path
|
||||
if (tab) {
|
||||
router.push(tab)
|
||||
tabsViewStore.addCachedTab(tab)
|
||||
if (!isNestedRoute(tab)) {
|
||||
tabsViewStore.addCachedTab(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +43,11 @@ const removeHistoryTab = path => {
|
||||
|
||||
const refreshHistoryTab = tab => {
|
||||
const time = new Date().getTime()
|
||||
router.push(`${tab.path}?${time}`)
|
||||
tabsViewStore.addCachedTab(tab)
|
||||
const query = Object.assign({}, tab.query, { _t: time })
|
||||
router.replace(`${tab.path}?${toGetParams(query)}`)
|
||||
if (!isNestedRoute(tab)) {
|
||||
tabsViewStore.addCachedTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
const removeOtherHistoryTabs = tab => {
|
||||
@@ -82,6 +89,7 @@ const onDropdownVisibleChange = (visible, tab) => {
|
||||
ref="tabItems"
|
||||
:key="item.path"
|
||||
:tab-item="item"
|
||||
:label-config="item.labelConfig"
|
||||
@refresh-history-tab="refreshHistoryTab"
|
||||
@remove-other-history-tabs="removeOtherHistoryTabs"
|
||||
@remove-history-tabs="removeHistoryTabs"
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
import { useMenuInfo, useMenuName } from '@/components/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||
import { $i18nKey } from '@/messages'
|
||||
|
||||
const tabsViewStore = useTabsViewStore()
|
||||
|
||||
const props = defineProps({
|
||||
labelConfig: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* @type RouteRecordRaw
|
||||
*/
|
||||
@@ -18,6 +23,11 @@ const props = defineProps({
|
||||
defineEmits(['removeHistoryTab', 'removeOtherHistoryTabs', 'removeHistoryTabs', 'refreshHistoryTab', 'onDropdownVisibleChange'])
|
||||
|
||||
const menuName = computed(() => {
|
||||
const labelConfig = props.labelConfig
|
||||
if (labelConfig) {
|
||||
const label = labelConfig.label || $i18nKey(labelConfig.labelKey)
|
||||
if (label) return label
|
||||
}
|
||||
return useMenuName(props.tabItem)
|
||||
})
|
||||
|
||||
@@ -26,6 +36,11 @@ const menuInfo = computed(() => {
|
||||
})
|
||||
|
||||
const menuIcon = computed(() => {
|
||||
const labelConfig = props.labelConfig
|
||||
if (labelConfig) {
|
||||
const icon = labelConfig.icon
|
||||
if (icon) return icon
|
||||
}
|
||||
if (menuInfo.value && menuInfo.value.icon) {
|
||||
return menuInfo.value.icon
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
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 { proxyMethod } from '@/components/utils'
|
||||
|
||||
@@ -13,6 +13,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
header: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: ''
|
||||
@@ -33,6 +37,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showFullscreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
dblclickToFullscreen: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
beforeClose: {
|
||||
type: Function,
|
||||
default: null
|
||||
@@ -65,6 +77,10 @@ const props = defineProps({
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
showButtons: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -99,6 +115,11 @@ const commonWindow = ref({
|
||||
if (form && !windowForms.value.includes(form)) {
|
||||
windowForms.value.push(form)
|
||||
}
|
||||
},
|
||||
removeForm (form) {
|
||||
if (form && windowForms.value.includes(form)) {
|
||||
windowForms.value.splice(windowForms.value.indexOf(form), 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
provide('commonWindow', commonWindow)
|
||||
@@ -138,21 +159,61 @@ const calcBeforeClose = computed(() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="title"
|
||||
class="common-window"
|
||||
:header="header||title"
|
||||
:before-close="calcBeforeClose"
|
||||
:width="width"
|
||||
:draggable="draggable"
|
||||
:overflow="true"
|
||||
:overflow="overflow"
|
||||
:destroy-on-close="destroyOnClose"
|
||||
:close-on-click-modal="closeOnClickModal"
|
||||
:close-on-press-escape="closeOnPressEscape"
|
||||
: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
|
||||
:class="defaultCls"
|
||||
:style="{ height:height }"
|
||||
@@ -161,7 +222,10 @@ const calcBeforeClose = computed(() => {
|
||||
name="default"
|
||||
/>
|
||||
</el-container>
|
||||
<template #footer>
|
||||
<template
|
||||
v-if="showButtons"
|
||||
#footer
|
||||
>
|
||||
<span class="dialog-footer container-center">
|
||||
<el-button
|
||||
v-if="showOk"
|
||||
@@ -177,7 +241,7 @@ const calcBeforeClose = computed(() => {
|
||||
</el-button>
|
||||
<template v-for="(button, index) in buttons">
|
||||
<el-button
|
||||
v-if="!button.buttonIf||button.buttonIf()"
|
||||
v-if="button.enabled!==false&&(!button.buttonIf||button.buttonIf())"
|
||||
:key="index"
|
||||
:type="button.type"
|
||||
:icon="button.icon"
|
||||
|
||||
@@ -12,7 +12,8 @@ defineProps({
|
||||
|
||||
const propConfig = ref({
|
||||
placement: 'top-start',
|
||||
rawContent: true
|
||||
rawContent: true,
|
||||
effect: 'dark'
|
||||
})
|
||||
|
||||
const setConfig = (config) => {
|
||||
@@ -42,6 +43,7 @@ defineExpose({
|
||||
v-bind="propConfig"
|
||||
>
|
||||
<template #content>
|
||||
<!--eslint-disable-next-line vue/no-v-html-->
|
||||
<div v-html="propConfig.content" />
|
||||
</template>
|
||||
</component>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isObject, isString } from 'lodash'
|
||||
import { isObject, isString } from 'lodash-es'
|
||||
import CommonTooltip from '@/components/directives/CommonTooltip.vue'
|
||||
import { DynamicHelper } from '@/components/directives/index'
|
||||
|
||||
@@ -9,7 +9,7 @@ const calcTooltipConfig = (binding) => {
|
||||
} else if (isString(binding.value)) {
|
||||
config.content = binding.value
|
||||
}
|
||||
if (binding.arg) {
|
||||
if (!config.placement && binding.arg) {
|
||||
config.placement = binding.arg
|
||||
}
|
||||
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 { OpenNewWindowDirective, DisableAffixDirective } from '@/components/directives/SimpleDirectives'
|
||||
import { h, render } from 'vue'
|
||||
import { isFunction } from 'lodash-es'
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import { elementLocale } from '@/messages'
|
||||
|
||||
export class DynamicHelper {
|
||||
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 vnode = h(...args)
|
||||
vnode.appContext = this.context
|
||||
render(vnode, container)
|
||||
const configVNode = h(ElConfigProvider, { // 处理动态调用组件页面中element控件语言不正确问题
|
||||
locale: elementLocale.value.localeData
|
||||
}, () => [vnode])
|
||||
configVNode.appContext = this.context
|
||||
render(configVNode, container)
|
||||
const appDiv = document.getElementById(this.appDivId)
|
||||
if (appDiv && container.firstElementChild) {
|
||||
appDiv.appendChild(container.firstElementChild)
|
||||
@@ -39,5 +53,7 @@ export default {
|
||||
DynamicHelper.app = Vue
|
||||
Vue.directive('common-tooltip', CommonTooltipDirective)
|
||||
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 CommonTabsView from '@/components/common-tabs-view/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 CommonBreadcrumb from '@/components/common-breadcrumb/index.vue'
|
||||
import CommonWindow from '@/components/common-window/index.vue'
|
||||
@@ -35,6 +36,7 @@ export default {
|
||||
Vue.component('CommonMenuItem', CommonMenuItem)
|
||||
Vue.component('CommonTabsView', CommonTabsView)
|
||||
Vue.component('CommonTable', CommonTable)
|
||||
Vue.component('CommonTableV2', CommonTableV2)
|
||||
Vue.component('CommonTableForm', CommonTableForm)
|
||||
Vue.component('CommonBreadcrumb', CommonBreadcrumb)
|
||||
Vue.component('CommonWindow', CommonWindow)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ref, unref } from 'vue'
|
||||
import { $i18nBundle, $i18nKey } from '@/messages'
|
||||
import { isArray, isObject, isFunction } from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
import { isArray, isFunction, isObject } from 'lodash-es'
|
||||
|
||||
export const getFrontendPage = (totalCount, pageSize, pageNumber = 1) => {
|
||||
const pageCount = Math.floor((totalCount + pageSize - 1) / pageSize)
|
||||
@@ -138,10 +137,27 @@ export const parsePathParams = (path, params) => {
|
||||
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注解,方便代码提示
|
||||
* @param {CommonFormOption[]} formOptions 表单选项
|
||||
* @return {CommonFormOption[]} 表单选项配置
|
||||
* @param {CommonFormOption|CommonFormOption[]} formOptions 表单选项
|
||||
* @return {CommonFormOption|CommonFormOption[]} 表单选项配置
|
||||
*/
|
||||
export const defineFormOptions = (formOptions) => {
|
||||
return formOptions
|
||||
@@ -171,31 +187,3 @@ export const defineTableButtons = (tableButtons) => {
|
||||
export const defineMenuItems = (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
|
||||
|
||||
/**
|
||||
* 参数保存过期时间,单位分钟
|
||||
* @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
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
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 MaterialIconsVue from '@vicons/material'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import { kebabCase } from 'lodash-es'
|
||||
|
||||
export const INSTALL_ICONS = []
|
||||
export const ICON_PREFIX = 'icon-'
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import '@/vendors/dayjs.js'
|
||||
import ElementPlus from '@/vendors/element-plus'
|
||||
import stores from '@/stores'
|
||||
import icons from '@/icons'
|
||||
import messages from '@/messages'
|
||||
@@ -13,6 +12,7 @@ import App from '@/App.vue'
|
||||
import router from '@/route/routes'
|
||||
|
||||
import './assets/main.css'
|
||||
import MonacoEditor from '@/vendors/monaco-editor'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -23,5 +23,6 @@ app.use(ElementPlus)
|
||||
app.use(icons)
|
||||
app.use(messages)
|
||||
app.use(commons)
|
||||
app.use(MonacoEditor)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const base = { // 预定义几种属性
|
||||
label: {},
|
||||
|
||||
@@ -8,6 +8,7 @@ common.label.settings = '设置'
|
||||
common.label.confirm = '确认'
|
||||
common.label.save = '保存'
|
||||
common.label.close = '关闭'
|
||||
common.label.copy = '复制'
|
||||
common.label.cancel = '取消'
|
||||
common.label.refresh = '刷新'
|
||||
common.label.closeOther = '关闭其他'
|
||||
@@ -46,6 +47,23 @@ common.label.keywords = '关键字'
|
||||
common.label.breadcrumb = '面包屑导航'
|
||||
common.label.username = '用户名'
|
||||
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=====================//
|
||||
common.msg.nonNull = '{0}不能为空'
|
||||
|
||||
@@ -8,6 +8,7 @@ common.label.settings = 'Settings'
|
||||
common.label.confirm = 'Confirm'
|
||||
common.label.save = 'Save'
|
||||
common.label.close = 'Close'
|
||||
common.label.copy = 'Copy'
|
||||
common.label.cancel = 'Cancel'
|
||||
common.label.refresh = 'Refresh'
|
||||
common.label.closeOther = 'Close Others'
|
||||
@@ -46,6 +47,23 @@ common.label.keywords = 'Keywords'
|
||||
common.label.breadcrumb = 'Breadcrumb'
|
||||
common.label.username = 'User Name'
|
||||
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=====================//
|
||||
common.msg.nonNull = '{0} is required.'
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'dayjs/locale/zh-cn'
|
||||
import dayjs from 'dayjs'
|
||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||
import { GlobalLocales } from '@/consts/GlobalConstants'
|
||||
import { isArray, isString } from 'lodash-es'
|
||||
|
||||
const DEFAULT_LOCALE = 'zh-CN'
|
||||
dayjs.locale(DEFAULT_LOCALE) // dayjs的语言配置
|
||||
@@ -57,7 +58,12 @@ export const $i18nMsg = (cn, en, replaceEmpty = true) => {
|
||||
* @param {String[]=} params 可选参数
|
||||
* @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>
|
||||
@@ -78,14 +84,22 @@ export const $i18nByLocale = (key, locale, args) => {
|
||||
|
||||
/**
|
||||
* 方便多个资源key解析
|
||||
* @param {String} key 国际化资源key
|
||||
* @param {String|[String]} key 国际化资源key
|
||||
* @param {String} args 可选参数,也是资源key,方便多个资源key解析
|
||||
*/
|
||||
export const $i18nKey = (key, ...args) => {
|
||||
if (isArray(key)) {
|
||||
args = key.slice(1)
|
||||
key = key[0]
|
||||
}
|
||||
args = args.map(argKey => $i18nBundle(argKey))
|
||||
return $i18nBundle(key, args)
|
||||
}
|
||||
|
||||
export const $i18nConcat = (...items) => {
|
||||
return items.map(item => (item ?? '')).filter(item => !!item).join($i18nMsg('', ' ', false))
|
||||
}
|
||||
|
||||
export default {
|
||||
install (app) {
|
||||
app.use(i18n)
|
||||
@@ -95,6 +109,7 @@ export default {
|
||||
$i18nKey,
|
||||
$i18nBundle,
|
||||
$isLocale,
|
||||
$i18nConcat,
|
||||
$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',
|
||||
name: 'TestPage',
|
||||
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',
|
||||
name: 'WindowForms',
|
||||
|
||||
@@ -2,7 +2,9 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import Login from '@/views/Login.vue'
|
||||
import AdminRoutes from '@/route/AdminRoutes'
|
||||
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({
|
||||
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.afterEach((...args) => {
|
||||
processRouteLoading(...args)
|
||||
routeScrollBehavior(...args)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useCityAutocompleteConfig = () => {
|
||||
label: $i18nMsg('英文名', 'EN Name'),
|
||||
property: 'nameEn'
|
||||
}],
|
||||
searchMethod ({ query, page }, cb) {
|
||||
searchMethod ({ page }, cb) {
|
||||
loadAutoCities({ page }) // {query, page}
|
||||
.then(result => {
|
||||
const data = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { INSTALL_ICONS } from '@/icons'
|
||||
import chunk from 'lodash/chunk'
|
||||
import { chunk } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* @param keywords {string}
|
||||
@@ -9,7 +9,14 @@ import chunk from 'lodash/chunk'
|
||||
export const filterIconsByKeywords = (keywords, colSize) => {
|
||||
let installIcons = INSTALL_ICONS
|
||||
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 {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { $i18nMsg } from '@/messages'
|
||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||
import { GlobalLocales } from '@/consts/GlobalConstants'
|
||||
import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
||||
import { I18N_ENABLED, THEME_ENABLED } from '@/config'
|
||||
import { $logout } from '@/utils'
|
||||
|
||||
export const searchMenusResult = (queryParam, config) => {
|
||||
return $httpPost('/api/searchMenus', queryParam, config)
|
||||
@@ -136,6 +138,7 @@ export const useThemeAndLocaleMenus = () => {
|
||||
return [{
|
||||
icon: 'LanguageFilled',
|
||||
isDropdown: true,
|
||||
enabled: I18N_ENABLED,
|
||||
children: [
|
||||
{
|
||||
iconIf: () => GlobalLocales.CN === globalConfigStore.currentLocale ? 'check' : '',
|
||||
@@ -151,6 +154,7 @@ export const useThemeAndLocaleMenus = () => {
|
||||
},
|
||||
{
|
||||
isDropdown: true,
|
||||
enabled: THEME_ENABLED,
|
||||
iconIf: () => !globalConfigStore.isDarkTheme ? 'moon' : 'sunny',
|
||||
click: () => globalConfigStore.changeTheme(!globalConfigStore.isDarkTheme)
|
||||
}]
|
||||
@@ -189,9 +193,8 @@ export const useBaseTopMenus = () => {
|
||||
},
|
||||
{
|
||||
labelKey: 'common.label.logout',
|
||||
click (router) {
|
||||
loginConfigStore.logout()
|
||||
router.push('/login')
|
||||
click () {
|
||||
$logout()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -93,7 +93,7 @@ export const useUserAutocompleteConfig = () => {
|
||||
property: 'address',
|
||||
width: '300px'
|
||||
}],
|
||||
searchMethod ({ query, page }, cb) {
|
||||
searchMethod ({ page }, cb) {
|
||||
loadUsersResult({ page })
|
||||
.then(result => {
|
||||
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 { defineStore } from 'pinia'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { GlobalLayoutMode, GlobalLocales } from '@/consts/GlobalConstants'
|
||||
import { useDark, useMediaQuery } from '@vueuse/core'
|
||||
import { GlobalLayoutMode, GlobalLocales, LoadSaveParamMode } from '@/consts/GlobalConstants'
|
||||
import { changeMessages } from '@/messages'
|
||||
import { useSystemKey } from '@/utils'
|
||||
import { AUTO_LAYOUT_ENABLED, I18N_ENABLED, THEME_ENABLED } from '@/config'
|
||||
|
||||
export const useGlobalConfigStore = defineStore('globalConfig', () => {
|
||||
const currentLocale = ref(GlobalLocales.CN)
|
||||
const systemKey = import.meta.env.VITE_APP_SYSTEM_KEY
|
||||
const isDarkTheme = useDark({
|
||||
storageKey: `__${systemKey}__vueuse-color-scheme`
|
||||
})
|
||||
const systemKey = useSystemKey()
|
||||
const isDarkTheme = THEME_ENABLED
|
||||
? useDark({
|
||||
storageKey: `__${systemKey}__vueuse-color-scheme`
|
||||
})
|
||||
: ref(false)
|
||||
const isCollapseLeft = ref(false)
|
||||
const isShowSettings = ref(false)
|
||||
const isShowBreadcrumb = 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 {
|
||||
currentLocale,
|
||||
isDarkTheme,
|
||||
@@ -22,8 +28,10 @@ export const useGlobalConfigStore = defineStore('globalConfig', () => {
|
||||
isShowSettings,
|
||||
isShowBreadcrumb,
|
||||
layoutMode,
|
||||
loadSaveParamMode,
|
||||
showMenuIcon,
|
||||
changeLocale (locale) {
|
||||
if (!I18N_ENABLED) return
|
||||
if (Object.values(GlobalLocales).includes(locale)) {
|
||||
currentLocale.value = locale
|
||||
} else {
|
||||
@@ -32,6 +40,7 @@ export const useGlobalConfigStore = defineStore('globalConfig', () => {
|
||||
changeMessages(locale)
|
||||
},
|
||||
changeTheme (dark) {
|
||||
if (!THEME_ENABLED) return
|
||||
isDarkTheme.value = dark
|
||||
},
|
||||
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 { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { login } from '@/services/login/LoginService'
|
||||
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', () => {
|
||||
/**
|
||||
* 登录结果
|
||||
* @type {{value: {accessToken: string, account: Object, systemKey: string, expires: Date}}}
|
||||
*/
|
||||
const loginResult = ref(null)
|
||||
/**
|
||||
* 登录成功后保存accessToken
|
||||
* @type {Ref<string>}
|
||||
*/
|
||||
const accessToken = ref('')
|
||||
const accessToken = computed(() => loginResult.value?.accessToken)
|
||||
/**
|
||||
* 保存登录用户信息
|
||||
* @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 {
|
||||
loginResult,
|
||||
lastLoginName,
|
||||
accessToken,
|
||||
accountInfo,
|
||||
systemKey,
|
||||
/**
|
||||
* @param {{account: Object, accessToken:string}} loginResult
|
||||
* @param {{account: Object, accessToken:string}} resultData
|
||||
*/
|
||||
setLoginAccountInfo (loginResult) {
|
||||
accountInfo.value = loginResult.account
|
||||
accessToken.value = loginResult.accessToken
|
||||
setLoginAccountInfo (resultData) {
|
||||
loginResult.value = resultData
|
||||
lastLoginName.value = resultData?.account?.actualLoginName
|
||||
},
|
||||
clearLoginInfo () {
|
||||
accessToken.value = ''
|
||||
accountInfo.value = null
|
||||
loginResult.value = null
|
||||
},
|
||||
isLoginIn () {
|
||||
if (loginResult.value?.expires) {
|
||||
if (dayjs(loginResult.value.expires).isBefore(dayjs())) { // Token过期
|
||||
this.logout()
|
||||
return false
|
||||
}
|
||||
}
|
||||
return !!accessToken.value
|
||||
},
|
||||
logout () {
|
||||
// 清理登录数据
|
||||
this.clearLoginInfo()
|
||||
// 清理TAB数据, $reset似乎不能用
|
||||
useTabsViewStore().clearAllTabs()
|
||||
useMenuConfigStore().clearBusinessMenus()
|
||||
// $reset清理数据
|
||||
useTabsViewStore().$reset()
|
||||
useGlobalSearchParamStore().$reset()
|
||||
},
|
||||
async 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 {
|
||||
checkReplaceHistoryShouldReplace,
|
||||
isNestedRoute
|
||||
} from '@/route/RouteUtils'
|
||||
import { TAB_MODE_MAX_CACHES } from '@/config'
|
||||
|
||||
/**
|
||||
* @typedef {Object} TabsViewStore
|
||||
@@ -14,10 +19,12 @@ import { defineStore } from 'pinia'
|
||||
* @return {TabsViewStore}
|
||||
*/
|
||||
export const useTabsViewStore = defineStore('tabsView', () => {
|
||||
const isTabMode = ref(true)
|
||||
const isTabMode = ref(false)
|
||||
const isCachedTabMode = ref(true)
|
||||
const isShowTabIcon = ref(true)
|
||||
const currentTab = ref('')
|
||||
const currentTabItem = ref(null)
|
||||
const maxCacheCount = ref(TAB_MODE_MAX_CACHES)
|
||||
/**
|
||||
* @type {{value: [import('vue-router').RouteRecordRaw]}}
|
||||
*/
|
||||
@@ -48,14 +55,15 @@ export const useTabsViewStore = defineStore('tabsView', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const checkMataReplaceHistory = (historyTab, tab) => {
|
||||
// 如果meta中配置有replaceTabHistory,默认替换相关的tab
|
||||
return historyTab.meta && historyTab.meta.replaceTabHistory && historyTab.meta.replaceTabHistory === tab.name
|
||||
}
|
||||
const isHomeTab = tab => !!tab?.meta?.homeFlag
|
||||
|
||||
const isSameReplaceHistory = (historyTab, tab) => {
|
||||
return historyTab.meta && historyTab.meta.replaceTabHistory && tab.meta && tab.meta.replaceTabHistory &&
|
||||
historyTab.meta.replaceTabHistory === tab.meta.replaceTabHistory
|
||||
const getNestedParentTab = tab => isNestedRoute(tab) && tab.matched?.length >= 2 ? tab.matched[tab.matched.length - 2] : tab
|
||||
|
||||
const addNestedParentTab = (tab, replaceTab) => {
|
||||
if (isCachedTabMode.value && !forceNotCache(tab)) {
|
||||
const parentTab = getNestedParentTab(tab)
|
||||
addCachedTab(parentTab, replaceTab)
|
||||
}
|
||||
}
|
||||
|
||||
const addHistoryTab = (tab) => {
|
||||
@@ -63,16 +71,20 @@ export const useTabsViewStore = defineStore('tabsView', () => {
|
||||
if (isTabMode.value) {
|
||||
const idx = historyTabs.value.findIndex(v => v.path === tab.path)
|
||||
if (idx < 0) {
|
||||
const replaceIdx = historyTabs.value.findIndex(v => checkMataReplaceHistory(v, tab) ||
|
||||
checkMataReplaceHistory(tab, v) || isSameReplaceHistory(v, tab))
|
||||
const replaceIdx = historyTabs.value.findIndex(v => checkReplaceHistoryShouldReplace(v, tab))
|
||||
let replaceTab = null
|
||||
if (replaceIdx > -1) {
|
||||
replaceTab = historyTabs.value[replaceIdx]
|
||||
historyTabs.value.splice(replaceIdx, 1, Object.assign({}, tab))
|
||||
} 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) => {
|
||||
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 = []
|
||||
if (type === 'right') {
|
||||
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) => {
|
||||
if (isCachedTabMode.value && tab.name && !tab.name.includes('-')) {
|
||||
if (isCachedTabMode.value && !forceNotCache(tab) && tab.name && !tab.name.includes('-')) {
|
||||
removeCachedTab(replaceTab)
|
||||
if (!cachedTabs.value.includes(tab.name)) {
|
||||
cachedTabs.value.push(tab.name)
|
||||
nextTick(() => { cachedTabs.value.push(tab.name) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeCachedTab = tab => {
|
||||
if (tab) {
|
||||
tab = getNestedParentTab(tab)
|
||||
const idx = cachedTabs.value.findIndex(v => v === tab.name)
|
||||
if (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 {
|
||||
isTabMode,
|
||||
isCachedTabMode,
|
||||
isShowTabIcon,
|
||||
maxCacheCount,
|
||||
currentTab,
|
||||
currentTabItem,
|
||||
historyTabs,
|
||||
cachedTabs,
|
||||
$customReset (initState) {
|
||||
Object.assign(initState, { // 保留部分配置
|
||||
isTabMode: isTabMode.value,
|
||||
isCachedTabMode: isCachedTabMode.value,
|
||||
isShowTabIcon: isShowTabIcon.value
|
||||
})
|
||||
},
|
||||
changeTabMode (val) {
|
||||
isTabMode.value = val
|
||||
if (!isTabMode.value) {
|
||||
clearHistoryTabs()
|
||||
clearAllTabs()
|
||||
}
|
||||
},
|
||||
changeCachedTabMode (val) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
import { useSystemKey } from '@/utils'
|
||||
import { cloneDeep, isFunction } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* 组合式api的$reset需要自己实现
|
||||
@@ -7,9 +9,17 @@ import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
* @param 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.$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(createPersistedState({
|
||||
key: key => {
|
||||
const systemKey = import.meta.env.VITE_APP_SYSTEM_KEY
|
||||
const systemKey = useSystemKey()
|
||||
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 { $i18nBundle } from '@/messages'
|
||||
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({
|
||||
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
|
||||
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 loginConfigStore = useLoginConfigStore()
|
||||
config.headers.locale = globalConfigStore.currentLocale
|
||||
if (config.addToken !== false && loginConfigStore.accessToken) { // 添加token
|
||||
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
|
||||
})
|
||||
|
||||
const networkErrorFun = debounce(() => ElMessage.error($i18nBundle('common.msg.networkError')), 300)
|
||||
const networkTimeoutFun = debounce(() => ElMessage.error($i18nBundle('common.msg.networkTimeout')), 300)
|
||||
|
||||
$http.interceptors.response.use(data => {
|
||||
// todo 其他处理
|
||||
return data
|
||||
$http.interceptors.response.use(response => {
|
||||
if (hasLoading(response.config)) {
|
||||
$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 => {
|
||||
console.info(error.code, error.message)
|
||||
if (hasLoading(error?.config)) {
|
||||
$coreHideLoading()
|
||||
}
|
||||
console.info('=========================axios', error)
|
||||
if (error.message === 'Network Error') {
|
||||
networkErrorFun()
|
||||
} else if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') > -1) {
|
||||
networkTimeoutFun()
|
||||
}
|
||||
if (error.response.status === 401 && !error.response.config.isLogin) {
|
||||
if (error.response?.status === 401 && !error.response?.config.isLogin) {
|
||||
// 跳转登录页面
|
||||
$goto('/login')
|
||||
}
|
||||
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) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
$http.post(url, data, config).then(response => {
|
||||
if (response.data) {
|
||||
resolve(response.data) // 只要有数据就认为成功,内容再解析
|
||||
} else {
|
||||
reject(new Error('No response data'))
|
||||
}
|
||||
}, reject)
|
||||
return $http.post(url, data, config).then(response => {
|
||||
if (response?.data) {
|
||||
return response.data // 只要有数据就认为成功,内容再解析
|
||||
} else {
|
||||
throw new Error('No response data')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const $httpGet = (url, data, config) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
$http.get(url, config).then(response => {
|
||||
if (response.data) {
|
||||
resolve(response.data) // 只要有数据就认为成功,内容再解析
|
||||
} else {
|
||||
reject(new Error('No response data'))
|
||||
}
|
||||
}, reject)
|
||||
/**
|
||||
* @param url URL地址
|
||||
* @param [config] {ServiceRequestConfig} 配置对象
|
||||
* @return {Promise<unknown>}
|
||||
*/
|
||||
export const $httpGet = (url, config) => {
|
||||
return $http.get(url, config).then(response => {
|
||||
if (response?.data) {
|
||||
return response.data // 只要有数据就认为成功,内容再解析
|
||||
} 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 { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||
import { useMenuConfigStore } from '@/stores/MenuConfigStore'
|
||||
import { GlobalLayoutMode } from '@/consts/GlobalConstants'
|
||||
import { computed } from '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 menuConfigStore = useMenuConfigStore()
|
||||
const tabsViewStore = useTabsViewStore()
|
||||
const breadcrumbConfigStore = useBreadcrumbConfigStore()
|
||||
const showLeftMenu = computed(() => {
|
||||
return globalConfigStore.layoutMode === GlobalLayoutMode.LEFT
|
||||
})
|
||||
menuConfigStore.loadBusinessMenus()
|
||||
useTabModeScrollSaver()
|
||||
useMenuConfigStore().loadBusinessMenus()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="index-container">
|
||||
<el-aside
|
||||
v-if="showLeftMenu"
|
||||
class="index-aside"
|
||||
class="index-aside menu"
|
||||
width="auto"
|
||||
>
|
||||
<left-menu />
|
||||
@@ -33,39 +38,63 @@ menuConfigStore.loadBusinessMenus()
|
||||
v-if="globalConfigStore.layoutMode === GlobalLayoutMode.TOP && globalConfigStore.isShowBreadcrumb"
|
||||
class="tabs-header"
|
||||
>
|
||||
<common-breadcrumb />
|
||||
<common-breadcrumb
|
||||
:show-icon="tabsViewStore.isShowTabIcon"
|
||||
:label-config="breadcrumbConfigStore.breadcrumbConfig"
|
||||
/>
|
||||
</el-header>
|
||||
<el-header
|
||||
v-if="tabsViewStore.isTabMode"
|
||||
class="tabs-header"
|
||||
class="tabs-header tabMode"
|
||||
>
|
||||
<common-tabs-view />
|
||||
</el-header>
|
||||
<el-main>
|
||||
<el-main class="home-main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition
|
||||
name="slide-fade"
|
||||
:name="route.meta?.transition!==false?'slide-fade':''"
|
||||
mode="out-in"
|
||||
>
|
||||
<KeepAlive
|
||||
v-if="tabsViewStore.isTabMode&&tabsViewStore.isCachedTabMode"
|
||||
:include="tabsViewStore.cachedTabs"
|
||||
:max="10"
|
||||
:max="tabsViewStore.maxCacheCount"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.fullPath"
|
||||
:key="getParentRootKey(route)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-else
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</transition>
|
||||
</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>
|
||||
<global-settings />
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
<style scoped>
|
||||
.tabs-header {
|
||||
margin-top: 5px;
|
||||
height: 40px
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -76,7 +76,7 @@ const searchFormOptions = computed(() => {
|
||||
]
|
||||
})
|
||||
const doSearch = form => {
|
||||
console.info('=================searchParam', searchParam.value)
|
||||
console.info('=================searchParam', searchParam.value, form)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ const searchFormOptions = computed(() => {
|
||||
})
|
||||
const doSearch = form => {
|
||||
console.info('=================searchParam', form, searchParam.value)
|
||||
loadUsers()
|
||||
}
|
||||
/** *************用户编辑**************/
|
||||
const currentUser = ref(null)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||
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 tabsViewStore = useTabsViewStore()
|
||||
/**
|
||||
@@ -12,7 +13,7 @@ const options = [
|
||||
labelKey: 'common.label.theme',
|
||||
prop: 'isDarkTheme',
|
||||
type: 'switch',
|
||||
model: globalConfigStore,
|
||||
enabled: THEME_ENABLED,
|
||||
attrs: {
|
||||
activeActionIcon: 'icon-moon',
|
||||
inactiveActionIcon: 'icon-sunny'
|
||||
@@ -22,7 +23,7 @@ const options = [
|
||||
labelKey: 'common.label.language',
|
||||
type: 'select',
|
||||
prop: 'currentLocale',
|
||||
model: globalConfigStore,
|
||||
enabled: I18N_ENABLED,
|
||||
change (val) {
|
||||
globalConfigStore.changeLocale(val)
|
||||
},
|
||||
@@ -36,9 +37,7 @@ const options = [
|
||||
},
|
||||
{
|
||||
labelKey: 'common.label.layout',
|
||||
type: 'select',
|
||||
prop: 'layoutMode',
|
||||
model: globalConfigStore,
|
||||
slot: 'layout',
|
||||
change (val) {
|
||||
globalConfigStore.changeLayout(val)
|
||||
},
|
||||
@@ -53,14 +52,12 @@ const options = [
|
||||
{
|
||||
labelKey: 'common.label.showMenuIcon',
|
||||
prop: 'showMenuIcon',
|
||||
type: 'switch',
|
||||
model: globalConfigStore
|
||||
type: 'switch'
|
||||
},
|
||||
{
|
||||
labelKey: 'common.label.breadcrumb',
|
||||
prop: 'isShowBreadcrumb',
|
||||
type: 'switch',
|
||||
model: globalConfigStore,
|
||||
change (val) {
|
||||
globalConfigStore.isShowBreadcrumb = val
|
||||
}
|
||||
@@ -88,6 +85,25 @@ const options = [
|
||||
prop: 'isShowTabIcon',
|
||||
type: 'switch',
|
||||
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>
|
||||
@@ -106,7 +122,39 @@ const options = [
|
||||
:show-buttons="false"
|
||||
:options="options"
|
||||
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 #footer>
|
||||
<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
|
||||
v-model="showWindow"
|
||||
:ok-click="submitForm"
|
||||
show-fullscreen
|
||||
>
|
||||
<el-container class="flex-column container-center">
|
||||
<common-form
|
||||
|
||||
@@ -1,20 +1,76 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
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/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
viteMockServe({
|
||||
mockPath: './mock'
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
export default ({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
return defineConfig({
|
||||
base: env.VITE_APP_CONTEXT_PATH,
|
||||
define: {
|
||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version)
|
||||
},
|
||||
plugins: [vue(), vueJsx(), eslint(), ...optionalPlugins],
|
||||
esbuild: {
|
||||
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