mirror of
https://github.com/fugary/simple-element-plus-template.git
synced 2026-09-11 09:23:53 +00:00
feat: 更新一些新功能
This commit is contained in:
73
index.html
73
index.html
@@ -1,13 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>%VITE_APP_NAME%</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.index-loading-spinner {
|
||||
top: 50%;
|
||||
margin-top: calc((0px - 42px) / 2);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.index-loading-spinner .circular {
|
||||
display: inline;
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
animation: loading-rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.index-loading-spinner .path {
|
||||
animation: loading-dash 1.5s ease-in-out infinite;
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-width: 2;
|
||||
stroke: #409EFFFF;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
@keyframes loading-rotate {
|
||||
to {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 200;
|
||||
stroke-dashoffset: 0
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -40px
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -120px
|
||||
}
|
||||
}
|
||||
|
||||
.index-loading-spinner .el-loading-text {
|
||||
color: #409EFFFF;
|
||||
margin: 3px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<div class="index-loading-spinner">
|
||||
<svg class="circular" viewBox="0 0 50 50">
|
||||
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
|
||||
<p class="el-loading-text">系统加载中...</p>
|
||||
<p class="el-loading-text">System Loading...</p>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -52,8 +52,9 @@ export default [
|
||||
{
|
||||
url: '/simple/city/selectCities',
|
||||
method: 'post',
|
||||
response: request => {
|
||||
response: () => {
|
||||
return {
|
||||
|
||||
success: true,
|
||||
message: 'Success',
|
||||
resultData: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, isVNode } from 'vue'
|
||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||
import { isFunction } from 'lodash-es'
|
||||
|
||||
@@ -45,6 +45,19 @@ const tooltipFunc = ($event) => {
|
||||
:readonly="option.readonly"
|
||||
v-bind="option.attrs"
|
||||
>
|
||||
<template
|
||||
v-for="(slot, slotKey) in (option.slots||{})"
|
||||
:key="slotKey"
|
||||
#[slotKey]="scope"
|
||||
>
|
||||
<component
|
||||
:is="scope[`__slotResult__${slotKey}`]"
|
||||
v-if="isVNode(scope[`__slotResult__${slotKey}`] = slot(scope))"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ scope[`__slotResult__${slotKey}`] }}
|
||||
</template>
|
||||
</template>
|
||||
{{ label }}
|
||||
<el-tooltip
|
||||
v-if="option.tooltip||option.tooltipFunc"
|
||||
@@ -58,6 +71,8 @@ const tooltipFunc = ($event) => {
|
||||
<span>
|
||||
<el-link
|
||||
underline="never"
|
||||
:type="option.tooltipFunc ? 'primary' : 'default'"
|
||||
v-bind="option.tooltipLinkAttrs"
|
||||
@click="tooltipFunc($event)"
|
||||
>
|
||||
<common-icon
|
||||
|
||||
@@ -3,7 +3,7 @@ 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, get, isFunction, set, isArray, isString } from 'lodash-es'
|
||||
import { cloneDeep, get, isFunction, set, isArray, isString, isEqual } from 'lodash-es'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
@@ -173,10 +173,15 @@ const rules = computed(() => {
|
||||
}, ..._rules]
|
||||
}
|
||||
}
|
||||
formItemRef.value && formItemRef.value.clearValidate()
|
||||
return _rules
|
||||
})
|
||||
|
||||
watch(rules, (newRules, oldRules) => {
|
||||
if (formItemRef.value && !isEqual(newRules, oldRules)) {
|
||||
formItemRef.value.clearValidate()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
const initFormModel = () => {
|
||||
if (formModel.value) {
|
||||
const option = calcOption.value
|
||||
@@ -222,6 +227,15 @@ const formatResult = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
const tooltips = computed(() => {
|
||||
if (calcOption.value.tooltips?.length) {
|
||||
return calcOption.value.tooltips
|
||||
} else if (calcOption.value.tooltip || calcOption.value.tooltipFunc) {
|
||||
return [calcOption.value]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -251,23 +265,25 @@ const formatResult = computed(() => {
|
||||
/>
|
||||
<slot name="afterLabel" />
|
||||
<el-tooltip
|
||||
v-if="calcOption.tooltip||calcOption.tooltipFunc"
|
||||
v-for="(tooltipOption, index) in tooltips"
|
||||
:key="index"
|
||||
class="box-item common-el-tooltip"
|
||||
effect="dark"
|
||||
:disabled="!calcOption.tooltip"
|
||||
:content="calcOption.tooltip"
|
||||
:disabled="!tooltipOption.tooltip"
|
||||
:content="tooltipOption.tooltip"
|
||||
placement="top-start"
|
||||
raw-content
|
||||
v-bind="calcOption.tooltipAttrs"
|
||||
v-bind="tooltipOption.tooltipAttrs"
|
||||
>
|
||||
<span>
|
||||
<el-link
|
||||
v-bind="calcOption.tooltipLinkAttrs"
|
||||
underline="never"
|
||||
@click="calcOption.tooltipFunc"
|
||||
:type="tooltipOption.tooltipFunc ? 'primary' : 'default'"
|
||||
v-bind="tooltipOption.tooltipLinkAttrs"
|
||||
@click="tooltipOption.tooltipFunc"
|
||||
>
|
||||
<common-icon
|
||||
:icon="calcOption.tooltipIcon||'QuestionFilled'"
|
||||
:icon="tooltipOption.tooltipIcon||'QuestionFilled'"
|
||||
/>
|
||||
</el-link>
|
||||
</span>
|
||||
|
||||
@@ -176,6 +176,11 @@ const goBack = (...args) => {
|
||||
v-bind="{...$attrs, 'class':undefined}"
|
||||
@submit.prevent
|
||||
>
|
||||
<slot
|
||||
:form="form"
|
||||
:model="formModel"
|
||||
name="before-options"
|
||||
/>
|
||||
<template
|
||||
v-for="(option,index) in options"
|
||||
:key="index"
|
||||
@@ -230,6 +235,11 @@ const goBack = (...args) => {
|
||||
>
|
||||
{{ resetLabel||$t('common.label.reset') }}
|
||||
</el-button>
|
||||
<slot
|
||||
:form="form"
|
||||
:model="formModel"
|
||||
name="buttons"
|
||||
/>
|
||||
<el-button
|
||||
v-if="showBack||backUrl"
|
||||
:disabled="disableButtons"
|
||||
@@ -237,11 +247,6 @@ const goBack = (...args) => {
|
||||
>
|
||||
{{ backLabel||$t('common.label.back') }}
|
||||
</el-button>
|
||||
<slot
|
||||
:form="form"
|
||||
:model="formModel"
|
||||
name="buttons"
|
||||
/>
|
||||
</el-form-item>
|
||||
<slot
|
||||
:form="form"
|
||||
|
||||
12
src/components/common-form/public.d.ts
vendored
12
src/components/common-form/public.d.ts
vendored
@@ -83,6 +83,7 @@ export type PropsMap = {
|
||||
'common-form-label': CommonFormLabelProps,
|
||||
'common-icon-select': IconSelectProps,
|
||||
'common-autocomplete': CommonAutocompleteProps,
|
||||
'common-object': Record<string, any>,
|
||||
[key:string]: InputProps
|
||||
}
|
||||
|
||||
@@ -146,8 +147,19 @@ export interface CommonFormOption extends FormControlTypeOption {
|
||||
tooltipLinkAttrs?: LinkProps;
|
||||
/** 提示函数 */
|
||||
tooltipFunc?: () => void;
|
||||
/** 多个提示信息配置 */
|
||||
tooltips?: Array<{
|
||||
tooltip?: string;
|
||||
tooltipIcon?: string;
|
||||
tooltipAttrs?: ElTooltipProps;
|
||||
tooltipLinkAttrs?: LinkProps;
|
||||
tooltipFunc?: () => void;
|
||||
}>;
|
||||
/** 子选项自定义插槽映射 */
|
||||
slots?: Record<string, (scope: any) => any>;
|
||||
/** 自动trim,默认false**/
|
||||
trim?: boolean,
|
||||
|
||||
/** 自动upperCase,默认false**/
|
||||
upperCase?: boolean,
|
||||
/** 自动lowerCase,默认false**/
|
||||
|
||||
@@ -159,8 +159,8 @@ const selectIcon = icon => {
|
||||
<el-backtop
|
||||
v-common-tooltip="$t('common.label.backtop')"
|
||||
target=".scroller"
|
||||
:right="10"
|
||||
:bottom="10"
|
||||
:right="40"
|
||||
:bottom="40"
|
||||
/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
@@ -33,11 +33,9 @@ const activeRoutePath = computed(() => {
|
||||
router
|
||||
>
|
||||
<slot name="before" />
|
||||
<template
|
||||
<common-menu-item
|
||||
v-for="(menuItem, index) in menuItems"
|
||||
:key="index"
|
||||
>
|
||||
<common-menu-item
|
||||
:menu-item="menuItem"
|
||||
:index="`${index}`"
|
||||
>
|
||||
@@ -45,7 +43,6 @@ const activeRoutePath = computed(() => {
|
||||
<slot name="split" />
|
||||
</template>
|
||||
</common-menu-item>
|
||||
</template>
|
||||
<slot name="default" />
|
||||
</el-menu>
|
||||
</template>
|
||||
|
||||
74
src/components/common-object/index.vue
Normal file
74
src/components/common-object/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import CommonParamsEdit from '@/views/components/utils/CommonParamsEdit.vue'
|
||||
import { isEqual } from 'lodash-es'
|
||||
|
||||
defineProps({
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const vModel = defineModel({
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
})
|
||||
|
||||
const parsedModel = ref({
|
||||
params: []
|
||||
})
|
||||
|
||||
watch(vModel, value => {
|
||||
parsedModel.value.params = Object.entries(value || {}).map(([k, v]) => {
|
||||
return {
|
||||
name: k,
|
||||
value: v
|
||||
}
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
const parseModelParams = params => {
|
||||
const newVal = (params || []).filter(param => !!param.name).reduce((res, item) => {
|
||||
res[item.name] = item.value
|
||||
return res
|
||||
}, {})
|
||||
if (!isEqual(newVal, vModel.value)) {
|
||||
vModel.value = newVal
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => parsedModel.value.params, parseModelParams, { deep: true })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="flex-column common-object">
|
||||
<common-form
|
||||
:model="parsedModel"
|
||||
:show-buttons="false"
|
||||
>
|
||||
<common-params-edit
|
||||
v-model="parsedModel.params"
|
||||
class="form-edit-width-100"
|
||||
form-prop="params"
|
||||
name-required
|
||||
:name-read-only="readonly||disabled"
|
||||
:value-read-only="readonly||disabled"
|
||||
:show-enable-switch="false"
|
||||
:show-copy-button="false"
|
||||
:show-add-button="!disabled&&!readonly"
|
||||
:show-paste-button="!disabled&&!readonly"
|
||||
:show-remove-button="!disabled&&!readonly"
|
||||
/>
|
||||
</common-form>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { toLabelByKey } from '@/components/utils'
|
||||
import { useRenderKey, useSortableParams } from '@/hooks/CommonHooks'
|
||||
|
||||
const props = defineProps({
|
||||
formOptions: {
|
||||
@@ -22,6 +23,14 @@ const props = defineProps({
|
||||
operationWidth: {
|
||||
type: String,
|
||||
default: '110px'
|
||||
},
|
||||
formPropPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
sortable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,6 +40,12 @@ const dataList = computed(() => {
|
||||
|
||||
const emit = defineEmits(['delete', 'change'])
|
||||
|
||||
const { renderKey } = useRenderKey()
|
||||
const rowKey = (row) => renderKey(row)
|
||||
|
||||
const sortableState = props.sortable ? useSortableParams(dataList, '.el-table__row') : {}
|
||||
const sortableRef = sortableState.sortableRef || ref()
|
||||
|
||||
const deleteItem = (item, index) => {
|
||||
emit('delete', {
|
||||
item, index
|
||||
@@ -57,13 +72,36 @@ const options = computed(() => {
|
||||
|
||||
<template>
|
||||
<el-table
|
||||
ref="sortableRef"
|
||||
:data="dataList"
|
||||
:row-key="rowKey"
|
||||
class="common-table-form"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="sortable"
|
||||
width="40px"
|
||||
align="center"
|
||||
>
|
||||
<template #default>
|
||||
<div
|
||||
class="el-form-item"
|
||||
style="display: flex; align-items: center; justify-content: center; height: 32px;"
|
||||
>
|
||||
<common-icon
|
||||
:size="18"
|
||||
icon="DragIndicatorFilled"
|
||||
class="move-indicator"
|
||||
style="cursor: move; color: var(--el-text-color-secondary);"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="(option, index) in options"
|
||||
:key="`${option.prop}__${index}`"
|
||||
:width="option.width"
|
||||
:min-width="option.minWidth"
|
||||
v-bind="option.columnAttrs"
|
||||
>
|
||||
<template
|
||||
v-if="option.headerSlot"
|
||||
@@ -103,7 +141,7 @@ const options = computed(() => {
|
||||
:model="row"
|
||||
label-width="0"
|
||||
:option="option"
|
||||
:prop="`${dataListKey}.${$index}.${option.prop}`"
|
||||
:prop="formPropPrefix ? `${formPropPrefix}.${dataListKey}.${$index}.${option.prop}` : `${dataListKey}.${$index}.${option.prop}`"
|
||||
@change="formChange($event, row, $index, option)"
|
||||
/>
|
||||
</template>
|
||||
@@ -145,5 +183,10 @@ const options = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.common-table-form :deep(.el-table__row .move-indicator) {
|
||||
visibility: hidden;
|
||||
}
|
||||
.common-table-form :deep(.el-table__row:hover .move-indicator) {
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,6 +65,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
okLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
okClick: {
|
||||
type: Function,
|
||||
default: null
|
||||
@@ -230,6 +234,7 @@ if (props.showFullscreen && props.dblclickToFullscreen) {
|
||||
<el-button
|
||||
v-if="showOk"
|
||||
type="primary"
|
||||
:loading="okLoading"
|
||||
@click="okButtonClick($event)"
|
||||
>{{ okLabel||$t('common.label.confirm') }}</el-button>
|
||||
<el-button
|
||||
|
||||
@@ -17,6 +17,7 @@ import CommonAutocomplete from '@/components/common-autocomplete/index.vue'
|
||||
import CommonSort from '@/components/common-sort/index.vue'
|
||||
import CommonDescriptions from '@/components/common-descriptions/index.vue'
|
||||
import CommonSplit from '@/components/common-split/index.vue'
|
||||
import CommonObject from '@/components/common-object/index.vue'
|
||||
import CommonDirectives from '@/components/directives'
|
||||
|
||||
/**
|
||||
@@ -46,6 +47,7 @@ export default {
|
||||
Vue.component('CommonSort', CommonSort)
|
||||
Vue.component('CommonDescriptions', CommonDescriptions)
|
||||
Vue.component('CommonSplit', CommonSplit)
|
||||
Vue.component('CommonObject', CommonObject)
|
||||
Vue.use(CommonDirectives)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { $i18nBundle } from '@/messages'
|
||||
/**
|
||||
* 全局布局模式
|
||||
* @readonly
|
||||
@@ -25,3 +26,44 @@ export const LoadSaveParamMode = {
|
||||
BACK: 'back',
|
||||
NEVER: 'never'
|
||||
}
|
||||
|
||||
export const useFormStatus = (prop = 'status', activeValue = 1, inactiveValue = 0) => {
|
||||
return {
|
||||
labelKey: 'common.label.status',
|
||||
prop,
|
||||
type: 'switch',
|
||||
attrs: {
|
||||
activeValue,
|
||||
inactiveValue,
|
||||
style: '--el-switch-on-color: #67c23a; --el-switch-off-color: #f56c6c',
|
||||
activeText: $i18nBundle('common.label.statusEnabled'),
|
||||
inactiveText: $i18nBundle('common.label.statusDisabled')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useSearchStatus = ({ prop = 'status', activeValue = 1, inactiveValue = 0, change, ...option } = {}) => {
|
||||
return {
|
||||
labelKey: 'common.label.status',
|
||||
prop,
|
||||
type: 'select',
|
||||
children: [
|
||||
{ value: activeValue, label: $i18nBundle('common.label.statusEnabled') },
|
||||
{ value: inactiveValue, label: $i18nBundle('common.label.statusDisabled') }
|
||||
],
|
||||
change,
|
||||
...option
|
||||
}
|
||||
}
|
||||
|
||||
export const useFormDelay = (prop = 'delay') => {
|
||||
return {
|
||||
labelKey: 'common.label.delay',
|
||||
tooltip: $i18nBundle('common.label.commonDelay', [100]),
|
||||
type: 'input-number',
|
||||
prop,
|
||||
attrs: {
|
||||
min: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { isFunction, isNumber } from 'lodash-es'
|
||||
import { provide, inject, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
|
||||
import { isFunction, isNumber, uniqueId } from 'lodash-es'
|
||||
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||
import { $coreHideLoading, $coreShowLoading } from '@/utils'
|
||||
import { GLOBAL_LOADING } from '@/config'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
const defaultPageProcessor = (searchResult, searchParam) => {
|
||||
const resultData = searchResult?.resultData || searchResult
|
||||
const pageObj = searchResult?.page || resultData?.page || searchResult?.pageSetting || resultData?.pageSetting
|
||||
if (pageObj && searchParam.value) {
|
||||
if (searchParam.value.page) {
|
||||
Object.assign(searchParam.value.page, pageObj)
|
||||
} else if (searchParam.value.pageSetting) {
|
||||
Object.assign(searchParam.value.pageSetting, pageObj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用搜索表格表单封装
|
||||
@@ -10,30 +25,30 @@ import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||
export const useTableAndSearchForm = ({
|
||||
searchMethod,
|
||||
defaultParam = {},
|
||||
dataProcessor,
|
||||
pageProcessor,
|
||||
dataProcessor = searchResult => searchResult.resultData,
|
||||
pageProcessor = defaultPageProcessor,
|
||||
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)
|
||||
const searchTableItems = async (pageNumber, newParams = {}, saveConfig) => {
|
||||
if (isNumber(pageNumber) && searchParam.value) {
|
||||
if (searchParam.value.page) {
|
||||
searchParam.value.page.pageNumber = pageNumber
|
||||
} else if (searchParam.value.pageSetting) {
|
||||
searchParam.value.pageSetting.pageNumber = pageNumber
|
||||
}
|
||||
}
|
||||
loading.value = true
|
||||
saveParam && globalSearchParamStore.saveCurrentParam(searchParam.value)
|
||||
saveParam && globalSearchParamStore.saveCurrentParam(searchParam.value, saveConfig)
|
||||
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)
|
||||
tableData.value = isFunction(dataProcessor) && dataProcessor?.(searchResult, searchParam)
|
||||
pageProcessor?.(searchResult, searchParam)
|
||||
}
|
||||
return searchResult
|
||||
}
|
||||
@@ -45,6 +60,30 @@ export const useTableAndSearchForm = ({
|
||||
}
|
||||
}
|
||||
|
||||
export const useInitLoadOnce = (loader, config = {}) => {
|
||||
const initLoading = ref(false)
|
||||
const initLoadOnce = async () => {
|
||||
if (!initLoading.value) {
|
||||
try {
|
||||
initLoading.value = true
|
||||
if (config.loading ?? GLOBAL_LOADING) {
|
||||
$coreShowLoading()
|
||||
}
|
||||
await loader()
|
||||
} finally {
|
||||
initLoading.value = false
|
||||
if (config.loading ?? GLOBAL_LOADING) {
|
||||
$coreHideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
initLoading,
|
||||
initLoadOnce
|
||||
}
|
||||
}
|
||||
|
||||
export const useDateStr = (watchFn) => {
|
||||
const dateStr = ref(new Date().getTime())
|
||||
watchFn && watch(watchFn, (update) => {
|
||||
@@ -68,3 +107,201 @@ export const useGlobalSaveSearchParam = (defaultParam) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useManagedArrayItems = () => {
|
||||
const managedItems = ref([])
|
||||
const context = ref(false)
|
||||
const startContext = () => (context.value = true)
|
||||
const pushItem = (item) => {
|
||||
const index = managedItems.value.findIndex(it => item.id === it.id)
|
||||
if (index < 0) {
|
||||
managedItems.value.push(item)
|
||||
return item
|
||||
} else {
|
||||
return goToItem(index)
|
||||
}
|
||||
}
|
||||
const goToItem = index => {
|
||||
let result = null
|
||||
if (index > -1 && (result = managedItems.value[index])) {
|
||||
context.value = true
|
||||
managedItems.value = managedItems.value.slice(0, index + 1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
const clearItems = () => {
|
||||
if (!context.value) {
|
||||
managedItems.value = []
|
||||
}
|
||||
context.value = false
|
||||
}
|
||||
return {
|
||||
managedItems,
|
||||
startContext,
|
||||
pushItem,
|
||||
goToItem,
|
||||
clearItems
|
||||
}
|
||||
}
|
||||
|
||||
export const useSortableParams = (params, selector, moveCls = '.move-indicator') => {
|
||||
let sortable = null
|
||||
const sortableRef = ref()
|
||||
const dragging = ref(false)
|
||||
const hoverIndex = ref(-1)
|
||||
|
||||
const initSortable = () => {
|
||||
let el = sortableRef.value?.$el || sortableRef.value
|
||||
if (el && !sortable) {
|
||||
const tbody = el.querySelector('tbody')
|
||||
if (tbody) {
|
||||
el = tbody
|
||||
}
|
||||
sortable = new Sortable(el, {
|
||||
animation: 150,
|
||||
draggable: selector,
|
||||
handle: moveCls,
|
||||
onStart () {
|
||||
hoverIndex.value = -1
|
||||
dragging.value = true
|
||||
},
|
||||
onEnd (event) {
|
||||
const { oldIndex, newIndex } = event
|
||||
params.value.splice(newIndex, 0, params.value.splice(oldIndex, 1)[0]) // 插入到 newIndex 位置
|
||||
setTimeout(() => {
|
||||
dragging.value = false
|
||||
hoverIndex.value = newIndex
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const destroySortable = () => {
|
||||
if (sortable) {
|
||||
sortable.destroy()
|
||||
sortable = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initSortable()
|
||||
})
|
||||
|
||||
watch(() => sortableRef.value, (val) => {
|
||||
if (val) {
|
||||
initSortable()
|
||||
} else {
|
||||
destroySortable()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroySortable()
|
||||
})
|
||||
return {
|
||||
dragging,
|
||||
hoverIndex,
|
||||
sortableRef
|
||||
}
|
||||
}
|
||||
|
||||
export const useRenderKey = () => {
|
||||
const renderKeyMap = new WeakMap()
|
||||
|
||||
function renderKey (param) {
|
||||
if (param.id != null) {
|
||||
return `param-${param.id}`
|
||||
}
|
||||
if (!renderKeyMap.has(param)) {
|
||||
renderKeyMap.set(param, uniqueId())
|
||||
}
|
||||
return `param-tmp-${renderKeyMap.get(param)}`
|
||||
}
|
||||
|
||||
return {
|
||||
renderKey
|
||||
}
|
||||
}
|
||||
|
||||
export const useProvideDataLoading = (loadingKey = 'dataLoading') => {
|
||||
const dataLoading = ref(false)
|
||||
const startLoading = (config = { delay: 100 }) => {
|
||||
if (!dataLoading.value) {
|
||||
dataLoading.value = true
|
||||
$coreShowLoading(config)
|
||||
}
|
||||
}
|
||||
provide(loadingKey, { dataLoading, startLoading })
|
||||
return {
|
||||
dataLoading,
|
||||
startLoading
|
||||
}
|
||||
}
|
||||
|
||||
export const useInjectDataLoading = (loadingKey = 'dataLoading') => {
|
||||
const { dataLoading, startLoading } = inject(loadingKey,
|
||||
{ dataLoading: ref(false), startLoading: () => {} })
|
||||
const endLoading = () => {
|
||||
if (dataLoading?.value) {
|
||||
setTimeout(() => {
|
||||
$coreHideLoading()
|
||||
dataLoading.value = false
|
||||
})
|
||||
} else {
|
||||
$coreHideLoading()
|
||||
}
|
||||
}
|
||||
return {
|
||||
startLoading,
|
||||
endLoading
|
||||
}
|
||||
}
|
||||
|
||||
export const useContextMenu = () => {
|
||||
const showContextMenu = ref(false)
|
||||
const contextMenuRef = ref(null)
|
||||
const contextMenuHandlers = ref([])
|
||||
const contextMenuItem = ref(null)
|
||||
const contextMenuDropdownRef = ref()
|
||||
|
||||
const handleContextMenuVisibleChange = (visible) => {
|
||||
if (!visible) {
|
||||
showContextMenu.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRequestContextMenu = (event, item, buttons) => {
|
||||
showContextMenu.value = false
|
||||
contextMenuItem.value = item
|
||||
contextMenuHandlers.value = buttons || []
|
||||
contextMenuRef.value = {
|
||||
getBoundingClientRect () {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: event.clientY,
|
||||
bottom: event.clientY,
|
||||
left: event.clientX,
|
||||
right: event.clientX
|
||||
}
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
showContextMenu.value = true
|
||||
nextTick(() => {
|
||||
contextMenuDropdownRef.value?.handleOpen?.()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showContextMenu,
|
||||
contextMenuRef,
|
||||
contextMenuHandlers,
|
||||
contextMenuItem,
|
||||
contextMenuDropdownRef,
|
||||
handleContextMenuVisibleChange,
|
||||
handleRequestContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
57
src/hooks/useTabFocus.js
Normal file
57
src/hooks/useTabFocus.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { isNumber } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* Hook: 容器内 Tab / Shift+Tab 自动跳转
|
||||
* @param {import('vue').Ref<HTMLElement>} containerRef - 当前容器 ref
|
||||
* @param {string} selector - 可聚焦元素选择器,默认 input/textarea/select/button
|
||||
*/
|
||||
export function useTabFocus (containerRef, selector) {
|
||||
containerRef = containerRef || ref()
|
||||
selector = selector || 'input:not([disabled]):not([readonly]):not([type=checkbox]):not([type=file]), textarea:not([disabled]):not([readonly])'
|
||||
|
||||
const getFocusable = () => {
|
||||
const containerEl = containerRef.value?.$el || containerRef.value
|
||||
return containerEl ? Array.from(containerEl.querySelectorAll(selector)) : []
|
||||
}
|
||||
|
||||
const focusNext = (currentEl) => {
|
||||
const elements = getFocusable()
|
||||
const index = isNumber(currentEl) ? currentEl : elements.indexOf(currentEl)
|
||||
if (index === -1) return
|
||||
// 优先寻找没有值的输入框
|
||||
let targetIndex = -1
|
||||
const len = elements.length
|
||||
for (let i = 1; i < len; i++) {
|
||||
const idx = (index + i) % len
|
||||
if (!elements[idx].value) {
|
||||
targetIndex = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
// 如果都填了值,则按顺序跳转
|
||||
const nextEl = targetIndex !== -1 ? elements[targetIndex] : (elements[index + 1] || elements[0])
|
||||
nextEl.focus()
|
||||
}
|
||||
|
||||
const focusPrev = (currentEl) => {
|
||||
const elements = getFocusable()
|
||||
const index = isNumber(currentEl) ? currentEl : elements.indexOf(currentEl)
|
||||
if (index === -1) return
|
||||
const prevEl = elements[index - 1] || elements[elements.length - 1]
|
||||
prevEl.focus()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const containerEl = containerRef.value?.$el || containerRef.value
|
||||
if (!containerEl) return
|
||||
containerEl.addEventListener('keydown', e => {
|
||||
const target = e.target
|
||||
if (!target.matches(selector)) return
|
||||
if (e.key === 'Tab' && !e.shiftKey) { e.preventDefault(); focusNext(target) }
|
||||
if (e.key === 'Tab' && e.shiftKey) { e.preventDefault(); focusPrev(target) }
|
||||
})
|
||||
})
|
||||
|
||||
return { containerRef, focusNext, focusPrev }
|
||||
}
|
||||
@@ -67,17 +67,48 @@ common.label.modifier = '修改人'
|
||||
common.label.status = '状态'
|
||||
common.label.statusEnabled = '启用'
|
||||
common.label.statusDisabled = '禁用'
|
||||
common.label.description = '描述'
|
||||
common.label.delay = '延迟'
|
||||
common.label.add = '添加'
|
||||
common.label.paste = '粘贴'
|
||||
common.label.name = '名称'
|
||||
common.label.value = '值'
|
||||
common.label.files = '文件'
|
||||
common.label.input = '文本'
|
||||
common.label.number = '数字'
|
||||
common.label.date = '日期'
|
||||
common.label.dateTime = '日期时间'
|
||||
common.label.file = '文件'
|
||||
common.label.yes = '是'
|
||||
common.label.no = '否'
|
||||
common.label.example = '示例'
|
||||
common.label.params = '参数'
|
||||
|
||||
//= ============通用============
|
||||
common.label.commonCode = '{0}代码'
|
||||
common.label.commonConfig = '配置{0}'
|
||||
common.label.commonEdit = '{0}编辑'
|
||||
common.label.commonAdd = '新增{0}'
|
||||
common.label.commonView = '查看{0}'
|
||||
common.label.commonDelete = '删除{0}'
|
||||
common.label.commonParent = '上级{0}'
|
||||
common.label.commonAdd1 = '添加{0}'
|
||||
common.label.commonCopy = '复制{0}'
|
||||
common.label.commonSwap = '交换{0}'
|
||||
common.label.commonTest = '测试{0}'
|
||||
common.label.commonFormat = '格式化{0}'
|
||||
common.label.commonSave = '保存{0}'
|
||||
common.label.commonExpand = '展开{0}'
|
||||
common.label.commonCollapse = '收起{0}'
|
||||
common.label.commonEnable = '启用{0}'
|
||||
common.label.commonDisable = '禁用{0}'
|
||||
common.label.commonDelay = '延迟{0}毫秒'
|
||||
common.label.commonDownload = '下载{0}'
|
||||
common.label.commonExport = '导出为{0}'
|
||||
common.label.commonGenerate = '生成{0}'
|
||||
common.label.commonClear = '清空{0}'
|
||||
common.label.commonBack = '回到{0}'
|
||||
common.label.commonSelect = '选择{0}'
|
||||
|
||||
//* =======================msg=====================//
|
||||
common.msg.nonNull = '{0}不能为空'
|
||||
@@ -107,3 +138,6 @@ common.msg.notFound = '页面不存在,请访问其他页面'
|
||||
common.msg.accessDenied = '没有权限访问该页面'
|
||||
common.msg.emailError = '邮箱格式不正确'
|
||||
common.msg.cannotEnterChineseCharacters = '{0}不能输入中文!'
|
||||
common.msg.copySuccess = '复制成功。'
|
||||
common.msg.copyError = '复制失败或当前环境不支持!'
|
||||
common.msg.pasteToProcess = '粘贴文本、JSON或URL参数自动解析'
|
||||
|
||||
@@ -67,17 +67,48 @@ common.label.modifier = 'Modifier'
|
||||
common.label.status = 'Status'
|
||||
common.label.statusEnabled = 'Enabled'
|
||||
common.label.statusDisabled = 'Disabled'
|
||||
common.label.description = 'Description'
|
||||
common.label.delay = 'Delay'
|
||||
common.label.add = 'Add'
|
||||
common.label.paste = 'Paste'
|
||||
common.label.name = 'Name'
|
||||
common.label.value = 'Value'
|
||||
common.label.files = 'Files'
|
||||
common.label.input = 'Text'
|
||||
common.label.number = 'Number'
|
||||
common.label.date = 'Date'
|
||||
common.label.dateTime = 'Date Time'
|
||||
common.label.file = 'File'
|
||||
common.label.yes = 'Yes'
|
||||
common.label.no = 'No'
|
||||
common.label.example = 'Example'
|
||||
common.label.params = 'Params'
|
||||
|
||||
//= ============通用============
|
||||
common.label.commonConfig = 'Config {0}'
|
||||
common.label.commonCode = '{0} Code'
|
||||
common.label.commonEdit = '{0} Edit'
|
||||
common.label.commonAdd = 'Add {0}'
|
||||
common.label.commonView = 'View {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}'
|
||||
common.label.commonTest = 'Test {0}'
|
||||
common.label.commonFormat = 'Format {0}'
|
||||
common.label.commonSave = 'Save {0}'
|
||||
common.label.commonExpand = 'Expand {0}'
|
||||
common.label.commonCollapse = 'Collapse {0}'
|
||||
common.label.commonEnable = 'Enable {0}'
|
||||
common.label.commonDisable = 'Disable {0}'
|
||||
common.label.commonDelay = 'Delay {0}ms'
|
||||
common.label.commonDownload = 'Download {0}'
|
||||
common.label.commonExport = 'Export as {0}'
|
||||
common.label.commonGenerate = 'Generate {0}'
|
||||
common.label.commonClear = 'Clear {0}'
|
||||
common.label.commonBack = 'Back to {0}'
|
||||
common.label.commonSelect = 'Select {0}'
|
||||
|
||||
//* =======================msg=====================//
|
||||
common.msg.nonNull = '{0} is required.'
|
||||
@@ -107,3 +138,6 @@ common.msg.notFound = 'Page not found, please visit other pages!'
|
||||
common.msg.accessDenied = 'Page access denied'
|
||||
common.msg.emailError = 'E-mail format is invalid.'
|
||||
common.msg.cannotEnterChineseCharacters = '{0} cannot enter Chinese characters.'
|
||||
common.msg.copySuccess = 'Copied Successfully.'
|
||||
common.msg.copyError = 'Copy failed or not supported!'
|
||||
common.msg.pasteToProcess = 'Paste text, JSON or URL params to parse'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { markRaw, ref } from 'vue'
|
||||
import { isObject, isArray, set, isNumber } from 'lodash-es'
|
||||
import { markRaw, ref, nextTick } from 'vue'
|
||||
import { isObject, isArray, set, isNumber, isFunction, isBoolean, isString, get, cloneDeep } from 'lodash-es'
|
||||
import { ElLoading, ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import numeral from 'numeral'
|
||||
@@ -12,7 +12,7 @@ import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||
import { LoadSaveParamMode } from '@/consts/GlobalConstants'
|
||||
import { GlobalLayoutMode, LoadSaveParamMode } from '@/consts/GlobalConstants'
|
||||
|
||||
export const useSystemKey = () => {
|
||||
return SYSTEM_KEY
|
||||
@@ -284,6 +284,21 @@ export const $coreConfirm = (message, title = $i18nBundle('common.label.reminder
|
||||
options)
|
||||
}
|
||||
|
||||
export const $corePrompt = (message, title = $i18nBundle('common.label.reminder'), options = undefined) => {
|
||||
if (isObject(title) && !options) {
|
||||
options = title
|
||||
title = null
|
||||
}
|
||||
options = Object.assign({
|
||||
dangerouslyUseHTMLString: true,
|
||||
draggable: true,
|
||||
customClass: 'common-message-confirm'
|
||||
}, options || {})
|
||||
return ElMessageBox.prompt(message,
|
||||
title || $i18nBundle('common.label.reminder'),
|
||||
options)
|
||||
}
|
||||
|
||||
export const $formatNumber = (value, format) => {
|
||||
return numeral(value).format(format)
|
||||
}
|
||||
@@ -304,12 +319,12 @@ export const $currencyShort = (value, prefix) => {
|
||||
return `${prefix || '¥'} ${$formatNumber(value, '0,0.[00]')}`
|
||||
}
|
||||
/**
|
||||
* @typedef {{text:string, success?:string, error?:string}} CopyTextConfig
|
||||
* @typedef {{text:string, success?:string, error?:string, successKey?:string, errorKey?:string}} CopyTextConfig
|
||||
* @type {CopyTextConfig}
|
||||
*/
|
||||
const defaultCopyConfig = {
|
||||
success: 'Copied Successfully!',
|
||||
error: 'Copy Not supported!'
|
||||
successKey: 'common.msg.copySuccess',
|
||||
errorKey: 'common.msg.copyError'
|
||||
}
|
||||
/**
|
||||
* @param text {string | CopyTextConfig} 需要复制的文本
|
||||
@@ -328,12 +343,12 @@ export const $copyText = (text) => {
|
||||
if (isSupported) {
|
||||
copy(config.text)
|
||||
ElMessage({
|
||||
message: config.success,
|
||||
message: config.success || $i18nBundle(config.successKey),
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
message: config.error,
|
||||
message: config.error || $i18nBundle(config.errorKey),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
@@ -341,6 +356,213 @@ export const $copyText = (text) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const checkShowColumn = (dataList, field) => {
|
||||
let hasFieldData = false
|
||||
dataList = dataList || []
|
||||
let checkFun = (item, f) => {
|
||||
const value = get(item, f)
|
||||
return isNumber(value) || isBoolean(value) || !!value
|
||||
}
|
||||
if (isFunction(field)) {
|
||||
checkFun = field
|
||||
}
|
||||
for (const item of dataList) {
|
||||
if (checkFun(item, field)) {
|
||||
hasFieldData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return hasFieldData
|
||||
}
|
||||
|
||||
export const calcAffixOffset = (fix = 10) => {
|
||||
let initValue = 60 + fix
|
||||
useGlobalConfigStore().layoutMode === GlobalLayoutMode.TOP && useGlobalConfigStore().isShowBreadcrumb && (initValue += 40)
|
||||
useTabsViewStore().isTabMode && (initValue += 48)
|
||||
return initValue
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表数据解析成树结构
|
||||
* @template T
|
||||
* @param items {T[]}数据集
|
||||
* @param parent {T=} 上级,第一次调用都传null
|
||||
* @param config {Object=}配置信息
|
||||
* @param config.parentKey {string} 父级字段名
|
||||
* @param config.valueKey {string} 当前主字段名
|
||||
* @param config.pre {(item) => void} 前置处理,改变item的属性值
|
||||
* @param config.after {(item) => void} 后置处理,改变item的属性值
|
||||
* @returns {T[]}
|
||||
*/
|
||||
export const processTreeData = (items, parent, config) => {
|
||||
const results = []
|
||||
const { parentKey, valueKey, clone } = Object.assign({
|
||||
parentKey: 'parentId',
|
||||
valueKey: 'id',
|
||||
clone: true
|
||||
}, config || {})
|
||||
items.forEach(current => {
|
||||
if (!parent) {
|
||||
if (!current[parentKey]) { // 根节点
|
||||
const currentNode = clone ? cloneDeep(current) : current
|
||||
results.push(currentNode)
|
||||
isFunction(config?.pre) && config.pre(currentNode)
|
||||
processTreeData(items, currentNode, config)
|
||||
isFunction(config?.after) && config.after(currentNode)
|
||||
}
|
||||
} else {
|
||||
if (current[parentKey] === parent[valueKey]) {
|
||||
const currentNode = clone ? cloneDeep(current) : current
|
||||
parent.children = parent.children || []
|
||||
parent.children.push(currentNode)
|
||||
currentNode.parent = parent
|
||||
isFunction(config?.pre) && config.pre(currentNode)
|
||||
processTreeData(items, currentNode, config)
|
||||
isFunction(config?.after) && config.after(currentNode)
|
||||
}
|
||||
}
|
||||
return parent
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 产生随机字符,数字加字母
|
||||
* @param len
|
||||
* @returns {string}
|
||||
*/
|
||||
export const $randomStr = (len = 8) => {
|
||||
const str = Math.random().toString(36).substring(2)
|
||||
if (str.length >= len) {
|
||||
return str.substring(0, len)
|
||||
}
|
||||
return str + $randomStr(len - str.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载链接点击
|
||||
* @param downloadUrl
|
||||
*/
|
||||
export const $downloadWithLinkClick = (downloadUrl) => {
|
||||
const downloadLink = document.createElement('a')
|
||||
downloadLink.href = downloadUrl
|
||||
downloadLink.download = 'download'
|
||||
downloadLink.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空并设置
|
||||
* @param valueRef
|
||||
* @param newValue
|
||||
* @param emptyValue
|
||||
*/
|
||||
export const clearAndSetValue = (valueRef, newValue, emptyValue) => {
|
||||
valueRef.value = emptyValue
|
||||
return nextTick(() => {
|
||||
valueRef.value = newValue
|
||||
})
|
||||
}
|
||||
|
||||
export const getStyleGrow = flexGrow => ({
|
||||
flexGrow,
|
||||
minWidth: `calc(${flexGrow}0%)`
|
||||
})
|
||||
|
||||
/**
|
||||
* 检查是否包含数组中的任意一个子串(忽略大小写)
|
||||
* @param {string|string[]} strOrArr - 要检查的字符串或数组
|
||||
* @param {...string|string[]} searchItems - 要搜索的数据,可以是数组或可变参数
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const includesAnyIgnoreCase = (strOrArr, ...searchItems) => {
|
||||
if (!strOrArr) return false
|
||||
const searches = isArray(searchItems[0]) ? searchItems[0] : searchItems
|
||||
if (!searches || searches.length === 0) return false
|
||||
if (isArray(strOrArr)) {
|
||||
strOrArr = strOrArr.filter(s => !!s).map(s => isString(s) ? s.toLowerCase() : s)
|
||||
} else {
|
||||
strOrArr = strOrArr.toLowerCase()
|
||||
}
|
||||
return searches.some(item => item && strOrArr.includes(isString(item) ? item.toLowerCase() : item))
|
||||
}
|
||||
|
||||
export const DATE_FAST_REG = /^\d{4}-\d{2}-\d{2}(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})?)?$/
|
||||
|
||||
export const HAS_TIME_RE = /[ T]\d{2}:\d{2}/
|
||||
|
||||
export const isDateString = (val) => {
|
||||
if (!isString(val)) return false
|
||||
if (val.length < 10 || val.length > 35) return false
|
||||
if (!DATE_FAST_REG.test(val)) return false
|
||||
return dayjs(val).isValid()
|
||||
}
|
||||
|
||||
export const formatDateSmart = (val) => {
|
||||
const format = HAS_TIME_RE.test(val)
|
||||
? 'YYYY-MM-DD HH:mm:ss'
|
||||
: 'YYYY-MM-DD'
|
||||
return formatDate(val, format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小为易读字符串 (B, KB, MB, GB, TB)
|
||||
* @param {number} bytes 字节数
|
||||
* @param {number} precision 保留小数位数,默认2
|
||||
* @returns {string}
|
||||
*/
|
||||
export const formatFileSize = (bytes, precision = 2) => {
|
||||
if (!bytes || bytes <= 0 || isNaN(bytes)) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const k = 1024
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), units.length - 1)
|
||||
const val = bytes / Math.pow(k, i)
|
||||
const formatted = val % 1 === 0 ? val.toString() : val.toFixed(precision)
|
||||
return `${formatted} ${units[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索/输入建议生成函数
|
||||
* @param keySuggestions
|
||||
* @returns {(function(*, *): void)|*}
|
||||
*/
|
||||
export const calcSuggestionsFunc = (keySuggestions) => {
|
||||
if (isFunction(keySuggestions)) {
|
||||
return keySuggestions
|
||||
} else if (isArray(keySuggestions)) {
|
||||
return (queryString, cb) => {
|
||||
const dataList = keySuggestions.map(value => isObject(value) ? value : ({ value }))
|
||||
.filter(item => {
|
||||
let valueStr = item?.value ?? ''
|
||||
valueStr = isString(valueStr) ? valueStr : valueStr.toString()
|
||||
return valueStr.toLowerCase?.().includes(queryString?.toLowerCase())
|
||||
})
|
||||
cb(dataList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组合多个Suggestions配置
|
||||
* @param args
|
||||
* @returns {(function(*, *): void)|*}
|
||||
*/
|
||||
export const concatValueSuggestions = (...args) => {
|
||||
const suggestionsArr = args?.filter(suggestions => !!suggestions)
|
||||
if (suggestionsArr?.length) {
|
||||
return (queryString, cb) => {
|
||||
const dataList = []
|
||||
suggestionsArr.forEach(suggestions => {
|
||||
const callback = items => isArray(items) && dataList.push(...items)
|
||||
const suggestionsFunc = calcSuggestionsFunc(suggestions)
|
||||
if (suggestionsFunc) {
|
||||
suggestionsFunc(queryString, callback)
|
||||
}
|
||||
})
|
||||
cb(dataList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
install (app) {
|
||||
router = app.config.globalProperties.$router
|
||||
@@ -354,6 +576,7 @@ export default {
|
||||
$number,
|
||||
$currency,
|
||||
$currencyShort,
|
||||
$formatFileSize: formatFileSize,
|
||||
$coreShowLoading,
|
||||
$coreHideLoading,
|
||||
$coreAlert,
|
||||
@@ -361,8 +584,10 @@ export default {
|
||||
$coreWarning,
|
||||
$coreError,
|
||||
$coreConfirm,
|
||||
$corePrompt,
|
||||
$openNewWin,
|
||||
$openWin
|
||||
$openWin,
|
||||
$randomStr
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
705
src/views/components/utils/CommonParamsEdit.vue
Normal file
705
src/views/components/utils/CommonParamsEdit.vue
Normal file
@@ -0,0 +1,705 @@
|
||||
<script setup lang="jsx">
|
||||
import { defineFormOptions } from '@/components/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
import { $copyText, toFlatKeyValue, calcSuggestionsFunc, concatValueSuggestions } from '@/utils'
|
||||
import { $i18nBundle, $i18nKey } from '@/messages'
|
||||
import { ElMessage, ElButton } from 'element-plus'
|
||||
import { isFunction, cloneDeep } from 'lodash-es'
|
||||
import { useRenderKey, useSortableParams } from '@/hooks/CommonHooks'
|
||||
import { useTabFocus } from '@/hooks/useTabFocus'
|
||||
|
||||
const props = defineProps({
|
||||
formProp: {
|
||||
type: String,
|
||||
default: 'requestParams'
|
||||
},
|
||||
nameReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nameRequired: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
valueReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
valueRequired: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showEnableSwitch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showAddButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showCopyButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showPasteButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showRemoveButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showValueConfig: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
nameKey: {
|
||||
type: String,
|
||||
default: 'name'
|
||||
},
|
||||
valueKey: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
nameSuggestions: {
|
||||
type: [Array, Function],
|
||||
default: () => []
|
||||
},
|
||||
valueSuggestions: {
|
||||
type: [Array, Function],
|
||||
default: () => []
|
||||
},
|
||||
nameDynamicOption: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
valueDynamicOption: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
nameSpan: {
|
||||
type: Number,
|
||||
default: 8
|
||||
},
|
||||
valueSpan: {
|
||||
type: Number,
|
||||
default: 8
|
||||
},
|
||||
fileFlag: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
singleEnable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const params = defineModel('modelValue', {
|
||||
type: Array,
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const VALUE_TYPE_INPUT = 'input'
|
||||
const VALUE_TYPE_NUMBER = 'number'
|
||||
const VALUE_TYPE_DATE = 'date'
|
||||
const VALUE_TYPE_DATETIME = 'datetime'
|
||||
const VALUE_TYPE_FILE = 'file'
|
||||
const LEGACY_VALUE_TYPE_INPUT = 'text'
|
||||
const VALUE_TYPE_OPTIONS = [{
|
||||
value: VALUE_TYPE_INPUT,
|
||||
labelKey: 'common.label.input'
|
||||
}, {
|
||||
value: VALUE_TYPE_NUMBER,
|
||||
labelKey: 'common.label.number'
|
||||
}, {
|
||||
value: VALUE_TYPE_DATE,
|
||||
labelKey: 'common.label.date'
|
||||
}, {
|
||||
value: VALUE_TYPE_DATETIME,
|
||||
labelKey: 'common.label.dateTime'
|
||||
}, {
|
||||
value: VALUE_TYPE_FILE,
|
||||
labelKey: 'common.label.file'
|
||||
}]
|
||||
|
||||
const getParamMeta = (param) => {
|
||||
param.meta = param.meta || {}
|
||||
return param.meta
|
||||
}
|
||||
|
||||
const normalizeParamValueType = (param) => {
|
||||
const meta = param.meta || {}
|
||||
const valueType = meta.type ?? param.type
|
||||
let result = valueType
|
||||
if (!valueType || valueType === LEGACY_VALUE_TYPE_INPUT) {
|
||||
result = VALUE_TYPE_INPUT
|
||||
}
|
||||
if (param.type && meta.type !== result) {
|
||||
getParamMeta(param).type = result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const isFileParam = param => normalizeParamValueType(param) === VALUE_TYPE_FILE
|
||||
|
||||
const isInputParam = param => normalizeParamValueType(param) === VALUE_TYPE_INPUT
|
||||
|
||||
const isNumberParam = param => normalizeParamValueType(param) === VALUE_TYPE_NUMBER
|
||||
|
||||
const isDateParam = param => [VALUE_TYPE_DATE, VALUE_TYPE_DATETIME].includes(normalizeParamValueType(param))
|
||||
|
||||
const getDateValueFormat = type => type === VALUE_TYPE_DATETIME ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'
|
||||
|
||||
params.value.forEach(param => {
|
||||
param.enabled = param.enabled ?? true
|
||||
})
|
||||
|
||||
const addRequestParam = () => {
|
||||
params.value.push({
|
||||
enabled: !props.singleEnable || !params.value.filter(param => param.enabled).length,
|
||||
meta: {
|
||||
type: VALUE_TYPE_INPUT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const validParams = computed(() => {
|
||||
return params.value.filter(param => !!param.name && !isFileParam(param))
|
||||
})
|
||||
|
||||
const copyParams = () => $copyText(JSON.stringify(validParams.value))
|
||||
|
||||
const calcPasteParams = value => {
|
||||
let calcParams = []
|
||||
if (value.startsWith('{')) { // json
|
||||
try {
|
||||
let objValue = JSON.parse(value)
|
||||
if (objValue != null) {
|
||||
objValue = toFlatKeyValue(objValue)
|
||||
calcParams = Object.keys(objValue).map(key => {
|
||||
return {
|
||||
enabled: true,
|
||||
[props.nameKey]: key,
|
||||
[props.valueKey]: objValue[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message)
|
||||
}
|
||||
} else if (value.startsWith('[')) {
|
||||
calcParams = JSON.parse(value)
|
||||
} else {
|
||||
if (value.indexOf('?') > -1) {
|
||||
value = value.slice(value.indexOf('?') + 1)
|
||||
}
|
||||
calcParams = new URLSearchParams(value).entries().map(entry => {
|
||||
return {
|
||||
enabled: true,
|
||||
[props.nameKey]: entry[0],
|
||||
[props.valueKey]: entry[1]
|
||||
}
|
||||
})
|
||||
}
|
||||
return calcParams
|
||||
}
|
||||
|
||||
const showTextModel = ref(false)
|
||||
const inputTextModel = ref({
|
||||
text: ''
|
||||
})
|
||||
const inputTextOption = {
|
||||
tooltip: $i18nBundle('common.msg.pasteToProcess'),
|
||||
prop: 'text',
|
||||
labelWidth: '40px',
|
||||
attrs: {
|
||||
type: 'textarea'
|
||||
},
|
||||
change (value) {
|
||||
if (value) {
|
||||
const calcParams = calcPasteParams(value)
|
||||
params.value = [...calcParams]
|
||||
inputTextModel.value.text = ''
|
||||
showTextModel.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showValueSuggestionsDialog = ref(false)
|
||||
const valueSuggestionsModel = ref({
|
||||
readonlyItems: [],
|
||||
items: []
|
||||
})
|
||||
const currentValueSuggestionsParam = ref()
|
||||
|
||||
const valueTypeOptions = computed(() => {
|
||||
return VALUE_TYPE_OPTIONS.filter(option => props.fileFlag || option.value !== VALUE_TYPE_FILE)
|
||||
})
|
||||
|
||||
const normalizeValueSuggestion = (item) => {
|
||||
if (item && typeof item === 'object') {
|
||||
return {
|
||||
value: item.value,
|
||||
description: item.description
|
||||
}
|
||||
}
|
||||
return {
|
||||
value: item
|
||||
}
|
||||
}
|
||||
|
||||
const hasValueSuggestionText = value => value !== undefined && value !== ''
|
||||
|
||||
const formatValueSuggestion = (item = {}) => {
|
||||
if (item.description && hasValueSuggestionText(item.value)) {
|
||||
return `${item.value} - ${item.description}`
|
||||
}
|
||||
return item.description || item.value || ''
|
||||
}
|
||||
|
||||
const openValueSuggestions = (item) => {
|
||||
currentValueSuggestionsParam.value = item
|
||||
valueSuggestionsModel.value.readonlyItems = (Array.isArray(item.valueSuggestions) ? item.valueSuggestions : [])
|
||||
.map(normalizeValueSuggestion)
|
||||
valueSuggestionsModel.value.items = (Array.isArray(item.meta?.valueSuggestions) ? item.meta.valueSuggestions : [])
|
||||
.map(normalizeValueSuggestion)
|
||||
showValueSuggestionsDialog.value = true
|
||||
}
|
||||
|
||||
const addValueSuggestion = () => {
|
||||
valueSuggestionsModel.value.items.push({})
|
||||
}
|
||||
|
||||
const valueSuggestionItems = computed(() => [
|
||||
...valueSuggestionsModel.value.readonlyItems.map(item => ({
|
||||
...item,
|
||||
readonly: true
|
||||
})),
|
||||
...valueSuggestionsModel.value.items.map((item, index) => ({
|
||||
...item,
|
||||
index,
|
||||
readonly: false
|
||||
}))
|
||||
])
|
||||
|
||||
const saveValueSuggestions = () => {
|
||||
const valueSuggestions = valueSuggestionsModel.value.items
|
||||
.map(normalizeValueSuggestion)
|
||||
.filter(item => hasValueSuggestionText(item.value) || item.description)
|
||||
.map(item => {
|
||||
const suggestion = {}
|
||||
if (item.description) {
|
||||
suggestion.description = item.description
|
||||
}
|
||||
if (hasValueSuggestionText(item.value)) {
|
||||
suggestion.value = item.value
|
||||
}
|
||||
return suggestion
|
||||
})
|
||||
if (currentValueSuggestionsParam.value) {
|
||||
const meta = getParamMeta(currentValueSuggestionsParam.value)
|
||||
meta.valueSuggestions = valueSuggestions
|
||||
}
|
||||
}
|
||||
|
||||
const hasMetaConfig = item => {
|
||||
return !!item.meta?.valueSuggestions?.length
|
||||
}
|
||||
|
||||
const getValueOption = (param, paramValueSuggestions, nvSpan) => {
|
||||
const valueType = normalizeParamValueType(param)
|
||||
const option = {
|
||||
labelKey: 'common.label.value',
|
||||
prop: props.valueKey,
|
||||
required: props.nameReadOnly || props.valueRequired || param.valueRequired,
|
||||
colSpan: props.valueSpan || nvSpan,
|
||||
disabled: props.valueReadOnly,
|
||||
enabled: !isFileParam(param)
|
||||
}
|
||||
if (isDateParam(param)) {
|
||||
const valueFormat = getDateValueFormat(valueType)
|
||||
return {
|
||||
...option,
|
||||
type: 'date-picker',
|
||||
attrs: {
|
||||
type: valueType,
|
||||
format: valueFormat,
|
||||
valueFormat,
|
||||
style: {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isNumberParam(param)) {
|
||||
return {
|
||||
...option,
|
||||
type: 'input-number',
|
||||
attrs: {
|
||||
controlsPosition: 'right',
|
||||
style: {
|
||||
width: '100%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...option,
|
||||
type: paramValueSuggestions ? 'autocomplete' : 'input',
|
||||
attrs: {
|
||||
fetchSuggestions: paramValueSuggestions,
|
||||
triggerOnFocus: true
|
||||
},
|
||||
slots: paramValueSuggestions
|
||||
? {
|
||||
default: ({ item }) => formatValueSuggestion(item)
|
||||
}
|
||||
: undefined,
|
||||
dynamicOption: (item, ...args) => {
|
||||
if (isFunction(item.dynamicOption)) {
|
||||
return item.dynamicOption(item, ...args)
|
||||
}
|
||||
if (isFunction(props.valueDynamicOption)) {
|
||||
return props.valueDynamicOption(item, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const calcSuggestions = (key = 'name') => {
|
||||
const keySuggestions = props[`${key}Suggestions`]
|
||||
return calcSuggestionsFunc(keySuggestions)
|
||||
}
|
||||
|
||||
const paramsOptions = computed(() => {
|
||||
const nameSuggestions = calcSuggestions('name')
|
||||
const valueSuggestions = calcSuggestions('value')
|
||||
return params.value.map((param) => {
|
||||
const nvSpan = props.showEnableSwitch ? 8 : 10
|
||||
const paramValueSuggestions = concatValueSuggestions(
|
||||
param.meta?.valueSuggestions,
|
||||
param.valueSuggestions,
|
||||
valueSuggestions
|
||||
)
|
||||
return defineFormOptions([{
|
||||
labelWidth: '30px',
|
||||
prop: 'enabled',
|
||||
disabled: props.nameReadOnly,
|
||||
enabled: props.showEnableSwitch,
|
||||
type: 'switch',
|
||||
colSpan: 2,
|
||||
dynamicOption (item) {
|
||||
if (props.singleEnable) {
|
||||
return {
|
||||
change (value) {
|
||||
if (value) {
|
||||
params.value.filter(p => p !== item).forEach(p => (p.enabled = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
labelKey: 'common.label.name',
|
||||
prop: props.nameKey,
|
||||
required: props.nameReadOnly || props.nameRequired || param.nameRequired || param.valueRequired,
|
||||
disabled: props.nameReadOnly,
|
||||
colSpan: props.nameSpan || nvSpan,
|
||||
type: nameSuggestions ? 'autocomplete' : 'input',
|
||||
attrs: {
|
||||
fetchSuggestions: nameSuggestions,
|
||||
triggerOnFocus: false
|
||||
},
|
||||
dynamicOption: (item, ...args) => {
|
||||
if (isFunction(item.dynamicOption)) {
|
||||
return item.dynamicOption(item, ...args)
|
||||
}
|
||||
if (isFunction(props.nameDynamicOption)) {
|
||||
return props.nameDynamicOption(item, ...args)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
labelWidth: '1px',
|
||||
prop: 'meta.type',
|
||||
type: 'select',
|
||||
value: VALUE_TYPE_INPUT,
|
||||
children: valueTypeOptions.value,
|
||||
attrs: {
|
||||
clearable: false,
|
||||
style: {
|
||||
paddingTop: '2px'
|
||||
}
|
||||
},
|
||||
enabled: props.showValueConfig && valueTypeOptions.value.length > 1,
|
||||
colSpan: 3,
|
||||
change (value) {
|
||||
param[props.valueKey] = value === VALUE_TYPE_FILE ? [] : (value === VALUE_TYPE_NUMBER ? undefined : '')
|
||||
}
|
||||
}, getValueOption(param, isInputParam(param) ? paramValueSuggestions : undefined, nvSpan), {
|
||||
labelKey: 'common.label.files',
|
||||
type: 'upload',
|
||||
enabled: props.fileFlag && isFileParam(param),
|
||||
attrs: {
|
||||
fileList: param[props.valueKey],
|
||||
'onUpdate:fileList': (files) => {
|
||||
param[props.valueKey] = files
|
||||
},
|
||||
showFileList: true,
|
||||
autoUpload: false
|
||||
},
|
||||
slots: {
|
||||
trigger () {
|
||||
return <ElButton type="primary" size="small">{$i18nBundle('common.label.select')}</ElButton>
|
||||
}
|
||||
},
|
||||
colSpan: 6
|
||||
}])
|
||||
})
|
||||
})
|
||||
|
||||
const { sortableRef, hoverIndex, dragging } = useSortableParams(params, '.common-params-item')
|
||||
|
||||
const { renderKey } = useRenderKey()
|
||||
|
||||
useTabFocus(sortableRef)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container
|
||||
ref="sortableRef"
|
||||
class="flex-column common-params-edit"
|
||||
style="line-height: normal;"
|
||||
>
|
||||
<el-row
|
||||
v-for="(item, index) in params"
|
||||
:key="renderKey(item)"
|
||||
class="padding-bottom2 common-params-item"
|
||||
@mouseenter="hoverIndex=index"
|
||||
@mouseleave="hoverIndex=-1"
|
||||
>
|
||||
<template
|
||||
v-for="(option, idx) in paramsOptions[index]"
|
||||
:key="`${index}_${option.prop}_${option.type}`"
|
||||
>
|
||||
<el-col
|
||||
v-if="option.enabled!==false"
|
||||
:span="option.colSpan"
|
||||
>
|
||||
<common-form-control
|
||||
label-width="80px"
|
||||
:model="item"
|
||||
:option="option"
|
||||
:prop="`${formProp}.${index}.${option.prop}`"
|
||||
>
|
||||
<template #beforeLabel>
|
||||
<common-icon
|
||||
v-if="idx===0&&hoverIndex===index&&!dragging"
|
||||
:size="20"
|
||||
class="margin-top1 move-indicator"
|
||||
icon="DragIndicatorFilled"
|
||||
style="cursor: move;"
|
||||
/>
|
||||
</template>
|
||||
</common-form-control>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col
|
||||
:span="3"
|
||||
class="padding-left2 padding-top1 common-params-actions"
|
||||
>
|
||||
<el-button
|
||||
v-if="item.array"
|
||||
type="success"
|
||||
size="small"
|
||||
circle
|
||||
@click="params.splice(index + 1, 0, cloneDeep(item))"
|
||||
>
|
||||
<common-icon icon="Plus" />
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showRemoveButton&&!nameReadOnly&&!valueReadOnly"
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click="params.splice(index, 1)"
|
||||
>
|
||||
<common-icon icon="Delete" />
|
||||
</el-button>
|
||||
<el-tooltip
|
||||
v-if="props.showValueConfig && isInputParam(item)"
|
||||
:content="$i18nKey('common.label.commonConfig', 'common.label.value')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
:type="hasMetaConfig(item)?'warning':'info'"
|
||||
size="small"
|
||||
circle
|
||||
@click="openValueSuggestions(item)"
|
||||
>
|
||||
<common-icon icon="Setting" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col
|
||||
v-if="$slots.item"
|
||||
:span="24"
|
||||
>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
:index="index"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col>
|
||||
<el-button
|
||||
v-if="showAddButton&&!nameReadOnly&&!valueReadOnly"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="addRequestParam()"
|
||||
>
|
||||
<common-icon
|
||||
class="margin-right1"
|
||||
icon="Plus"
|
||||
/>
|
||||
{{ $t('common.label.add') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showCopyButton&&validParams?.length"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="copyParams()"
|
||||
>
|
||||
<common-icon
|
||||
class="margin-right1"
|
||||
icon="DocumentCopy"
|
||||
/>
|
||||
{{ $i18nKey('common.label.commonCopy', 'common.label.params') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showPasteButton&&!nameReadOnly&&!valueReadOnly"
|
||||
:type="showTextModel?'success':'info'"
|
||||
size="small"
|
||||
@click="showTextModel=!showTextModel"
|
||||
>
|
||||
<common-icon
|
||||
class="margin-right1"
|
||||
icon="ContentPasteGoFilled"
|
||||
/>
|
||||
{{ $t('common.label.paste') }}
|
||||
</el-button>
|
||||
<common-form-control
|
||||
v-if="showTextModel"
|
||||
class="padding-top2"
|
||||
:model="inputTextModel"
|
||||
:option="inputTextOption"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<common-window
|
||||
v-model="showValueSuggestionsDialog"
|
||||
:title="$i18nKey('common.label.commonConfig', 'common.label.value')"
|
||||
width="650px"
|
||||
:ok-click="saveValueSuggestions"
|
||||
>
|
||||
<el-container class="flex-column value-suggestions-window">
|
||||
<el-row class="padding-bottom2 value-suggestions-header">
|
||||
<el-col :span="10">
|
||||
{{ $t('common.label.value') }}
|
||||
</el-col>
|
||||
<el-col
|
||||
:span="11"
|
||||
class="padding-left2"
|
||||
>
|
||||
{{ $t('common.label.description') }}
|
||||
</el-col>
|
||||
<el-col :span="3" />
|
||||
</el-row>
|
||||
<el-row
|
||||
v-for="(suggestion, index) in valueSuggestionItems"
|
||||
:key="`${suggestion.readonly?'readonly':'custom'}_${index}`"
|
||||
class="padding-bottom2"
|
||||
>
|
||||
<el-col :span="10">
|
||||
<el-input
|
||||
v-if="suggestion.readonly"
|
||||
:model-value="suggestion.value"
|
||||
disabled
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="valueSuggestionsModel.items[suggestion.index].value"
|
||||
clearable
|
||||
/>
|
||||
</el-col>
|
||||
<el-col
|
||||
:span="11"
|
||||
class="padding-left2"
|
||||
>
|
||||
<el-input
|
||||
v-if="suggestion.readonly"
|
||||
:model-value="suggestion.description"
|
||||
disabled
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="valueSuggestionsModel.items[suggestion.index].description"
|
||||
clearable
|
||||
/>
|
||||
</el-col>
|
||||
<el-col
|
||||
:span="3"
|
||||
class="padding-left2"
|
||||
>
|
||||
<el-button
|
||||
v-if="!suggestion.readonly"
|
||||
type="danger"
|
||||
size="small"
|
||||
circle
|
||||
@click="valueSuggestionsModel.items.splice(suggestion.index, 1)"
|
||||
>
|
||||
<common-icon icon="Delete" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="addValueSuggestion()"
|
||||
>
|
||||
<common-icon
|
||||
class="margin-right1"
|
||||
icon="Plus"
|
||||
/>
|
||||
{{ $t('common.label.add') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-container>
|
||||
</common-window>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.common-params-edit :deep(.common-params-actions) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.common-params-edit :deep(.common-params-actions .el-tooltip) {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.value-suggestions-window {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.value-suggestions-header {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -44,8 +44,8 @@ const chartConfig = {
|
||||
<el-container class="container-center">
|
||||
<v-chart
|
||||
v-if="chartConfig"
|
||||
class="chart"
|
||||
:key="theme"
|
||||
class="chart"
|
||||
:theme="theme"
|
||||
:option="chartConfig"
|
||||
autoresize
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCityAutocompleteConfig, useCitySelectPageConfig } from '@/services/c
|
||||
import { $i18nMsg } from '@/messages'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { defineFormOptions } from '@/components/utils'
|
||||
import { useFormStatus, useFormDelay } from '@/consts/GlobalConstants'
|
||||
|
||||
const defaultCity = ref({})
|
||||
|
||||
@@ -67,6 +68,10 @@ const formOptions = computed(() => {
|
||||
value: '',
|
||||
placeholder: '请输入用户名',
|
||||
required: true,
|
||||
tooltips: [
|
||||
{ tooltip: '用户名在2-6位之间' },
|
||||
{ tooltip: '点击查看帮助', tooltipFunc: () => ElMessage.info('用户名规则帮助') }
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
min: 2,
|
||||
@@ -182,12 +187,29 @@ const formOptions = computed(() => {
|
||||
maxlength: 100,
|
||||
showWordLimit: true
|
||||
}
|
||||
},
|
||||
useFormStatus('status'),
|
||||
useFormDelay('delay'),
|
||||
{
|
||||
label: '扩展参数',
|
||||
prop: 'extraParams',
|
||||
type: 'common-object',
|
||||
value: { env: 'production', role: 'admin' }
|
||||
}])
|
||||
})
|
||||
const userDto = ref({
|
||||
status: 1,
|
||||
delay: 200,
|
||||
extraParams: {
|
||||
env: 'production',
|
||||
role: 'admin'
|
||||
},
|
||||
contacts: [{
|
||||
name: 'Jerry',
|
||||
phone: '1234567890'
|
||||
}, {
|
||||
name: 'Tom',
|
||||
phone: '1987654321'
|
||||
}]
|
||||
})
|
||||
/**
|
||||
@@ -204,10 +226,10 @@ const contactsOptions = ref([{
|
||||
}])
|
||||
|
||||
const addContact = () => {
|
||||
if (userDto.value.contacts.length < 3) {
|
||||
if (userDto.value.contacts.length < 5) {
|
||||
userDto.value.contacts.push({})
|
||||
} else {
|
||||
ElMessage.error('联系人不能超过3个')
|
||||
ElMessage.error('联系人不能超过5个')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,12 +238,11 @@ const deleteContact = idx => {
|
||||
}
|
||||
|
||||
const submitForm = (form) => {
|
||||
console.info(form)
|
||||
form.validate((valid) => {
|
||||
if (valid) {
|
||||
console.log('submit!')
|
||||
ElMessage.success('表单校验通过并提交!')
|
||||
} else {
|
||||
console.log('error submit!')
|
||||
ElMessage.warning('表单校验未通过')
|
||||
return false
|
||||
}
|
||||
})
|
||||
@@ -237,37 +258,24 @@ const submitForm = (form) => {
|
||||
@submit-form="submitForm"
|
||||
>
|
||||
<template #default>
|
||||
<el-form-item>
|
||||
<el-form-item label="联系人列表">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="margin-bottom2"
|
||||
@click="addContact"
|
||||
>
|
||||
添加联系人
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<div
|
||||
v-for="(contact, index) in userDto.contacts"
|
||||
:key="index"
|
||||
class="common-subform el-form--inline"
|
||||
>
|
||||
<common-form-control
|
||||
v-for="(option, optIdx) in contactsOptions"
|
||||
:key="`${index}-${optIdx}`"
|
||||
:model="contact"
|
||||
:option="option"
|
||||
:prop="`contacts.${index}.${option.prop}`"
|
||||
<common-table-form
|
||||
class="form-edit-width-100"
|
||||
:model="userDto"
|
||||
:form-options="contactsOptions"
|
||||
data-list-key="contacts"
|
||||
sortable
|
||||
@delete="deleteContact($event.index)"
|
||||
/>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click.prevent="deleteContact(index)"
|
||||
>
|
||||
Delete
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
#buttons="{form}"
|
||||
@@ -277,8 +285,13 @@ const submitForm = (form) => {
|
||||
</el-button>
|
||||
</template>
|
||||
</common-form>
|
||||
<div>
|
||||
{{ userDto }}
|
||||
<div class="margin-top4">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>表单数据预览 (JSON)</span>
|
||||
</template>
|
||||
<pre>{{ JSON.stringify(userDto, null, 2) }}</pre>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineFormOptions } from '@/components/utils'
|
||||
const showWindow = ref(false)
|
||||
const okLoading = ref(false)
|
||||
|
||||
const userModel = ref({
|
||||
cert: {}
|
||||
@@ -42,9 +43,14 @@ const formOptions2 = defineFormOptions([
|
||||
])
|
||||
|
||||
const submitForm = ({ form }) => {
|
||||
console.info('=============================', form)
|
||||
form.validate(valid => {
|
||||
console.info('======================valid', valid)
|
||||
if (valid) {
|
||||
okLoading.value = true
|
||||
setTimeout(() => {
|
||||
okLoading.value = false
|
||||
showWindow.value = false
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
@@ -62,6 +68,7 @@ const submitForm = ({ form }) => {
|
||||
</el-button>
|
||||
<common-window
|
||||
v-model="showWindow"
|
||||
:ok-loading="okLoading"
|
||||
:ok-click="submitForm"
|
||||
show-fullscreen
|
||||
>
|
||||
|
||||
@@ -8,30 +8,33 @@ 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
|
||||
}].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 ({ mode }) => {
|
||||
export default ({ mode, command }) => {
|
||||
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
const optionalPlugins = [{
|
||||
plugin: visualizer({ open: true }),
|
||||
enabled: false
|
||||
}, {
|
||||
plugin: viteMockServe({
|
||||
mockPath: './mock',
|
||||
enable: command === 'serve'
|
||||
}),
|
||||
enabled: command === 'serve'
|
||||
}].filter(p => p.enabled).map(p => p.plugin)
|
||||
|
||||
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'] : []
|
||||
},
|
||||
Reference in New Issue
Block a user