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:
81
index.html
81
index.html
@@ -1,13 +1,82 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>%VITE_APP_NAME%</title>
|
<title>%VITE_APP_NAME%</title>
|
||||||
</head>
|
<style>
|
||||||
<body>
|
body {
|
||||||
<div id="app"></div>
|
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 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>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -52,8 +52,9 @@ export default [
|
|||||||
{
|
{
|
||||||
url: '/simple/city/selectCities',
|
url: '/simple/city/selectCities',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
response: request => {
|
response: () => {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
resultData: {
|
resultData: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, isVNode } from 'vue'
|
||||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||||
import { isFunction } from 'lodash-es'
|
import { isFunction } from 'lodash-es'
|
||||||
|
|
||||||
@@ -45,6 +45,19 @@ const tooltipFunc = ($event) => {
|
|||||||
:readonly="option.readonly"
|
:readonly="option.readonly"
|
||||||
v-bind="option.attrs"
|
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 }}
|
{{ label }}
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
v-if="option.tooltip||option.tooltipFunc"
|
v-if="option.tooltip||option.tooltipFunc"
|
||||||
@@ -58,6 +71,8 @@ const tooltipFunc = ($event) => {
|
|||||||
<span>
|
<span>
|
||||||
<el-link
|
<el-link
|
||||||
underline="never"
|
underline="never"
|
||||||
|
:type="option.tooltipFunc ? 'primary' : 'default'"
|
||||||
|
v-bind="option.tooltipLinkAttrs"
|
||||||
@click="tooltipFunc($event)"
|
@click="tooltipFunc($event)"
|
||||||
>
|
>
|
||||||
<common-icon
|
<common-icon
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { computed, isVNode, ref, watch } from 'vue'
|
|||||||
import { $i18nBundle } from '@/messages'
|
import { $i18nBundle } from '@/messages'
|
||||||
import ControlChild from '@/components/common-form-control/control-child.vue'
|
import ControlChild from '@/components/common-form-control/control-child.vue'
|
||||||
import { toLabelByKey, useInputType } from '@/components/utils'
|
import { toLabelByKey, useInputType } from '@/components/utils'
|
||||||
import { cloneDeep, get, isFunction, set, isArray, isString } from 'lodash-es'
|
import { cloneDeep, get, isFunction, set, isArray, isString, isEqual } from 'lodash-es'
|
||||||
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
@@ -173,10 +173,15 @@ const rules = computed(() => {
|
|||||||
}, ..._rules]
|
}, ..._rules]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
formItemRef.value && formItemRef.value.clearValidate()
|
|
||||||
return _rules
|
return _rules
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(rules, (newRules, oldRules) => {
|
||||||
|
if (formItemRef.value && !isEqual(newRules, oldRules)) {
|
||||||
|
formItemRef.value.clearValidate()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
const initFormModel = () => {
|
const initFormModel = () => {
|
||||||
if (formModel.value) {
|
if (formModel.value) {
|
||||||
const option = calcOption.value
|
const option = calcOption.value
|
||||||
@@ -222,6 +227,15 @@ const formatResult = computed(() => {
|
|||||||
return null
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -251,23 +265,25 @@ const formatResult = computed(() => {
|
|||||||
/>
|
/>
|
||||||
<slot name="afterLabel" />
|
<slot name="afterLabel" />
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
v-if="calcOption.tooltip||calcOption.tooltipFunc"
|
v-for="(tooltipOption, index) in tooltips"
|
||||||
|
:key="index"
|
||||||
class="box-item common-el-tooltip"
|
class="box-item common-el-tooltip"
|
||||||
effect="dark"
|
effect="dark"
|
||||||
:disabled="!calcOption.tooltip"
|
:disabled="!tooltipOption.tooltip"
|
||||||
:content="calcOption.tooltip"
|
:content="tooltipOption.tooltip"
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
raw-content
|
raw-content
|
||||||
v-bind="calcOption.tooltipAttrs"
|
v-bind="tooltipOption.tooltipAttrs"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<el-link
|
<el-link
|
||||||
v-bind="calcOption.tooltipLinkAttrs"
|
|
||||||
underline="never"
|
underline="never"
|
||||||
@click="calcOption.tooltipFunc"
|
:type="tooltipOption.tooltipFunc ? 'primary' : 'default'"
|
||||||
|
v-bind="tooltipOption.tooltipLinkAttrs"
|
||||||
|
@click="tooltipOption.tooltipFunc"
|
||||||
>
|
>
|
||||||
<common-icon
|
<common-icon
|
||||||
:icon="calcOption.tooltipIcon||'QuestionFilled'"
|
:icon="tooltipOption.tooltipIcon||'QuestionFilled'"
|
||||||
/>
|
/>
|
||||||
</el-link>
|
</el-link>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -176,6 +176,11 @@ const goBack = (...args) => {
|
|||||||
v-bind="{...$attrs, 'class':undefined}"
|
v-bind="{...$attrs, 'class':undefined}"
|
||||||
@submit.prevent
|
@submit.prevent
|
||||||
>
|
>
|
||||||
|
<slot
|
||||||
|
:form="form"
|
||||||
|
:model="formModel"
|
||||||
|
name="before-options"
|
||||||
|
/>
|
||||||
<template
|
<template
|
||||||
v-for="(option,index) in options"
|
v-for="(option,index) in options"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -230,6 +235,11 @@ const goBack = (...args) => {
|
|||||||
>
|
>
|
||||||
{{ resetLabel||$t('common.label.reset') }}
|
{{ resetLabel||$t('common.label.reset') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<slot
|
||||||
|
:form="form"
|
||||||
|
:model="formModel"
|
||||||
|
name="buttons"
|
||||||
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="showBack||backUrl"
|
v-if="showBack||backUrl"
|
||||||
:disabled="disableButtons"
|
:disabled="disableButtons"
|
||||||
@@ -237,11 +247,6 @@ const goBack = (...args) => {
|
|||||||
>
|
>
|
||||||
{{ backLabel||$t('common.label.back') }}
|
{{ backLabel||$t('common.label.back') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<slot
|
|
||||||
:form="form"
|
|
||||||
:model="formModel"
|
|
||||||
name="buttons"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<slot
|
<slot
|
||||||
:form="form"
|
: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-form-label': CommonFormLabelProps,
|
||||||
'common-icon-select': IconSelectProps,
|
'common-icon-select': IconSelectProps,
|
||||||
'common-autocomplete': CommonAutocompleteProps,
|
'common-autocomplete': CommonAutocompleteProps,
|
||||||
|
'common-object': Record<string, any>,
|
||||||
[key:string]: InputProps
|
[key:string]: InputProps
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +147,19 @@ export interface CommonFormOption extends FormControlTypeOption {
|
|||||||
tooltipLinkAttrs?: LinkProps;
|
tooltipLinkAttrs?: LinkProps;
|
||||||
/** 提示函数 */
|
/** 提示函数 */
|
||||||
tooltipFunc?: () => void;
|
tooltipFunc?: () => void;
|
||||||
|
/** 多个提示信息配置 */
|
||||||
|
tooltips?: Array<{
|
||||||
|
tooltip?: string;
|
||||||
|
tooltipIcon?: string;
|
||||||
|
tooltipAttrs?: ElTooltipProps;
|
||||||
|
tooltipLinkAttrs?: LinkProps;
|
||||||
|
tooltipFunc?: () => void;
|
||||||
|
}>;
|
||||||
|
/** 子选项自定义插槽映射 */
|
||||||
|
slots?: Record<string, (scope: any) => any>;
|
||||||
/** 自动trim,默认false**/
|
/** 自动trim,默认false**/
|
||||||
trim?: boolean,
|
trim?: boolean,
|
||||||
|
|
||||||
/** 自动upperCase,默认false**/
|
/** 自动upperCase,默认false**/
|
||||||
upperCase?: boolean,
|
upperCase?: boolean,
|
||||||
/** 自动lowerCase,默认false**/
|
/** 自动lowerCase,默认false**/
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ const selectIcon = icon => {
|
|||||||
<el-backtop
|
<el-backtop
|
||||||
v-common-tooltip="$t('common.label.backtop')"
|
v-common-tooltip="$t('common.label.backtop')"
|
||||||
target=".scroller"
|
target=".scroller"
|
||||||
:right="10"
|
:right="40"
|
||||||
:bottom="10"
|
:bottom="40"
|
||||||
/>
|
/>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
|||||||
@@ -33,19 +33,16 @@ const activeRoutePath = computed(() => {
|
|||||||
router
|
router
|
||||||
>
|
>
|
||||||
<slot name="before" />
|
<slot name="before" />
|
||||||
<template
|
<common-menu-item
|
||||||
v-for="(menuItem, index) in menuItems"
|
v-for="(menuItem, index) in menuItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
:menu-item="menuItem"
|
||||||
|
:index="`${index}`"
|
||||||
>
|
>
|
||||||
<common-menu-item
|
<template #split>
|
||||||
:menu-item="menuItem"
|
<slot name="split" />
|
||||||
:index="`${index}`"
|
</template>
|
||||||
>
|
</common-menu-item>
|
||||||
<template #split>
|
|
||||||
<slot name="split" />
|
|
||||||
</template>
|
|
||||||
</common-menu-item>
|
|
||||||
</template>
|
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</template>
|
</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>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { toLabelByKey } from '@/components/utils'
|
import { toLabelByKey } from '@/components/utils'
|
||||||
|
import { useRenderKey, useSortableParams } from '@/hooks/CommonHooks'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
formOptions: {
|
formOptions: {
|
||||||
@@ -22,6 +23,14 @@ const props = defineProps({
|
|||||||
operationWidth: {
|
operationWidth: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '110px'
|
default: '110px'
|
||||||
|
},
|
||||||
|
formPropPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
sortable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,6 +40,12 @@ const dataList = computed(() => {
|
|||||||
|
|
||||||
const emit = defineEmits(['delete', 'change'])
|
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) => {
|
const deleteItem = (item, index) => {
|
||||||
emit('delete', {
|
emit('delete', {
|
||||||
item, index
|
item, index
|
||||||
@@ -57,13 +72,36 @@ const options = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-table
|
<el-table
|
||||||
|
ref="sortableRef"
|
||||||
:data="dataList"
|
:data="dataList"
|
||||||
|
:row-key="rowKey"
|
||||||
class="common-table-form"
|
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
|
<el-table-column
|
||||||
v-for="(option, index) in options"
|
v-for="(option, index) in options"
|
||||||
:key="`${option.prop}__${index}`"
|
:key="`${option.prop}__${index}`"
|
||||||
:width="option.width"
|
:width="option.width"
|
||||||
|
:min-width="option.minWidth"
|
||||||
|
v-bind="option.columnAttrs"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-if="option.headerSlot"
|
v-if="option.headerSlot"
|
||||||
@@ -103,7 +141,7 @@ const options = computed(() => {
|
|||||||
:model="row"
|
:model="row"
|
||||||
label-width="0"
|
label-width="0"
|
||||||
:option="option"
|
:option="option"
|
||||||
:prop="`${dataListKey}.${$index}.${option.prop}`"
|
:prop="formPropPrefix ? `${formPropPrefix}.${dataListKey}.${$index}.${option.prop}` : `${dataListKey}.${$index}.${option.prop}`"
|
||||||
@change="formChange($event, row, $index, option)"
|
@change="formChange($event, row, $index, option)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -145,5 +183,10 @@ const options = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
okLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
okClick: {
|
okClick: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: null
|
default: null
|
||||||
@@ -230,6 +234,7 @@ if (props.showFullscreen && props.dblclickToFullscreen) {
|
|||||||
<el-button
|
<el-button
|
||||||
v-if="showOk"
|
v-if="showOk"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
:loading="okLoading"
|
||||||
@click="okButtonClick($event)"
|
@click="okButtonClick($event)"
|
||||||
>{{ okLabel||$t('common.label.confirm') }}</el-button>
|
>{{ okLabel||$t('common.label.confirm') }}</el-button>
|
||||||
<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 CommonSort from '@/components/common-sort/index.vue'
|
||||||
import CommonDescriptions from '@/components/common-descriptions/index.vue'
|
import CommonDescriptions from '@/components/common-descriptions/index.vue'
|
||||||
import CommonSplit from '@/components/common-split/index.vue'
|
import CommonSplit from '@/components/common-split/index.vue'
|
||||||
|
import CommonObject from '@/components/common-object/index.vue'
|
||||||
import CommonDirectives from '@/components/directives'
|
import CommonDirectives from '@/components/directives'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,6 +47,7 @@ export default {
|
|||||||
Vue.component('CommonSort', CommonSort)
|
Vue.component('CommonSort', CommonSort)
|
||||||
Vue.component('CommonDescriptions', CommonDescriptions)
|
Vue.component('CommonDescriptions', CommonDescriptions)
|
||||||
Vue.component('CommonSplit', CommonSplit)
|
Vue.component('CommonSplit', CommonSplit)
|
||||||
|
Vue.component('CommonObject', CommonObject)
|
||||||
Vue.use(CommonDirectives)
|
Vue.use(CommonDirectives)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { $i18nBundle } from '@/messages'
|
||||||
/**
|
/**
|
||||||
* 全局布局模式
|
* 全局布局模式
|
||||||
* @readonly
|
* @readonly
|
||||||
@@ -25,3 +26,44 @@ export const LoadSaveParamMode = {
|
|||||||
BACK: 'back',
|
BACK: 'back',
|
||||||
NEVER: 'never'
|
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 { provide, inject, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
|
||||||
import { isFunction, isNumber } from 'lodash-es'
|
import { isFunction, isNumber, uniqueId } from 'lodash-es'
|
||||||
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
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 = ({
|
export const useTableAndSearchForm = ({
|
||||||
searchMethod,
|
searchMethod,
|
||||||
defaultParam = {},
|
defaultParam = {},
|
||||||
dataProcessor,
|
dataProcessor = searchResult => searchResult.resultData,
|
||||||
pageProcessor,
|
pageProcessor = defaultPageProcessor,
|
||||||
saveParam = true
|
saveParam = true
|
||||||
}) => {
|
}) => {
|
||||||
const globalSearchParamStore = useGlobalSearchParamStore()
|
const globalSearchParamStore = useGlobalSearchParamStore()
|
||||||
const tableData = ref([])
|
const tableData = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const searchParam = ref(saveParam ? globalSearchParamStore.getCurrentParam(defaultParam) : defaultParam)
|
const searchParam = ref(saveParam ? globalSearchParamStore.getCurrentParam(defaultParam) : defaultParam)
|
||||||
const searchTableItems = async (pageNumber, newParams = {}) => {
|
const searchTableItems = async (pageNumber, newParams = {}, saveConfig) => {
|
||||||
if (isNumber(pageNumber)) {
|
if (isNumber(pageNumber) && searchParam.value) {
|
||||||
searchParam.value?.pageSetting && (searchParam.value.pageSetting.pageNumber = pageNumber)
|
if (searchParam.value.page) {
|
||||||
|
searchParam.value.page.pageNumber = pageNumber
|
||||||
|
} else if (searchParam.value.pageSetting) {
|
||||||
|
searchParam.value.pageSetting.pageNumber = pageNumber
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
saveParam && globalSearchParamStore.saveCurrentParam(searchParam.value)
|
saveParam && globalSearchParamStore.saveCurrentParam(searchParam.value, saveConfig)
|
||||||
const searchResult = await searchMethod({ ...searchParam.value, ...newParams })
|
const searchResult = await searchMethod({ ...searchParam.value, ...newParams })
|
||||||
.finally(() => { loading.value = false })
|
.finally(() => { loading.value = false })
|
||||||
loading.value = false
|
loading.value = false
|
||||||
if (searchResult.success && searchResult.resultData) {
|
if (searchResult.success && searchResult.resultData) {
|
||||||
const resultData = searchResult.resultData
|
tableData.value = isFunction(dataProcessor) && dataProcessor?.(searchResult, searchParam)
|
||||||
tableData.value = isFunction(dataProcessor) && dataProcessor?.(resultData, searchParam)
|
pageProcessor?.(searchResult, searchParam)
|
||||||
pageProcessor = pageProcessor || ((resultData, searchParam) => {
|
|
||||||
searchParam.value?.pageSetting && resultData.pageSetting && Object.assign(searchParam.value.pageSetting, resultData.pageSetting || {})
|
|
||||||
})
|
|
||||||
pageProcessor?.(resultData, searchParam)
|
|
||||||
}
|
}
|
||||||
return searchResult
|
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) => {
|
export const useDateStr = (watchFn) => {
|
||||||
const dateStr = ref(new Date().getTime())
|
const dateStr = ref(new Date().getTime())
|
||||||
watchFn && watch(watchFn, (update) => {
|
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.status = '状态'
|
||||||
common.label.statusEnabled = '启用'
|
common.label.statusEnabled = '启用'
|
||||||
common.label.statusDisabled = '禁用'
|
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.commonCode = '{0}代码'
|
||||||
common.label.commonConfig = '配置{0}'
|
common.label.commonConfig = '配置{0}'
|
||||||
common.label.commonEdit = '{0}编辑'
|
common.label.commonEdit = '{0}编辑'
|
||||||
common.label.commonAdd = '新增{0}'
|
common.label.commonAdd = '新增{0}'
|
||||||
|
common.label.commonView = '查看{0}'
|
||||||
common.label.commonDelete = '删除{0}'
|
common.label.commonDelete = '删除{0}'
|
||||||
common.label.commonParent = '上级{0}'
|
common.label.commonParent = '上级{0}'
|
||||||
common.label.commonAdd1 = '添加{0}'
|
common.label.commonAdd1 = '添加{0}'
|
||||||
common.label.commonCopy = '复制{0}'
|
common.label.commonCopy = '复制{0}'
|
||||||
common.label.commonSwap = '交换{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=====================//
|
//* =======================msg=====================//
|
||||||
common.msg.nonNull = '{0}不能为空'
|
common.msg.nonNull = '{0}不能为空'
|
||||||
@@ -107,3 +138,6 @@ common.msg.notFound = '页面不存在,请访问其他页面'
|
|||||||
common.msg.accessDenied = '没有权限访问该页面'
|
common.msg.accessDenied = '没有权限访问该页面'
|
||||||
common.msg.emailError = '邮箱格式不正确'
|
common.msg.emailError = '邮箱格式不正确'
|
||||||
common.msg.cannotEnterChineseCharacters = '{0}不能输入中文!'
|
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.status = 'Status'
|
||||||
common.label.statusEnabled = 'Enabled'
|
common.label.statusEnabled = 'Enabled'
|
||||||
common.label.statusDisabled = 'Disabled'
|
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.commonConfig = 'Config {0}'
|
||||||
common.label.commonCode = '{0} Code'
|
common.label.commonCode = '{0} Code'
|
||||||
common.label.commonEdit = '{0} Edit'
|
common.label.commonEdit = '{0} Edit'
|
||||||
common.label.commonAdd = 'Add {0}'
|
common.label.commonAdd = 'Add {0}'
|
||||||
|
common.label.commonView = 'View {0}'
|
||||||
common.label.commonDelete = 'Delete {0}'
|
common.label.commonDelete = 'Delete {0}'
|
||||||
common.label.commonParent = 'Parent {0}'
|
common.label.commonParent = 'Parent {0}'
|
||||||
common.label.commonAdd1 = 'Add {0}'
|
common.label.commonAdd1 = 'Add {0}'
|
||||||
common.label.commonCopy = 'Copy {0}'
|
common.label.commonCopy = 'Copy {0}'
|
||||||
common.label.commonSwap = 'Swap {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=====================//
|
//* =======================msg=====================//
|
||||||
common.msg.nonNull = '{0} is required.'
|
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.accessDenied = 'Page access denied'
|
||||||
common.msg.emailError = 'E-mail format is invalid.'
|
common.msg.emailError = 'E-mail format is invalid.'
|
||||||
common.msg.cannotEnterChineseCharacters = '{0} cannot enter Chinese characters.'
|
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 dayjs from 'dayjs'
|
||||||
import { markRaw, ref } from 'vue'
|
import { markRaw, ref, nextTick } from 'vue'
|
||||||
import { isObject, isArray, set, isNumber } from 'lodash-es'
|
import { isObject, isArray, set, isNumber, isFunction, isBoolean, isString, get, cloneDeep } from 'lodash-es'
|
||||||
import { ElLoading, ElMessageBox, ElMessage } from 'element-plus'
|
import { ElLoading, ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||||
import numeral from 'numeral'
|
import numeral from 'numeral'
|
||||||
@@ -12,7 +12,7 @@ import { useLoginConfigStore } from '@/stores/LoginConfigStore'
|
|||||||
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
|
||||||
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
import { useGlobalSearchParamStore } from '@/stores/GlobalSearchParamStore'
|
||||||
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
import { useTabsViewStore } from '@/stores/TabsViewStore'
|
||||||
import { LoadSaveParamMode } from '@/consts/GlobalConstants'
|
import { GlobalLayoutMode, LoadSaveParamMode } from '@/consts/GlobalConstants'
|
||||||
|
|
||||||
export const useSystemKey = () => {
|
export const useSystemKey = () => {
|
||||||
return SYSTEM_KEY
|
return SYSTEM_KEY
|
||||||
@@ -284,6 +284,21 @@ export const $coreConfirm = (message, title = $i18nBundle('common.label.reminder
|
|||||||
options)
|
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) => {
|
export const $formatNumber = (value, format) => {
|
||||||
return numeral(value).format(format)
|
return numeral(value).format(format)
|
||||||
}
|
}
|
||||||
@@ -304,12 +319,12 @@ export const $currencyShort = (value, prefix) => {
|
|||||||
return `${prefix || '¥'} ${$formatNumber(value, '0,0.[00]')}`
|
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}
|
* @type {CopyTextConfig}
|
||||||
*/
|
*/
|
||||||
const defaultCopyConfig = {
|
const defaultCopyConfig = {
|
||||||
success: 'Copied Successfully!',
|
successKey: 'common.msg.copySuccess',
|
||||||
error: 'Copy Not supported!'
|
errorKey: 'common.msg.copyError'
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param text {string | CopyTextConfig} 需要复制的文本
|
* @param text {string | CopyTextConfig} 需要复制的文本
|
||||||
@@ -328,12 +343,12 @@ export const $copyText = (text) => {
|
|||||||
if (isSupported) {
|
if (isSupported) {
|
||||||
copy(config.text)
|
copy(config.text)
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: config.success,
|
message: config.success || $i18nBundle(config.successKey),
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: config.error,
|
message: config.error || $i18nBundle(config.errorKey),
|
||||||
type: 'error'
|
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 {
|
export default {
|
||||||
install (app) {
|
install (app) {
|
||||||
router = app.config.globalProperties.$router
|
router = app.config.globalProperties.$router
|
||||||
@@ -354,6 +576,7 @@ export default {
|
|||||||
$number,
|
$number,
|
||||||
$currency,
|
$currency,
|
||||||
$currencyShort,
|
$currencyShort,
|
||||||
|
$formatFileSize: formatFileSize,
|
||||||
$coreShowLoading,
|
$coreShowLoading,
|
||||||
$coreHideLoading,
|
$coreHideLoading,
|
||||||
$coreAlert,
|
$coreAlert,
|
||||||
@@ -361,8 +584,10 @@ export default {
|
|||||||
$coreWarning,
|
$coreWarning,
|
||||||
$coreError,
|
$coreError,
|
||||||
$coreConfirm,
|
$coreConfirm,
|
||||||
|
$corePrompt,
|
||||||
$openNewWin,
|
$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">
|
<el-container class="container-center">
|
||||||
<v-chart
|
<v-chart
|
||||||
v-if="chartConfig"
|
v-if="chartConfig"
|
||||||
class="chart"
|
|
||||||
:key="theme"
|
:key="theme"
|
||||||
|
class="chart"
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
:option="chartConfig"
|
:option="chartConfig"
|
||||||
autoresize
|
autoresize
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useCityAutocompleteConfig, useCitySelectPageConfig } from '@/services/c
|
|||||||
import { $i18nMsg } from '@/messages'
|
import { $i18nMsg } from '@/messages'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { defineFormOptions } from '@/components/utils'
|
import { defineFormOptions } from '@/components/utils'
|
||||||
|
import { useFormStatus, useFormDelay } from '@/consts/GlobalConstants'
|
||||||
|
|
||||||
const defaultCity = ref({})
|
const defaultCity = ref({})
|
||||||
|
|
||||||
@@ -67,6 +68,10 @@ const formOptions = computed(() => {
|
|||||||
value: '',
|
value: '',
|
||||||
placeholder: '请输入用户名',
|
placeholder: '请输入用户名',
|
||||||
required: true,
|
required: true,
|
||||||
|
tooltips: [
|
||||||
|
{ tooltip: '用户名在2-6位之间' },
|
||||||
|
{ tooltip: '点击查看帮助', tooltipFunc: () => ElMessage.info('用户名规则帮助') }
|
||||||
|
],
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
min: 2,
|
min: 2,
|
||||||
@@ -182,12 +187,29 @@ const formOptions = computed(() => {
|
|||||||
maxlength: 100,
|
maxlength: 100,
|
||||||
showWordLimit: true
|
showWordLimit: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
useFormStatus('status'),
|
||||||
|
useFormDelay('delay'),
|
||||||
|
{
|
||||||
|
label: '扩展参数',
|
||||||
|
prop: 'extraParams',
|
||||||
|
type: 'common-object',
|
||||||
|
value: { env: 'production', role: 'admin' }
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
const userDto = ref({
|
const userDto = ref({
|
||||||
|
status: 1,
|
||||||
|
delay: 200,
|
||||||
|
extraParams: {
|
||||||
|
env: 'production',
|
||||||
|
role: 'admin'
|
||||||
|
},
|
||||||
contacts: [{
|
contacts: [{
|
||||||
name: 'Jerry',
|
name: 'Jerry',
|
||||||
phone: '1234567890'
|
phone: '1234567890'
|
||||||
|
}, {
|
||||||
|
name: 'Tom',
|
||||||
|
phone: '1987654321'
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
@@ -204,10 +226,10 @@ const contactsOptions = ref([{
|
|||||||
}])
|
}])
|
||||||
|
|
||||||
const addContact = () => {
|
const addContact = () => {
|
||||||
if (userDto.value.contacts.length < 3) {
|
if (userDto.value.contacts.length < 5) {
|
||||||
userDto.value.contacts.push({})
|
userDto.value.contacts.push({})
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error('联系人不能超过3个')
|
ElMessage.error('联系人不能超过5个')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,12 +238,11 @@ const deleteContact = idx => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitForm = (form) => {
|
const submitForm = (form) => {
|
||||||
console.info(form)
|
|
||||||
form.validate((valid) => {
|
form.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
console.log('submit!')
|
ElMessage.success('表单校验通过并提交!')
|
||||||
} else {
|
} else {
|
||||||
console.log('error submit!')
|
ElMessage.warning('表单校验未通过')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -237,37 +258,24 @@ const submitForm = (form) => {
|
|||||||
@submit-form="submitForm"
|
@submit-form="submitForm"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-form-item>
|
<el-form-item label="联系人列表">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
|
class="margin-bottom2"
|
||||||
@click="addContact"
|
@click="addContact"
|
||||||
>
|
>
|
||||||
添加联系人
|
添加联系人
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
<common-table-form
|
||||||
<div
|
class="form-edit-width-100"
|
||||||
v-for="(contact, index) in userDto.contacts"
|
:model="userDto"
|
||||||
:key="index"
|
:form-options="contactsOptions"
|
||||||
class="common-subform el-form--inline"
|
data-list-key="contacts"
|
||||||
>
|
sortable
|
||||||
<common-form-control
|
@delete="deleteContact($event.index)"
|
||||||
v-for="(option, optIdx) in contactsOptions"
|
|
||||||
:key="`${index}-${optIdx}`"
|
|
||||||
:model="contact"
|
|
||||||
:option="option"
|
|
||||||
:prop="`contacts.${index}.${option.prop}`"
|
|
||||||
/>
|
/>
|
||||||
<el-form-item>
|
</el-form-item>
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
size="small"
|
|
||||||
@click.prevent="deleteContact(index)"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
#buttons="{form}"
|
#buttons="{form}"
|
||||||
@@ -277,8 +285,13 @@ const submitForm = (form) => {
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</common-form>
|
</common-form>
|
||||||
<div>
|
<div class="margin-top4">
|
||||||
{{ userDto }}
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>表单数据预览 (JSON)</span>
|
||||||
|
</template>
|
||||||
|
<pre>{{ JSON.stringify(userDto, null, 2) }}</pre>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { defineFormOptions } from '@/components/utils'
|
import { defineFormOptions } from '@/components/utils'
|
||||||
const showWindow = ref(false)
|
const showWindow = ref(false)
|
||||||
|
const okLoading = ref(false)
|
||||||
|
|
||||||
const userModel = ref({
|
const userModel = ref({
|
||||||
cert: {}
|
cert: {}
|
||||||
@@ -42,9 +43,14 @@ const formOptions2 = defineFormOptions([
|
|||||||
])
|
])
|
||||||
|
|
||||||
const submitForm = ({ form }) => {
|
const submitForm = ({ form }) => {
|
||||||
console.info('=============================', form)
|
|
||||||
form.validate(valid => {
|
form.validate(valid => {
|
||||||
console.info('======================valid', valid)
|
if (valid) {
|
||||||
|
okLoading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
okLoading.value = false
|
||||||
|
showWindow.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -62,6 +68,7 @@ const submitForm = ({ form }) => {
|
|||||||
</el-button>
|
</el-button>
|
||||||
<common-window
|
<common-window
|
||||||
v-model="showWindow"
|
v-model="showWindow"
|
||||||
|
:ok-loading="okLoading"
|
||||||
:ok-click="submitForm"
|
:ok-click="submitForm"
|
||||||
show-fullscreen
|
show-fullscreen
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,30 +8,33 @@ import eslint from 'vite-plugin-eslint'
|
|||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import packageJson from './package.json'
|
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 JS_FILE_NAMES = 'js/[name]-[hash].js'
|
||||||
const CSS_FILE_NAMES = 'css/[name]-[hash].css'
|
const CSS_FILE_NAMES = 'css/[name]-[hash].css'
|
||||||
const IMG_EXT_LIST = ['.png', '.jpg', '.gif', '.svg', '.bmp', '.webp']
|
const IMG_EXT_LIST = ['.png', '.jpg', '.gif', '.svg', '.bmp', '.webp']
|
||||||
const IMG_FILE_NAMES = 'images/[name]-[hash].[ext]'
|
const IMG_FILE_NAMES = 'images/[name]-[hash].[ext]'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default ({ mode }) => {
|
export default ({ mode, command }) => {
|
||||||
|
|
||||||
const env = loadEnv(mode, process.cwd())
|
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({
|
return defineConfig({
|
||||||
base: env.VITE_APP_CONTEXT_PATH,
|
base: env.VITE_APP_CONTEXT_PATH,
|
||||||
define: {
|
define: {
|
||||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version)
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version)
|
||||||
},
|
},
|
||||||
plugins: [vue(), vueJsx(), eslint(), ...optionalPlugins],
|
plugins: [vue(), vueJsx(), eslint(), ...optionalPlugins],
|
||||||
|
|
||||||
esbuild: {
|
esbuild: {
|
||||||
drop: mode === 'production' ? ['console', 'debugger'] : []
|
drop: mode === 'production' ? ['console', 'debugger'] : []
|
||||||
},
|
},
|
||||||
@@ -46,7 +49,7 @@ export default ({ mode }) => {
|
|||||||
output: {
|
output: {
|
||||||
chunkFileNames: JS_FILE_NAMES, // 引入文件名的名称
|
chunkFileNames: JS_FILE_NAMES, // 引入文件名的名称
|
||||||
entryFileNames: JS_FILE_NAMES, // 包的入口文件名称
|
entryFileNames: JS_FILE_NAMES, // 包的入口文件名称
|
||||||
assetFileNames(assetInfo) {
|
assetFileNames (assetInfo) {
|
||||||
if (assetInfo.name?.endsWith('.css')) { // CSS文件
|
if (assetInfo.name?.endsWith('.css')) { // CSS文件
|
||||||
return CSS_FILE_NAMES
|
return CSS_FILE_NAMES
|
||||||
} else if (IMG_EXT_LIST.some((ext) => assetInfo.name?.endsWith(ext))) { // 图片
|
} else if (IMG_EXT_LIST.some((ext) => assetInfo.name?.endsWith(ext))) { // 图片
|
||||||
@@ -54,7 +57,7 @@ export default ({ mode }) => {
|
|||||||
}
|
}
|
||||||
return 'assets/[name]-[hash].[ext]' // 其他资源
|
return 'assets/[name]-[hash].[ext]' // 其他资源
|
||||||
},
|
},
|
||||||
manualChunks(id) {
|
manualChunks (id) {
|
||||||
if (id.includes('element-plus')) {
|
if (id.includes('element-plus')) {
|
||||||
return 'elp'
|
return 'elp'
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user