1. 控件优化

2. 增加monaco-editor、echarts等
This commit is contained in:
Gary Fu
2024-06-30 10:58:24 +08:00
parent 56fd1fa151
commit aac8f09802
74 changed files with 8494 additions and 936 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -90,6 +90,8 @@ export interface CommonAutocompleteProps {
minHeight?: string;
// input自定义属性
inputAttrs?: InputProps;
// 输入当做值的特殊模式
inputAsValue?: boolean;
// 验证事件
validateEvent?: boolean;
}

View File

@@ -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 }}

View File

@@ -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 = () => {

View File

@@ -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>

View File

@@ -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"

View File

@@ -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)"
>&nbsp;
<common-icon
icon="QuestionFilled"
/>
</el-link>
</span>
</el-tooltip>
</component>
</template>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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: {

View File

@@ -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"

View File

@@ -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" />

View File

@@ -3,7 +3,7 @@ export interface SortOption {
labelKey?: string;// 国际化资源key首选该属性不存在才使用label
label?: string;
showIcon?: boolean; // 控制某些排序不显示图标
fixedValue?: 'ASC' | 'DESC' // 固定排序模式
fixedValue?: 'ASC' | 'DESC'; // 固定排序模式
}
/**

View File

@@ -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"

View File

@@ -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"

View 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>

View File

@@ -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 // 这里有个buginfinitePaging时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"

View File

@@ -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显示消息 */

View 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>

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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

View 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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
View 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
View 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>;
}

View 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
}
}

View File

@@ -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-'

View File

@@ -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')

View File

@@ -1,4 +1,4 @@
import cloneDeep from 'lodash/cloneDeep'
import { cloneDeep } from 'lodash-es'
const base = { // 预定义几种属性
label: {},

View File

@@ -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}不能为空'

View File

@@ -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.'

View File

@@ -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
View 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)
}
})
}

View File

@@ -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',

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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 {

View File

@@ -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()
}
}
]

View File

@@ -93,7 +93,7 @@ export const useUserAutocompleteConfig = () => {
property: 'address',
width: '300px'
}],
searchMethod ({ query, page }, cb) {
searchMethod ({ page }, cb) {
loadUsersResult({ page })
.then(result => {
const data = {

View 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)
}
}
})

View File

@@ -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) {

View 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
})

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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
View 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
View File

@@ -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
View 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
View 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
View 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
View 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(/^&lt;.*/)) {
data = data.replace(/&lt;/ig, '<').replace(/&gt;/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()])
}
})
}
}

View File

@@ -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>&nbsp;
<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>

View File

@@ -76,7 +76,7 @@ const searchFormOptions = computed(() => {
]
})
const doSearch = form => {
console.info('=================searchParam', searchParam.value)
console.info('=================searchParam', searchParam.value, form)
}
</script>

View File

@@ -105,6 +105,7 @@ const searchFormOptions = computed(() => {
})
const doSearch = form => {
console.info('=================searchParam', form, searchParam.value)
loadUsers()
}
/** *************用户编辑**************/
const currentUser = ref(null)

View File

@@ -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">

View 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>

View 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>

View File

@@ -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

View File

@@ -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
}
}
}
}
})
})
}