Pre Merge pull request !253 from 尹琦/新增表格通用组件、搜索通用组件

This commit is contained in:
尹琦 2025-12-11 11:45:47 +00:00 committed by Gitee
commit 9a3ab4cb98
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
18 changed files with 2020 additions and 4 deletions

View File

@ -44,7 +44,9 @@
"vue-json-pretty": "2.6.0",
"vue-router": "4.6.3",
"vue-types": "6.0.0",
"vxe-table": "4.17.7"
"vxe-table": "4.17.7",
"@layui/layui-vue": "^2.12.0",
"@vitejs/plugin-vue-jsx": "^4.1.2"
},
"devDependencies": {
"@iconify/json": "^2.2.403",

View File

@ -15,6 +15,25 @@ export const listDemo = (query?: DemoQuery): AxiosPromise<DemoVO[]> => {
});
};
/**
*
* @param current
* @param size
* @param params
* @returns {*}
*/
export const listTableDemo = (current: number, size: number, params: any): AxiosPromise<any> => {
return request({
url: '/demo/demo/list',
method: 'get',
params: {
pageNum:current,
pageSize: size,
...params
}
});
};
/**
*
* @param id

View File

@ -193,6 +193,7 @@ h6 {
}
.el-card__body {
height: 100%;
padding: 15px 20px 20px 20px !important;
}

View File

@ -0,0 +1,29 @@
<template>
<el-button size="small" type="danger" icon="Close" :disabled="disabled" class="el-button-close" @click="close"></el-button>
</template>
<script>
export default {
name: 'el-closebox',
props: {
disabled: {
type: Boolean,
default: false
}
},
methods: {
close() {
this.$emit('close');
}
}
};
</script>
<style lang="scss" scoped>
.el-button.el-button--small.el-button-close {
--el-button-size: 15px;
width: var(--el-button-size);
height: var(--el-button-size);
padding: 3px;
}
</style>

View File

@ -0,0 +1,34 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ex-slot',
props: {
value: [String, Number],
row: Object,
render: Function,
rowIndex: Number,
column: Object,
columnIndex: Number,
class: [String, Array, Object],
style: [String, Array, Object],
dict: [Array]
},
render() {
const value = this.render({
value: this.value,
row: this.row,
column: this.column,
rowIndex: this.rowIndex,
columnIndex: this.columnIndex,
dict: this.dict
});
if (this.style || this.class) {
return (
<div style={this.style} class={this.class}>
{value}
</div>
);
}
return value;
}
});

View File

@ -0,0 +1,507 @@
<template>
<lay-table
ref="table"
:id="id"
:page="page"
:size="size"
:height="height"
:max-height="maxHeight"
:loading="loading"
:columns="tableColumns"
:data-source="dataSource"
:default-toolbar="defaultToolbar"
:resize="resize"
:children-column-name="childrenColumnName"
:indent-size="indentSize"
:even="even"
:cell-style="cellStyle"
:row-style="rowStyle"
:cell-class-name="cellClassName"
:row-class-name="rowClassName"
:skin="skin"
:expand-index="expandIndex"
:expand-change="expandChange"
:default-expand-all="defaultExpandAll"
:span-method="arraySpanMethod"
:empty-description="emptyDescription"
v-model:expand-keys="expands"
v-model:selected-keys="selecteds"
v-model:selectedKey="selected"
>
<template v-if="$slots.toolbar" #toolbar>
<slot name="toolbar" />
</template>
<template v-if="$slots.footer" #footer>
<slot name="footer" />
</template>
<template v-if="$slots.empty" #empty>
<slot name="empty" />
</template>
<template v-if="$slots.expand" #expand="{ data }">
<slot name="expand" :data="data" />
</template>
<template #closeAll>
<close-box :disabled="disCloseAll" @close="closeAll" />
</template>
<template #close="{ row, rowIndex, column, columnIndex }">
<close-box :disabled="disClose" @close="close(row, rowIndex, column, columnIndex)" />
</template>
<template v-for="item in headColumns" #[item.titleSlot]>
<slot :name="item.titleSlot" :column="item">
<ex-slot v-if="item.renderHead" :render="item.renderHead" :column="item" :style="item.headStyle" :class="item.headClass" />
</slot>
</template>
<template v-for="item in customColumns" #[item.customSlot]="{ row, rowIndex, column, columnIndex }">
<slot :name="item.customSlot" :row="row" :row-index="rowIndex" :column="column" :column-index="columnIndex" :value="getValue(column, row)">
<ex-slot
v-if="item.render"
:style="isFunc(item.style, { row, rowIndex, column, columnIndex })"
:class="isFunc(item.class, { row, rowIndex, column, columnIndex })"
:render="item.render"
:row="row"
:row-index="rowIndex"
:dict="dictStore.getDict(column.dict)"
:column="item"
:column-index="columnIndex"
:value="getValue(column, row)"
/>
<template v-else>
<el-tooltip
popper-style="z-index: 9999 !important;"
v-if="item.showOverflowTooltip"
:content="getValue(column, row)"
placement="top"
:effect="isFunc(item.tooltipEffect, { row, rowIndex, column, columnIndex }) || 'dark'"
:disabled="!row.showTooltip"
>
<el-text
:type="isFunc(item.color, { row, rowIndex, column, columnIndex })"
:size="isFunc(item.size, { row, rowIndex, column, columnIndex }) || elSize"
:class="isFunc(item.class, { row, rowIndex, column, columnIndex })"
:style="isFunc(item.style, { row, rowIndex, column, columnIndex })"
:line-clamp="isFunc(item.lines, { row, rowIndex, column, columnIndex }) || 1"
@mouseenter="showTips($event, row, column)"
@click="typeof item.click == 'function' && item.click({ row, rowIndex, column, columnIndex })"
>{{ getValue(column, row) }}</el-text
>
</el-tooltip>
<el-text
v-else
:type="isFunc(item.color, { row, rowIndex, column, columnIndex })"
:size="isFunc(item.size, { row, rowIndex, column, columnIndex }) || elSize"
:class="isFunc(item.class, { row, rowIndex, column, columnIndex })"
:style="isFunc(item.style, { row, rowIndex, column, columnIndex })"
@click="typeof item.click == 'function' && item.click({ row, rowIndex, column, columnIndex })"
>{{ getValue(column, row) }}</el-text
>
</template>
</slot>
</template>
</lay-table>
</template>
<script>
import { createApp, isVNode } from 'vue';
import { renderToString } from '@vue/server-renderer';
import { getProperty, getDataFromTreeByKey } from '@/utils/common';
import CloseBox from '@/components/CloseBox/main.vue';
import exSlot from './exSlot';
import { useAppStore } from '@/store/modules/app';
import { useDictStore } from '@/store/modules/dict';
import { getDicts } from '@/api/system/dict/data';
export default {
name: 'form-table',
components: {
'ex-slot': exSlot,
'close-box': CloseBox
},
props: {
//
id: {
type: String,
default: 'id'
},
//
page: {
type: Object,
default: () => {}
},
//-
childrenColumnName: {
type: String,
default: 'children'
},
//
height: {
type: String,
default: '300px'
},
//
maxHeight: {
type: String,
default: ''
},
//
loading: {
type: Boolean,
default: false
},
//
columns: {
type: Array,
default: () => []
},
//
dataSource: {
type: Array,
default: () => []
},
//
defaultToolbar: {
type: [Boolean, Array],
default: true
},
//
resize: {
type: Boolean,
default: true
},
//-
indentSize: {
type: Number,
default: 30
},
//
even: {
type: Boolean,
default: false
},
//
cellStyle: Function,
//
rowStyle: Function,
//
cellClassName: Function,
//
rowClassName: Function,
// linerownob
skin: {
type: String,
default: ''
},
//
expandIndex: {
type: Number,
default: 0
},
//-
expandChange: {
type: Function,
default: () => {}
},
//-
defaultExpandAll: {
type: Boolean,
default: false
},
//
spanMethods: Function,
//
emptyDescription: {
type: String,
default: ''
},
//key
expandKeys: {
type: Array,
default: () => []
},
//key
selectedKeys: {
type: Array,
default: () => []
},
//key
selectedKey: {
type: [String, Number],
default: ''
},
//
disCloseAll: {
type: Boolean,
default: false
},
//
disClose: {
type: Boolean,
default: false
},
//
totalRowMap: {
type: Object,
default: () => {}
}
},
data() {
return {
expands: [],
selecteds: [],
selected: '',
tableColumns: [],
headColumns: [],
customColumns: [],
dicts: [],
dict: {},
appStore: useAppStore(), // appStore
size: 'sm',
dictStore: useDictStore()
};
},
computed: {
//layuiel
// eslint-disable-next-line vue/return-in-computed-property
elSize() {
if (this.size == 'sm') {
return 'small';
}
if (this.size == 'md') {
return 'default';
}
if (this.size == 'lg') {
return 'large';
}
}
},
watch: {
expands(val) {
this.expandKeys != val && this.$emit('update:expandKeys', val);
},
selecteds(val) {
this.selectedKeys != val && this.$emit('update:selectedKeys', val);
},
selected(val) {
this.selectedKey != val && this.$emit('update:selectedKey', val);
},
expandKeys(val) {
if (this.expands != val || this.expands.length != val.length) {
this.expands = val;
}
},
selectedKeys(val) {
if (this.selecteds != val || this.selecteds.length != val.length) {
this.selecteds = val;
}
},
selectedKey(val) {
if (this.selected != val) {
this.selected = val;
}
},
dataSource() {
this.totalRow();
},
'appStore.size'(newSize, oldSize) {
// size
if (newSize == 'small') {
this.size = 'sm';
} else if (newSize == 'default') {
this.size = 'md';
} else if (newSize == 'large') {
this.size = 'lg';
}
}
},
beforeMount() {
this.expands = this.expandKeys;
this.selecteds = this.selectedKeys;
this.selected = this.selectedKey;
this.tableColumns = this.initColumns();
//
this.renderDict(this.dicts);
},
methods: {
renderDict(dicts) {
const dictStore = useDictStore();
const dictTypes = [].concat(dicts);
Promise.all(
dictTypes.map(
(type) =>
new Promise((resolve, reject) => {
if (dictStore.getDict(type)) {
resolve(dictStore.getDict(type));
return;
}
getDicts(type)
.then((res) => {
const dictData = res.data.map((item) => ({
label: item.dictLabel,
value: item.dictValue
}));
dictStore.setDict(type, dictData);
resolve(res);
})
.catch((err) => reject(err));
})
)
);
this.dict = dictStore.dict;
},
//
initColumns(columns = this.columns) {
return columns.map((item) => {
if (item.children) {
item.children = this.initColumns(item.children);
}
if (item.type) {
if (item.type == 'close') {
const closeColumn = {
key: 'close',
customSlot: 'close',
titleSlot: 'closeAll',
...item
};
item.noCloseAll && delete closeColumn.titleSlot;
delete closeColumn.type;
return closeColumn;
}
return item;
}
item.customSlot = item.customSlot || item.key;
if (item.renderHead || item.titleSlot) {
item.titleSlot = item.titleSlot || `${item.key}Title`;
this.headColumns.push(item);
}
item.dict && this.dicts.push(item.dict);
this.customColumns.push(item);
return JSON.parse(JSON.stringify(item));
});
},
//
closeAll() {
this.$emit('closeAll');
},
//
close(row, rowIndex, column, columnIndex) {
this.$emit('close', { row, rowIndex, column, columnIndex });
},
//
arraySpanMethod(row, column, rowIndex, columnIndex) {
//
if (this.spanMethod) {
const value = this.spanMethod(row, column, rowIndex, columnIndex);
if (value) return value;
}
//merge
let rowspan = 1,
colspan = 1;
if (column.merge) {
//mergetruekey
const merge = typeof column.merge == 'boolean' ? column.key : column.merge;
//
const mergeKeys = [].concat(merge);
//
function isMerge(data) {
let isMerge = true;
mergeKeys.forEach((key) => (isMerge = isMerge && getProperty(row, key) == getProperty(data, key)));
return isMerge;
}
//[0, 0]
if (isMerge(this.dataSource[rowIndex - 1])) {
rowspan = 0;
colspan = 0;
} else {
//
for (let index = rowIndex + 1; index < this.dataSource.length; index++) {
const data = this.dataSource[index];
if (isMerge(data)) {
//1
rowspan += 1;
} else {
//
break;
}
}
}
}
//row
return [rowspan, colspan];
},
//
getCheckData() {
return this.$refs.table.getCheckData();
},
//
getValue(column, row) {
const value = getProperty(row, column.key);
if (column.dict) {
// return this.typeFormat(this.dict[column.dict], value) || value;
const dictStore = useDictStore();
const dictData = dictStore.getDict(column.dict);
if (dictData) {
const dictItem = dictData.find((item) => item.value == value);
return dictItem ? dictItem.label : value || '/';
}
}
return value;
},
//tooltips
showTips(obj, row, column) {
const lines = column.lines || 1;
let cellHeight = this.size === 'lg' ? 25 : 20;
cellHeight *= lines;
const scrollHeight = obj.target.scrollHeight;
row.showTooltip = scrollHeight > cellHeight;
},
//
totalRow(columns = this.columns) {
columns.forEach(async (item) => {
if (item.children) {
this.totalRow(item.children);
}
if (item.totalRow) {
if (typeof item.totalRow === 'string') return;
const column = getDataFromTreeByKey(this.tableColumns, item.key, 'key');
let pageTotal = 0;
this.dataSource.forEach((data) => {
const value = getProperty(data, column.key);
if (!isNaN(value)) pageTotal += value * 1;
});
const totalData = getProperty(this.totalRowMap, item.key) || pageTotal;
if (typeof item.totalRow === 'boolean') {
column.totalRow = totalData || pageTotal;
return;
}
if (typeof item.totalRow === 'function') {
const value = item.totalRow({ value: totalData, pageTotal, column });
if (isVNode(value)) {
const app = createApp({
render() {
return value;
}
});
column.totalRow = await renderToString(app);
} else {
column.totalRow = value;
}
}
}
});
},
//
isFunc(result, data) {
if (typeof result === 'function') {
const value = this.getValue(data.column, data.row);
return result({ value, ...data }) || null;
}
return result || null;
}
}
};
</script>
<style lang="scss" scoped>
:deep(.layui-table-cell-content) {
word-break: break-all;
}
</style>

View File

@ -0,0 +1,213 @@
<template>
<el-input
v-if="['text', 'number', 'textarea'].includes(config.type)"
v-model="value"
:type="config.type"
:clearable="config.clearable"
:placeholder="config.placeholder || '请输入'"
:readonly="config.readonly"
:suffix-icon="typeof config.suffixIcon == 'string' ? config.suffixIcon : ''"
:prefix-icon="typeof config.prefixIcon == 'string' ? config.prefixIcon : ''"
:disabled="config.disabled"
:rows="config.rows || 2"
:class="config.class"
:style="config.style"
@click="($event) => typeof config.click == 'function' && config.click(value, $event)"
@mousewheel.prevent
@keydown.up.prevent
@keydown.down.prevent
>
<template #suffix>
<ex-slot v-if="typeof config.suffixIcon == 'function'" :render="config.suffixIcon" :value="value" />
</template>
<template #prefix>
<ex-slot v-if="typeof config.prefixIcon == 'function'" :render="config.prefixIcon" :value="value" />
</template>
</el-input>
<el-select
ref="select"
v-else-if="config.type == 'select'"
v-model="value"
:clearable="config.clearable"
:placeholder="config.placeholder || '请选择'"
:multiple="config.multiple"
:filterable="config.filterable"
:disabled="config.disabled"
:loading="config.loading"
:loading-text="config.loadingText"
:class="config.class"
:style="config.style"
:teleported="false"
:fit-input-width="config.fitInputWidth || false"
:collapse-tags="config.collapseTags"
:collapse-tags-tooltip="config.collapseTagsTooltip"
:max-collapse-tags="config.maxCollapseTags"
@change="($event) => typeof config.change == 'function' && config.change($event, $refs.select)"
@visible-change="($event) => typeof config.visibleChange == 'function' && config.visibleChange($event, $refs.select)"
>
<el-option
v-for="(item, index) in options"
:key="index"
:value="config.props?.value ? item[config.props?.value] : item"
:label="config.props?.label ? item[config.props?.label] : item"
/>
</el-select>
<el-tree-select
v-else-if="config.type == 'treeselect'"
v-model="value"
:node-key="config.nodeKey || 'id'"
:data="config.options"
:props="config.props"
:clearable="config.clearable"
:placeholder="config.placeholder || '请选择'"
:multiple="config.multiple"
:filterable="config.filterable"
:disabled="config.disabled"
:check-strictly="config.checkStrictly"
:show-checkbox="config.showCheckbox"
:check-on-click-node="config.checkOnClickNode"
:loading="config.loading"
:loading-text="config.loadingText"
:highlight-current="config.highlightCurrent"
:class="config.class"
:style="config.style"
@change="($event) => typeof config.change == 'function' && config.change($event)"
/>
<el-date-picker
v-else-if="['year', 'month', 'date', 'dates', 'datetime', 'week', 'datetimerange', 'daterange', 'monthrange'].includes(config.type)"
v-model="value"
:clearable="config.clearable"
:editable="config.editable"
:type="config.type"
:placeholder="config.placeholder || '请选择'"
:range-separator="config.rangeSeparator"
:start-placeholder="config.startPlaceholder || '开始时间'"
:end-placeholder="config.endPlaceholder || '结束时间'"
:default-time="config.defaultTime"
:default-value="config.defaultValue"
:value-format="config.valueFormat"
:disabled="config.disabled"
:shortcuts="config.shortcuts"
:class="config.class"
:style="config.style"
@change="($event) => typeof config.change == 'function' && config.change($event)"
/>
<el-time-select
v-else-if="config.type == 'timeselect'"
v-model="value"
:type="config.type"
:start="config.start || '00:00'"
:step="config.step || '00:01'"
:end="config.start || '23:59'"
:placeholder="config.placeholder || '请选择'"
:class="config.class"
:style="config.style"
/>
<el-time-picker
v-else-if="config.type == 'timepicker'"
v-model="value"
:editable="config.editable"
:is-range="config.isRange"
:placeholder="config.placeholder || '请选择'"
:start-placeholder="config.startPlaceholder || '开始时间'"
:end-placeholder="config.endPlaceholder || '结束时间'"
:range-separator="config.rangeSeparator"
:clearable="config.clearable"
:class="config.class"
:style="config.style"
/>
<el-autocomplete
v-else-if="config.type == 'autocomplete'"
v-model="value"
:disabled="config.disabled"
:value-key="config.valueKey || 'value'"
:debounce="config.debounce || 300"
:placement="config.placement || 'bottom-start'"
:fetch-suggestions="config.fetchSuggestions"
:clearable="config.clearable"
:placeholder="config.placeholder || '请输入'"
@select="($event) => typeof config.select == 'function' && config.select($event)"
/>
</template>
<script>
import exSlot from '@/components/FormTable/exSlot';
import request from '@/utils/request';
import { useDictStore } from '@/store/modules/dict';
export default {
name: 'form-elem',
components: {
exSlot
},
props: {
config: Object,
start: {
type: String,
default: ''
},
end: {
type: String,
default: ''
}
},
data() {
return {
dictOptions: []
};
},
computed: {
options() {
return this.config.options || this.dictOptions;
}
},
created() {
this.requestApi();
this.loadDict();
},
methods: {
requestApi() {
if (this.config.request) {
request({
url: this.config.request.url,
method: this.config.request.method,
params: this.config.request.params
}).then((res) => {
if (res.data.success) {
// eslint-disable-next-line vue/no-mutating-props
this.config.options = res.data.data;
}
});
}
},
async loadDict() {
if (this.config.dict) {
const dictStore = useDictStore(); //
let dictData = dictStore.getDict(this.config.dict); //
if (!dictData) {
//
const response = await request({
url: `/system/dict/data/type/${this.config.dict}`,
method: 'get'
});
dictData = response.data.map((item) => ({
label: item.dictLabel,
value: item.dictValue
}));
dictStore.setDict(this.config.dict, dictData); //
}
this.dictOptions = dictData; //
}
}
}
};
</script>
<style lang="scss" scoped>
:deep(input::-webkit-outer-spin-button),
:deep(input::-webkit-inner-spin-button) {
-webkit-appearance: none !important;
}
:deep(input[type='number']) {
-moz-appearance: textfield !important;
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<div class="w-100" ref="wrap">
<el-form :model="model" @submit.prevent @keyup.enter="search" inline>
<el-form-item
v-for="(item, index) in config"
:key="index"
:ref="item.prop || `${item.start}_${item.end}`"
:label="`${item.label}`"
label-width="auto"
>
<form-elem v-model="model[item.prop]" :config="item" />
</el-form-item>
<el-form-item ref="search" style="margin-right: 0px">
<template v-if="isSearch">
<el-button type="primary" icon="Search" @click="search">搜索</el-button>
<el-button type="warning" icon="Refresh" v-if="isReset" @click="reset">重置</el-button>
</template>
<slot />
</el-form-item>
</el-form>
<template v-if="suffixConfig.length > 0">
<el-form v-if="visible" :model="model" @submit.prevent @keyup.enter="search" inline>
<el-form-item
v-for="(item, index) in suffixConfig"
:key="index"
:ref="item.prop || `${item.start}_${item.end}`"
:label="`${item.label}`"
label-width="auto"
>
<form-elem v-model="model[item.prop]" :config="item" />
</el-form-item>
</el-form>
<div class="cursor-pointer" @click="showMore">
<el-divider>
{{ visible ? '折叠' : '更多' }}
<el-icon>
<ArrowUp v-if="visible" />
<ArrowDown v-else />
</el-icon>
</el-divider>
</div>
</template>
</div>
</template>
<script>
import { validatenull } from '@/utils/validate';
import formElem from './formElem.vue';
export default {
name: 'search-form',
components: {
'form-elem': formElem
},
props: {
//
model: {
type: Object,
default: () => {}
},
//
configs: {
type: Array,
default: () => []
},
//
isReset: {
type: Boolean,
default: true
},
//
isSearch: {
type: Boolean,
default: true
}
},
data() {
return {
visible: false,
prefixConfig: [],
suffixConfig: []
};
},
computed: {
config() {
return this.prefixConfig.length > 0 ? this.prefixConfig : this.configs;
}
},
watch: {
configs: 'resize'
},
beforeMount() {
const dicts = [];
this.configs.forEach((item) => item.dict && !dicts.includes(item.dict) && dicts.push(item.dict));
},
mounted() {
this.resize();
},
methods: {
showMore() {
this.visible = !this.visible;
this.$emit('toggle', this.visible);
},
resize() {
this.prefixConfig = [];
this.suffixConfig = [];
const wrapWidth = this.$refs.wrap.offsetWidth;
const btnWidth = this.$refs.search?.$el.offsetWidth || 0;
const formWidth = wrapWidth - btnWidth - 1;
let width = 0;
for (let index = 0; index < this.configs.length; index++) {
const item = this.configs[index];
const prop = item.prop || `${item.start}_${item.end}`;
this.initialize(item);
this.$nextTick(() => {
if (validatenull(item.width) && this.$refs[prop]) {
const elem = this.$refs[prop][0].$el;
item.width = elem.offsetWidth + 32;
}
width += item.width;
if (index == 0 && item.width > formWidth) {
this.prefixConfig.push(item);
} else {
width > formWidth ? this.suffixConfig.push(item) : this.prefixConfig.push(item);
}
});
}
this.$nextTick(() => this.$emit('resize'));
},
initialize(item) {
if (!('type' in item)) item.type = 'text';
if (!('clearable' in item)) item.clearable = true;
if (!('props' in item) && item.dict) {
item.props = {
value: 'value',
label: 'label'
};
}
if (!('placeholder' in item)) {
if (['text', 'number', 'autocomplete'].includes(item.type)) {
item.placeholder = `请输入${item.label}`;
} else if (['select', 'treeselect', 'year', 'month', 'week', 'date', 'datetime'].includes(item.type)) {
item.placeholder = `请选择${item.label}`;
}
}
if (['select', 'treeselect'].includes(item.type)) {
if (item.loading && !('loadingText' in item)) item.loadingText = '加载中...';
if (item.type == 'treeselect') {
if (!('checkOnClickNode' in item)) item.checkOnClickNode = true;
if (!('checkStrictly' in item)) item.checkStrictly = true;
if (item.multiple && !('showCheckbox' in item)) item.showCheckbox = true;
}
}
if (['datetimerange', 'daterange', 'monthrange'].includes(item.type)) {
if (!('startPlaceholder' in item)) {
item.startPlaceholder = '开始时间';
}
if (!('endPlaceholder' in item)) {
item.endPlaceholder = '结束时间';
}
}
},
search() {
this.$emit('search');
},
reset() {
this.$emit('reset');
}
}
};
</script>
<style lang="scss" scoped>
:deep(.el-form-item--small) {
margin-bottom: 10px;
}
.w-100 {
width: 100%;
}
</style>
<style lang="scss">
.el-divider--horizontal {
margin: 10px 0;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div v-if="isCard" style="height: 100%">
<search-form
ref="search"
:configs="configs"
:model="model"
:is-reset="isReset"
:is-search="isSearch"
@search="search"
@reset="reset"
@toggle="toggle"
@resize="toggle"
>
<slot name="button" />
</search-form>
<div ref="wrap">
<slot />
</div>
</div>
<div v-else style="height: 100%">
<search-form
ref="search"
:configs="configs"
:model="model"
:is-reset="isReset"
:is-search="isSearch"
@search="search"
@reset="reset"
@toggle="toggle"
@resize="toggle"
/>
<div ref="wrap">
<slot />
</div>
</div>
</template>
<script>
import SearchForm from '@/components/SearchForm/index.vue';
export default {
name: 'search-panel',
components: {
'search-form': SearchForm
},
props: {
//
model: {
type: Object,
default: () => {}
},
//
configs: {
type: Array,
default: () => []
},
//
isReset: {
type: Boolean,
default: true
},
//
isSearch: {
type: Boolean,
default: true
},
//
isCard: {
type: Boolean,
default: true
}
},
mounted() {
this.toggle();
},
methods: {
search() {
this.$emit('search');
},
reset() {
this.$emit('reset');
},
toggle() {
if (this.$refs.search) {
this.$nextTick(() => {
const elem = this.$refs.search.$el;
const offsetHeight = elem.offsetHeight;
this.$refs.wrap.style.height = `calc(100% - ${offsetHeight}px)`;
});
}
}
}
};
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,90 @@
<template>
<lay-button-container>
<slot name="prefix"></slot>
<el-button v-if="showAdd" v-hasPermi="[`${permission}:${permissionKey}:add`]" type="primary" :disabled="disAdd" icon="Plus" @click="add" plain>{{
addName
}}</el-button>
<slot name="middle"></slot>
<el-button v-if="showEdit" v-hasPermi="[`${permission}:${permissionKey}:edit`]" type="success" :disabled="disEdit" icon="Edit" @click="edit" plain>{{
editName
}}</el-button>
<slot name="center"></slot>
<el-button v-if="showDel" v-hasPermi="[`${permission}:${permissionKey}:del`]" type="danger" :disabled="disRemove" icon="Delete" @click="remove" plain>{{
delName
}}</el-button>
<slot name="suffix"></slot>
</lay-button-container>
</template>
<script setup>
// props
defineProps({
// key
permission: {
type: String,
default: ''
},
permissionKey: {
type: String,
default: ''
},
disAdd: {
type: Boolean,
default: false
},
disEdit: {
type: Boolean,
default: false
},
disRemove: {
type: Boolean,
default: false
},
showAdd: {
type: Boolean,
default: true
},
showEdit: {
type: Boolean,
default: true
},
showDel: {
type: Boolean,
default: true
},
addName: {
type: String,
default: '新增'
},
editName: {
type: String,
default: '修改'
},
delName: {
type: String,
default: '删除'
},
permissionList: {
type: Object,
default: () => ({})
}
});
// emits
const emit = defineEmits(['add', 'edit', 'remove']);
//
const add = () => {
emit('add');
};
const edit = () => {
emit('edit');
};
const remove = () => {
emit('remove');
};
</script>
<style lang="scss" scoped></style>

View File

@ -81,5 +81,74 @@ export default {
layoutSetting: 'Layout Setting',
personalCenter: 'Personal Center',
logout: 'Logout'
},
// layui-vue国际化
input: {
placeholder: 'please input'
},
page: {
previous: 'previous',
next: 'next',
goTo: 'Go to',
confirm: 'confirm',
page: 'page',
item: 'item',
total: 'total'
},
table: {
filter: 'filter',
export: 'export',
print: 'print'
},
datePicker: {
year: '',
month: 'month',
sunday: 'SU',
monday: 'MO',
tuesday: 'TU',
wednesday: 'WE',
thursday: 'TH',
friday: 'FR',
saturday: 'SA',
january: 'January',
february: 'February',
march: 'March',
april: 'April',
may: 'May',
june: 'June',
july: 'July',
august: 'August',
september: 'September',
october: 'October',
november: 'November',
december: 'December',
selectDate: 'select date',
selectTime: 'select time',
selectYear: 'select year',
selectMonth: 'select month',
confirm: 'confirm',
cancel: 'cancel',
now: 'now',
startTime: 'start time',
endTime: 'end time'
},
empty: {
description: 'No data'
},
upload: {
text: 'Upload files',
dragText: 'Click Upload or drag the file here',
defaultErrorMsg: 'Upload failed',
urlErrorMsg: 'The upload address format is illegal',
numberErrorMsg: 'The number of files uploaded exceeds the specified number',
cutInitErrorMsg: 'Clipping plug-in initialization failed',
uploadSuccess: 'Upload succeeded',
cannotSupportCutMsg:
'The current version does not support single multiple file clipping. Try to set multiple to false, and get the returned file object through @ done',
occurFileSizeErrorMsg: 'File size warning,The maximum file size cannot exceed target KB',
startUploadMsg: 'Upload Start',
confirmBtn: 'confirm',
cancelBtn: 'cancel',
title: 'title'
}
};

View File

@ -81,5 +81,73 @@ export default {
layoutSetting: '布局设置',
personalCenter: '个人中心',
logout: '退出登录'
},
// layui-vue国际化
input: {
placeholder: '请输入'
},
page: {
previous: '上一页',
next: '下一页',
goTo: '到第',
confirm: '确认',
page: '页',
item: '条',
total: '共'
},
table: {
filter: '筛选',
export: '导出',
print: '打印'
},
datePicker: {
year: '年',
month: '月',
sunday: '日',
monday: '一',
tuesday: '二',
wednesday: '三',
thursday: '四',
friday: '五',
saturday: '六',
january: '1月',
february: '2月',
march: '3月',
april: '4月',
may: '5月',
june: '6月',
july: '7月',
august: '8月',
september: '9月',
october: '10月',
november: '11月',
december: '12月',
selectDate: '选择日期',
selectTime: '选择时间',
selectYear: '选择年份',
selectMonth: '选择月份',
confirm: '确认',
cancel: '取消',
now: '现在',
startTime: '开始时间',
endTime: '结束时间'
},
empty: {
description: '无数据'
},
upload: {
text: '上传文件',
dragText: '点击上传,或将文件拖拽到此处',
defaultErrorMsg: '上传失败',
urlErrorMsg: '上传地址格式不合法',
numberErrorMsg: '文件上传超过规定的个数',
cutInitErrorMsg: '剪裁插件初始化失败',
uploadSuccess: '上传成功',
cannotSupportCutMsg: '当前版本暂不支持单次多文件剪裁,尝试设置 multiple 为 false, 通过 @done 获取返回文件对象',
occurFileSizeErrorMsg: '文件大小超过限制,文件最大不可超过传入的指定size属性的KB数',
startUploadMsg: '开始上传',
confirmBtn: '确认',
cancelBtn: '取消',
title: '标题'
}
};

View File

@ -55,9 +55,10 @@ function addIframe() {
.app-main {
/* 50= navbar 50 */
min-height: calc(100vh - 50px);
height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
overflow: auto;
}
.fixed-header + .app-main {
@ -68,6 +69,7 @@ function addIframe() {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 84px);
height: calc(100vh - 84px);
}
.fixed-header + .app-main {

View File

@ -8,6 +8,8 @@ import '@/assets/styles/index.scss';
import App from './App.vue';
import store from './store';
import router from './router';
import Layui from '@layui/layui-vue';
import '@layui/layui-vue/lib/index.css';
// 自定义指令
import directive from './directive';
@ -51,6 +53,7 @@ app.use(HighLight);
app.use(ElementIcons);
app.use(router);
app.use(store);
app.use(Layui);
app.use(i18n);
app.use(VXETable);
app.use(plugins);

413
src/utils/common.ts Normal file
View File

@ -0,0 +1,413 @@
import { validatenull } from './validate';
/**keydata
* Arr
* value
* key value对应的key()
* condition (||&&)
**/
export function getDataFromTreeByKey(Arr, value, key = 'href', condition = '||') {
let Deep, T, F;
if (Arr.length > 0) {
for (F = Arr.length; F; ) {
T = Arr[--F];
if (typeof key == 'string' && T[key] == value) return T;
if (Array.isArray(key) && key.length > 0) {
const cond = key.map((k) => `T['${k}']==value`);
if (eval(cond.join(condition))) return T;
}
if (T.hasChildren || T.children) {
Deep = getDataFromTreeByKey(T.children, value, key);
if (Deep) return Deep;
}
}
}
}
//数组转树型数据
export function toTree(data, parentKey = 'parentId') {
const result = [];
if (!Array.isArray(data)) {
return result;
}
const map = {};
data.forEach((item) => {
delete item.children;
delete item.hasChildren;
map[item.id] = item;
});
data.forEach((item) => {
const parent = map[item[parentKey]];
if (parent && parent.id != '#') {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
}
//对象数组去重
/*
arr: 数组数据
uniId: 对象内key
*/
export function uniqueFunc(arr, uniId) {
const res = new Map();
return arr.filter((item) => !res.has(getProperty(item, uniId)) && res.set(getProperty(item, uniId), 1));
}
//去除空的children
export function hasChildren(tree) {
return tree.map((item) => {
if (!item.hasChildren) {
delete item.children;
return item;
}
hasChildren(item.children);
return item;
});
}
/*
e: 需补零数字
t: 数字固定长度(2)
*/
export function digit(e, t = 2) {
let i = '';
e = String(e);
for (let a = e.length; a < t; a++) i += '0';
return e < Math.pow(10, t) ? i + (0 | e) : e;
}
/*
key一样key值一样
a1
b2
*/
export function isObjValEqual(a, b) {
//取对象a和b的属性名
const aProps = Object.getOwnPropertyNames(a);
const bProps = Object.getOwnPropertyNames(b);
//判断属性名的length是否一致
if (aProps.length != bProps.length) {
return false;
}
//循环取出属性名,再判断属性值是否一致
for (let i = 0; i < aProps.length; i++) {
const propName = aProps[i];
if (a[propName] !== b[propName]) {
return false;
}
}
return true;
}
/**
*
* file: 文件对象(blob)
* filename: 后台传入文件名
*/
export function downloadFile({ file, filename }) {
const blob = new Blob([file], {
type: 'application/vnd.ms-excel'
});
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, filename);
} else {
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
link.click();
window.URL.revokeObjectURL(link.href);
}
}
export function formVal(form, data = {}) {
Object.keys(form).forEach((key) => {
if (data[key] !== undefined && data[key] !== null) {
form[key] = data[key];
}
});
}
/**
* @description url参数
* @param {object} data,
* @param {Boolean} isPrefix,"?"
* @param {string} arrayFormat indices|brackets|repeat|comma
*/
export function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
const prefix = isPrefix ? '?' : '';
const _result = [];
if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets';
for (const key in data) {
const value = data[key];
// 去掉为空的参数
if (['', undefined, null].indexOf(value) >= 0) {
continue;
}
// 如果值为数组,另行处理
if (value.constructor === Array) {
// e.g. {ids: [1, 2, 3]}
switch (arrayFormat) {
case 'indices':
// 结果: ids[0]=1&ids[1]=2&ids[2]=3
for (let i = 0; i < value.length; i++) {
_result.push(`${key}[${i}]=${value[i]}`);
}
break;
case 'brackets':
// 结果: ids[]=1&ids[]=2&ids[]=3
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`);
});
break;
case 'repeat':
// 结果: ids=1&ids=2&ids=3
value.forEach((_value) => {
_result.push(`${key}=${_value}`);
});
break;
case 'comma':
// 结果: ids=1,2,3
let commaStr = '';
value.forEach((_value) => {
commaStr += (commaStr ? ',' : '') + _value;
});
_result.push(`${key}=${commaStr}`);
break;
default:
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`);
});
}
} else {
_result.push(`${key}=${value}`);
}
}
return _result.length ? prefix + _result.join('&') : '';
}
export function deepClone(obj) {
const newObj = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === 'object') {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj && typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
}
return newObj;
}
/**
* @description 'a.b.c'
* @param {object} obj
* @param {string} key
* @returns {*}
*/
export function getProperty(obj, key) {
if (!obj) {
return;
}
if (typeof key !== 'string' || key === '') {
return '';
}
if (key.indexOf('.') !== -1) {
const keys = key.split('.');
let firstObj = obj[keys[0]] || {};
for (let i = 1; i < keys.length; i++) {
if (firstObj) {
firstObj = firstObj[keys[i]];
}
}
return firstObj;
}
return obj[key];
}
export function findLastIndex(arr, callback, thisArg) {
for (let index = arr.length - 1; index >= 0; index--) {
const value = arr[index];
if (callback.call(thisArg, value, index, arr)) {
return index;
}
}
return -1;
}
export function startToEnd(startTime, endTime = new Date(), equation = true) {
if (equation) {
return Number(new Date(startTime)) <= Number(new Date(endTime));
}
return Number(new Date(startTime)) < Number(new Date(endTime));
}
/**
*
* @param {} val
* @param {} decimal
* @returns
*/
export function toDecimal(val, decimal = 2) {
const power = Math.pow(10, decimal);
let f = parseFloat(val);
if (isNaN(f)) {
return '';
}
f = Math.round(val * power) / power;
return f;
}
/**
*
* @param {*} month
* @param {*} year
* @returns
*/
export function getEndDate(month = new Date().getMonth() + 1, year = new Date().getFullYear()) {
const a = new Date();
return a.setFullYear(year, month, 1), new Date(a.getTime() - 864e5).getDate();
}
/**
*
* @param {} value
* @returns
*/
export function numberToChanie(value) {
const chnNumChar = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
return chnNumChar[value * 1];
}
/**
*
* @param {} file
* @returns
*/
export function getDuration(file) {
const fileName = file.name || '';
const ext = fileName.split('.')[fileName.split('.').length - 1];
return new Promise((resolve, reject) => {
if (ext != 'mp4') {
reject('请上传MP4文件');
return;
}
const url = URL.createObjectURL(file);
const audioElement = new Audio(url);
audioElement.addEventListener('loadedmetadata', (_event) => {
resolve(audioElement.duration);
});
});
}
/**
*
* @param {} seconds
* @returns
*/
export function getTimes(seconds) {
const h = parseInt((seconds / 60 / 60) % 24);
const m = parseInt((seconds / 60) % 60);
const s = parseInt(seconds % 60);
let val = '';
if (h > 0) {
val += `${h}小时`;
}
if (m > 0) {
val += `${m}分钟`;
}
if (!isNaN(s)) {
val += `${s}`;
}
return val;
}
/**
* File文件对象转Bolb对象
* @param {} file
*/
export function FileToBolb(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function (e) {
const arr = e.target.result.split(',');
const data = window.atob(arr[1]);
const mime = arr[0].match(/:(.*?);/)[1];
const ia = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i);
}
resolve(new Blob([ia], { type: mime }));
};
reader.readAsDataURL(file);
});
}
/**
* blob文件对象转File文件对象
* @param {blob对象} blob
* @param {} fileName
* @param {} fileType
* @returns
*/
export function BlobToFile(blob, fileName, fileType) {
return new window.File([blob], fileName, { type: fileType });
}
/**
* FormData
* @param {} obj
* @returns
*/
export function ObjToForm(obj) {
const formData = new FormData();
if (obj && Object.keys(obj).length > 0) {
Object.keys(obj).forEach((key) => {
const val = obj[key];
formData.append(key, val);
});
}
return formData;
}
/**
*
* @param {} pwd
* @returns
*/
export function checkPwd(pwd) {
let level = 0;
if (pwd.length < 8) {
level = 1;
} else {
level = 0;
if (pwd.match(/\d/g)) {
level++;
}
if (pwd.match(/[a-z]/gi)) {
level++;
}
if (pwd.match(/[A-Z]/gi)) {
level++;
}
if (pwd.match("[`~!@#$^&*()=|{}':;',\\[\\].<>《》./~@#¥……&*()——|{}【】‘;:”“'。,、? ]")) {
level++;
}
}
return level;
}
/**
*
* @param {} value
* @returns
*/
export function toLocaleString(value, min = 2, max = 7) {
const imumFractionDigits = { minimumFractionDigits: min, maximumFractionDigits: max };
if (validatenull(value) || isNaN(value)) {
return '';
}
return value.toLocaleString('en-US', imumFractionDigits);
}

View File

@ -106,3 +106,26 @@ export const isArray = (arg: string | string[]) => {
}
return Array.isArray(arg);
};
/**
*
*/
export function validatenull(val) {
if (typeof val == 'boolean') {
return false;
}
if (typeof val == 'number') {
return false;
}
if (val instanceof Array) {
if (val.length == 0) return true;
} else if (val instanceof Object) {
if (JSON.stringify(val) === '{}') return true;
} else {
if (val == 'null' || val == null || val == 'undefined' || val == undefined || val == ''){
return true;
}
return false;
}
return false;
}

View File

@ -0,0 +1,256 @@
<template>
<div class="p-2 h-100">
<el-row :gutter="20" style="height: 100%">
<el-col :lg="24" :xs="24" style="height: 100%">
<el-card shadow="hover" class="h-100">
<search-panel ref="searchRef" :configs="search.configs" :model="search.data" @search="onLoad" @reset="reset">
<form-table
ref="tableRef"
:page="table.page"
height="100%"
:loading="table.loading"
:columns="columns"
:data-source="table.data"
:row-class-name="rowClassName"
v-model:selected-keys="table.selectedKeys"
@change="onLoad"
>
<template #toolbar>
<!-- permission为权限模块permissionKey为权限key根据实际情况进行修改 -->
<toolbar-button
permission="permission"
:permissionKey="'permissionKey'"
:disRemove="table.selectedData.length == 0"
:disEdit="table.selectedData.length != 1"
@add="handleAdd"
@edit="handleEdit(table.selectedData[0])"
@remove="remove"
>
<template #prefix>
<el-button type="warning" plain>前置其他按钮</el-button>
</template>
<template #suffix>
<el-button type="warning" plain>后置其他按钮</el-button>
</template>
</toolbar-button>
</template>
<template #operate="{ row }">
<!-- 可根据实际情况进行修改或封装 -->
<el-button-group>
<el-button type="primary" @click="view(row)">查看</el-button>
</el-button-group>
</template>
</form-table>
</search-panel>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="tsx">
import { listTableDemo } from '@/api/demo/demo/index';
const search = reactive({
data: {},
configs: [
/**
* 搜索内容如果超过一行则只显示一行多余搜索可折叠显示/隐藏
* 支持inputselecttree-selectdate-pickertime-selecttime-pickerautocomplete类型
* 输入框下拉选择框日期选择框时间选择框输入提示框
* 如有其他类型搜索可根据实际情况对组件进行完善
* 具体使用请参照SearchForm组件
*/
{ label: '输入框', prop: 'input' },
// ,options
{
label: '下拉选择框',
prop: 'select',
type: 'select',
dict: 'sys_user_sex',
change: ($event) => {
},
visibleChange: ($event) => {
},
},
{
label: '日期选择框',
prop: 'date',
type: 'date',
valueFormat: 'YYYY-MM-DD',
change: ($event) => {
},
},
{
label: '时间选择框',
prop: 'time',
type: 'daterange',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')],
change: ($event) => {
}
},
{
label: '输入提示框',
prop: 'autocomplete',
type: 'autocomplete',
fetchSuggestions: (queryString, cb) => {
// el-autocomplete
},
select: (item) => {
//
}
}
]
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const activatedFlag = ref(false);
const columns = ref([
{ title: '选择', type: 'checkbox', width: '55px', align: 'center' },
{ title: '序号', type: 'number', align: 'center', width: '55px' },
{ title: '简单显示', key: 'testKey', showOverflowTooltip: true },
/**
* 可以在render中使用tsx语法实现自定义显示任意内容
*/
{
title: 'render显示',
key: 'xxx',
render: ({ row }) => {
return (
<span style="color: #67C23A; cursor: pointer; font-size: 18px" onClick={() => handleChange(row)}>{row.orderNum}</span>
)
}
},
// dict:''
{ title: '字典显示', key: 'gender', dict: 'sys_user_sex' },
{ title: '操作', customSlot: 'operate', align: 'center', width: '200px', fixed: 'right' }
]);
const tableRef = ref();
const table = reactive({
loading: false,
page: {
limit: 30,
limits: [10, 20, 30, 40, 50, 100, 200, 500],
current: 1,
total: 0,
layout: ['count', 'prev', 'page', 'next', 'limits', 'skip'],
theme: 'blue'
},
title: '',
selectedData: [],
selectedKeys: [],
disDelIndex: [],
data: []
});
const onLoad = async () => {
table.loading = true;
const { current, limit } = table.page;
try {
const res = await listTableDemo(current, limit, search.data);
table.page.total = res.total;
table.data = res.rows;
table.selectedData = [];
} finally {
table.loading = false;
}
};
const reset = () => {
search.data = {};
onLoad();
};
//
const rowClassName = (row, rowIndex) => {
if(table.disDelIndex.includes(rowIndex)) return 'bg-warning-8';
};
//
watch(
() => [...table.selectedKeys],
(val) => {
initSelect();
}
);
const initSelect = () => {
if (tableRef.value) {
nextTick(() => {
table.selectedData = tableRef.value.getCheckData();
table.disDelIndex.length > 0 && initDelRow();
});
}
};
const initDelRow = () => {
table.disDelIndex = [];
table.selectedData.forEach(item => {
// 1,
if(item?.id == '1') {
let rowIndex = table.data.findIndex(data => item.id == data.id);
table.disDelIndex.push(rowIndex);
};
});
};
const view = (row: any) => {
//
ElMessage.success('查看操作');
};
const handleAdd = () => {
//
ElMessage.success('新增');
};
const handleEdit = (row: any) => {
//
ElMessage.success('编辑' + row.testKey);
};
const remove = async () => {
//
await initDelRow();
if(table.disDelIndex.length > 0){
ElMessage.warning('当前选中数据有不能删除的数据!');
return ;
}
};
//
const handleChange = (row: any) => {
ElMessage.success('点击' + row.orderNum);
};
onMounted(() => {
onLoad();
});
onActivated(() => {
activatedFlag.value && onLoad();
activatedFlag.value = true;
});
</script>
<style lang="scss" scoped>
.h-100 {
height: 100%;
}
:deep(.bg-warning-8) {
background-color: #f8e3c5;
.layui-table-fixed-left{
background-color: #f8e3c5;
}
.layui-table-fixed-right{
background-color: #f8e3c5;
}
}
</style>

View File

@ -1,4 +1,5 @@
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import vueDevTools from 'vite-plugin-vue-devtools';
import createUnoCss from './unocss';
@ -11,8 +12,14 @@ import createSetupExtend from './setup-extend';
import path from 'path';
export default (viteEnv: any, isBuild = false): [] => {
const vitePlugins: any = [];
vitePlugins.push(vue());
const vitePlugins: any = [
vue({
script: {
defineModel: true
}
}),
vueJsx()
];
vitePlugins.push(vueDevTools());
vitePlugins.push(createUnoCss());
vitePlugins.push(createAutoImport(path));