mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-13 07:43:43 +08:00
基于element-plus二次封装通用表格、搜索组件
This commit is contained in:
parent
f9c3958d5d
commit
c8e3ecac54
@ -23,7 +23,8 @@
|
||||
"@element-plus/icons-vue": "2.3.2",
|
||||
"@highlightjs/vue-plugin": "2.1.2",
|
||||
"@vueup/vue-quill": "1.2.0",
|
||||
"@vueuse/core": "13.9.0",
|
||||
"@vueuse/core": "13.1.0",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.2",
|
||||
"animate.css": "4.1.1",
|
||||
"await-to-js": "3.0.0",
|
||||
"axios": "1.13.1",
|
||||
|
||||
@ -193,6 +193,7 @@ h6 {
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
height: 100%;
|
||||
padding: 15px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
|
||||
370
src/components/FormTable/index.vue
Normal file
370
src/components/FormTable/index.vue
Normal file
@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="table-container">
|
||||
<el-row>
|
||||
<slot name="topButton" />
|
||||
</el-row>
|
||||
<div class="table">
|
||||
<!-- 表格组件 如需其他事件 请参考官方文档在下方添加 -->
|
||||
<el-table
|
||||
id="iTable"
|
||||
ref="iTableRef"
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载中..."
|
||||
element-loading-spinner="el-icon-loading"
|
||||
:height="height"
|
||||
:max-height="maxHeight"
|
||||
:row-key="rowKey"
|
||||
:data="tableData"
|
||||
show-overflow-tooltip
|
||||
v-bind="options"
|
||||
@sort-change="sortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
:tree-props="treeProps"
|
||||
stripe
|
||||
>
|
||||
<!-- 渲染所有列,包括多级表头 -->
|
||||
<template v-for="(column, index) in processedHeaders" :key="index + (column.prop || '')">
|
||||
<!-- 特殊列处理:selection、index 其他后续补充 -->
|
||||
<el-table-column
|
||||
v-if="column.type === 'selection'"
|
||||
type="selection"
|
||||
:width="column.width || 50"
|
||||
:align="column.align || 'center'"
|
||||
:fixed="column.fixed"
|
||||
/>
|
||||
<el-table-column
|
||||
v-else-if="column.type === 'index'"
|
||||
type="index"
|
||||
:label="column.label || '序号'"
|
||||
:width="column.width || 60"
|
||||
:align="column.align || 'center'"
|
||||
:index="column.indexMethod"
|
||||
:fixed="column.fixed"
|
||||
/>
|
||||
<!-- 多级表头处理 -->
|
||||
<el-table-column
|
||||
v-else-if="column.children && column.children.length"
|
||||
:label="column.label"
|
||||
:align="column.align || 'center'"
|
||||
:min-width="headSpanFit(column)"
|
||||
>
|
||||
<!-- 递归渲染子列 -->
|
||||
<template v-for="(child, childIndex) in column.children" :key="childIndex + (child.prop || '')">
|
||||
<el-table-column v-if="isColumnVisible(child)" :min-width="headSpanFit(child)" v-bind="child">
|
||||
<!-- 子列的插槽和内容渲染(与普通列相同) -->
|
||||
<template v-for="(value, key) in child.slot" #[key]="scope">
|
||||
<slot :name="value" v-bind="scope"></slot>
|
||||
</template>
|
||||
<template v-if="!child.slot" #default="scope">
|
||||
<template v-if="child.render">
|
||||
<div v-if="isString(getRenderContent(child, scope))" @click.stop="child.render(scope)">
|
||||
<div v-html="getRenderContent(child, scope)"></div>
|
||||
</div>
|
||||
<component v-else :is="child.render" v-bind="scope" :row="scope.row" :index="scope.$index" @click.stop />
|
||||
</template>
|
||||
<template v-else-if="child.formatter">
|
||||
<span v-html="child.formatter(scope.row, child)"></span>
|
||||
</template>
|
||||
<template v-else-if="child.dict">
|
||||
<dict-tag :options="useDict(child.dict)[child.dict]" :value="scope.row[child.prop]" />
|
||||
</template>
|
||||
<template v-else-if="!child.type">
|
||||
<span>{{ scope.row[child.prop] ?? '--' }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 普通列 -->
|
||||
<el-table-column v-else-if="isColumnVisible(column)" :min-width="headSpanFit(column)" v-bind="column">
|
||||
<!-- 渲染插槽 -->
|
||||
<template v-for="(value, key) in column.slot" #[key]="scope">
|
||||
<slot :name="value" v-bind="scope"></slot>
|
||||
</template>
|
||||
|
||||
<!-- 渲染默认内容 -->
|
||||
<template v-if="!column.slot" #default="scope">
|
||||
<!-- 渲染render -->
|
||||
<template v-if="column.render">
|
||||
<div v-if="isString(getRenderContent(column, scope))" @click.stop="column.render(scope)">
|
||||
<div v-html="getRenderContent(column, scope)"></div>
|
||||
</div>
|
||||
<component v-else :is="column.render" v-bind="scope" :row="scope.row" :index="scope.$index" @click.stop />
|
||||
</template>
|
||||
|
||||
<!-- 渲染formatter -->
|
||||
<template v-else-if="column.formatter">
|
||||
<span v-html="column.formatter(scope.row, column)"></span>
|
||||
</template>
|
||||
|
||||
<!-- 渲染字典内容 -->
|
||||
<template v-else-if="column.dict">
|
||||
<dict-tag :options="useDict(column.dict)[column.dict]" :value="scope.row[column.prop]" />
|
||||
</template>
|
||||
|
||||
<!-- 渲染默认内容 -->
|
||||
<template v-else-if="!column.type">
|
||||
<span>{{ scope.row[column.prop] ?? '--' }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<el-table-column v-if="operates.list?.length" label="操作" align="left" :width="operates.width" :fixed="operates.fixed">
|
||||
<template #default="scope">
|
||||
<div class="operate-group">
|
||||
<!-- 渲染外部按钮 -->
|
||||
<template v-for="(item, idx) in getVisibleButtons(scope.row).outside" :key="idx">
|
||||
<el-button
|
||||
v-bind="item"
|
||||
:type="item.type || 'primary'"
|
||||
:size="item.size || 'small'"
|
||||
@click.stop="item.method(scope.row, scope.$index)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<!-- 渲染下拉按钮 -->
|
||||
<el-dropdown v-if="getVisibleButtons(scope.row).inside.length" class="custom-dropdown" trigger="click">
|
||||
<el-button link size="small" class="custom-text">
|
||||
<el-icon :size="18"><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="(item, idx) in getVisibleButtons(scope.row).inside"
|
||||
:key="idx"
|
||||
v-bind="item"
|
||||
@click="item.method(scope.row, scope.$index)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="!hidden" class="pagination-container" :style="{ justifyContent: paginationJustify }">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:background="background"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:pager-count="pagerCount"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, useSlots, watch } from 'vue'
|
||||
import { MoreFilled } from '@element-plus/icons-vue'
|
||||
import { scrollTo } from '@/utils/scroll-to'
|
||||
import { isString, isFunction } from '@/utils/index'
|
||||
import { havePermi } from '@/plugins/auth'
|
||||
|
||||
defineOptions({ name: 'FormTable' })
|
||||
|
||||
const props = defineProps({
|
||||
height: { type: String, default: '300px' },
|
||||
maxHeight: { type: String, default: '100%' },
|
||||
loading: { type: Boolean, default: false },
|
||||
columns: { type: Array, default: () => [] },
|
||||
tableData: { type: Array, default: () => [] },
|
||||
total: { type: Number, default: 0 },
|
||||
page: { type: Number, default: 1 },
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({ stripe: false, highlightCurrentRow: false, border: true })
|
||||
},
|
||||
limit: { type: Number, default: 20 },
|
||||
pageSizes: { type: Array, default: () => [10, 20, 30, 50] },
|
||||
pagerCount: {
|
||||
type: Number,
|
||||
default: () => (typeof document !== 'undefined' && document.body.clientWidth < 992 ? 5 : 7)
|
||||
},
|
||||
layout: { type: String, default: 'total, sizes, prev, pager, next, jumper' },
|
||||
background: { type: Boolean, default: true },
|
||||
autoScroll: { type: Boolean, default: true },
|
||||
hidden: { type: Boolean, default: false },
|
||||
operates: { type: Object, default: () => ({ list: [] }) },
|
||||
paginationPosition: { type: String, default: 'right' },
|
||||
treeProps: {
|
||||
type: Object,
|
||||
default: () => ({ children: 'children', hasChildren: 'hasChildren' })
|
||||
},
|
||||
rowKey: { type: String, default: 'id' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:page', 'update:limit', 'pagination', 'sortChange', 'handleSelectionChange'])
|
||||
|
||||
const slots = useSlots()
|
||||
const iTableRef = ref(null)
|
||||
const processedHeaders = ref([])
|
||||
|
||||
/** 1. 逻辑优化:使用 watch 监听 columns 变化,避免在 onBeforeUpdate 中过度计算 **/
|
||||
watch(
|
||||
() => [props.columns, slots],
|
||||
() => {
|
||||
processedHeaders.value = props.columns.map(column => {
|
||||
const col = { ...column }
|
||||
if (!col.key) col.key = col.prop
|
||||
|
||||
// 匹配插槽逻辑保持不变
|
||||
Object.keys(slots).forEach(key => {
|
||||
const res = key.match(/^(\S+)-(\S+)/)
|
||||
|
||||
if (res && res[2] === col.key) {
|
||||
col.slot = { ...col.slot, [res[1]]: res[0] }
|
||||
}
|
||||
})
|
||||
|
||||
return col
|
||||
})
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
/** 2. 工具函数 **/
|
||||
|
||||
// 计算表头宽度
|
||||
const headSpanFit = column => {
|
||||
const labelLen = column?.label?.length || 0
|
||||
return Math.max(labelLen * 20, 100)
|
||||
}
|
||||
|
||||
// 判断列是否显示
|
||||
const isColumnVisible = column => {
|
||||
if (isFunction(column.show)) return column.show()
|
||||
return column.show !== false
|
||||
}
|
||||
|
||||
// 获取渲染内容
|
||||
const getRenderContent = (column, scope) => {
|
||||
return isFunction(column.render) ? column.render(scope) : column.render
|
||||
}
|
||||
|
||||
// 获取可见按钮
|
||||
const getVisibleButtons = row => {
|
||||
const list = (props.operates?.list || []).filter(item => {
|
||||
if (item.permission) {
|
||||
return havePermi(item.permission)
|
||||
}
|
||||
return isFunction(item.show) ? item.show(row) : item.show !== false
|
||||
})
|
||||
return list.length > 3
|
||||
? {
|
||||
outside: list.slice(0, 2),
|
||||
inside: list.slice(2)
|
||||
}
|
||||
: {
|
||||
outside: list,
|
||||
inside: []
|
||||
}
|
||||
}
|
||||
|
||||
/** 3. 分页计算属性 **/
|
||||
// 当前页码
|
||||
const currentPage = computed({
|
||||
get: () => props.page,
|
||||
set: val => emit('update:page', val)
|
||||
})
|
||||
|
||||
// 每页条数
|
||||
const pageSize = computed({
|
||||
get: () => props.limit,
|
||||
set: val => emit('update:limit', val)
|
||||
})
|
||||
|
||||
// 分页对齐方式
|
||||
const paginationJustify = computed(() => {
|
||||
const map = { left: 'flex-start', center: 'center', right: 'flex-end' }
|
||||
return map[props.paginationPosition] || 'flex-end'
|
||||
})
|
||||
|
||||
/** 4. 事件处理 **/
|
||||
// 改变每页条数
|
||||
const handleSizeChange = val => {
|
||||
if (currentPage.value * val > props.total) currentPage.value = 1
|
||||
emit('pagination', { page: currentPage.value, limit: val })
|
||||
if (props.autoScroll) scrollTo(0, 800)
|
||||
}
|
||||
|
||||
// 改变当前页
|
||||
const handleCurrentChange = val => {
|
||||
emit('pagination', { page: val, limit: pageSize.value })
|
||||
if (props.autoScroll) scrollTo(0, 800)
|
||||
}
|
||||
|
||||
// 改变排序
|
||||
const sortChange = args => emit('sortChange', args)
|
||||
|
||||
// 改变选择
|
||||
const handleSelectionChange = val => emit('handleSelectionChange', val)
|
||||
|
||||
defineExpose({ iTableRef })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.table {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
:deep(.el-table) {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.operate-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-button + .el-button,
|
||||
.el-button + .custom-dropdown {
|
||||
margin-left: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background-color: #dcdfe6;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.custom-text) {
|
||||
color: #606266;
|
||||
padding: 0;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/components/SearchForm/exSlot.tsx
Normal file
34
src/components/SearchForm/exSlot.tsx
Normal 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;
|
||||
}
|
||||
});
|
||||
215
src/components/SearchForm/formElem.vue
Normal file
215
src/components/SearchForm/formElem.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<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/SearchForm/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>
|
||||
185
src/components/SearchForm/index.vue
Normal file
185
src/components/SearchForm/index.vue
Normal 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>
|
||||
95
src/components/SearchPanel/index.vue
Normal file
95
src/components/SearchPanel/index.vue
Normal 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>
|
||||
115
src/components/ToolbarButton/index.vue
Normal file
115
src/components/ToolbarButton/index.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<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}:remove`, `${permission}:${permissionKey}:del`]"
|
||||
type="danger"
|
||||
:disabled="disRemove"
|
||||
icon="Delete"
|
||||
@click="remove"
|
||||
plain
|
||||
>
|
||||
{{ delName }}
|
||||
</el-button>
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 定义 props
|
||||
defineProps({
|
||||
// 权限模块
|
||||
permission: {
|
||||
type: String,
|
||||
default: 'device'
|
||||
},
|
||||
// 权限key
|
||||
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>
|
||||
@ -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 {
|
||||
|
||||
@ -24,6 +24,11 @@ const authRole = (role: string): boolean => {
|
||||
}
|
||||
};
|
||||
|
||||
export const havePermi = (permission: string): boolean => {
|
||||
// 你的权限校验逻辑
|
||||
return authPermission(permission);
|
||||
};
|
||||
|
||||
export default {
|
||||
// 验证用户是否具备某权限
|
||||
hasPermi(permission: string): boolean {
|
||||
|
||||
@ -316,3 +316,21 @@ export const removeClass = (ele: HTMLElement, cls: string) => {
|
||||
export const isExternal = (path: string) => {
|
||||
return /^(https?:|http?:|mailto:|tel:)/.test(path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a variable is a string
|
||||
* @param str
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isString = (str: any): str is string => {
|
||||
return typeof str === 'string' || str instanceof String;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a variable is a function
|
||||
* @param func
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isFunction = (func: any): func is Function => {
|
||||
return typeof func === 'function' || func instanceof Function;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
310
src/views/test/index.vue
Normal file
310
src/views/test/index.vue
Normal file
@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="p-2" style="height: 100%;">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<el-card shadow="hover" style="height: 100%;">
|
||||
<search-panel ref="searchRef" :configs="search.configs" :model="search.data" @search="onSearch" @reset="reset">
|
||||
<FormTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
height="100%"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:table-data="tableData"
|
||||
:columns="columns"
|
||||
:operates="operates"
|
||||
:total="total"
|
||||
v-model:page="page"
|
||||
v-model:limit="limit"
|
||||
:options="{
|
||||
border: true,
|
||||
stripe: true,
|
||||
highlightCurrentRow: true
|
||||
}"
|
||||
pagination-position="right"
|
||||
@handleSelectionChange="handleSelectionChange"
|
||||
@pagination="onPagination"
|
||||
>
|
||||
<template #topButton>
|
||||
<toolbar-button
|
||||
:permission="'permission'"
|
||||
:permissionKey="'permissionKey'"
|
||||
:disRemove="selectData.length == 0"
|
||||
:disEdit="selectData.length != 1"
|
||||
delName="批量删除"
|
||||
@add="handleAdd"
|
||||
@remove="handleRemoveAll"
|
||||
/>
|
||||
</template>
|
||||
<!-- 使用语法定义插槽: [类型]-[key/prop] -->
|
||||
<!-- header-name 定义姓名列的表头 -->
|
||||
<template #header-name="{ column }">
|
||||
<el-tag effect="dark">自定义 {{ column.label }}</el-tag>
|
||||
</template>
|
||||
|
||||
<!-- default-name 定义姓名列的内容 -->
|
||||
<template #default-name="{ row }">
|
||||
<span style="font-weight: bold; color: #67c23a">{{ row.name }}</span>
|
||||
</template>
|
||||
</FormTable>
|
||||
</search-panel>
|
||||
</el-card>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { h, ref, reactive, computed } from 'vue'
|
||||
import { ElTag, ElMessage } from 'element-plus'
|
||||
import * as iconsVue from '@element-plus/icons-vue'
|
||||
|
||||
// 表格引用
|
||||
const tableRef = ref(null)
|
||||
|
||||
// 模拟数据
|
||||
const tableData = ref([
|
||||
{
|
||||
id: 1,
|
||||
date: '2026-05-03',
|
||||
name: '11111',
|
||||
gender: 1,
|
||||
age: 35,
|
||||
city: '北京',
|
||||
price: 699,
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '2026-05-02',
|
||||
name: '22222',
|
||||
gender: 0,
|
||||
age: 45,
|
||||
city: '上海',
|
||||
price: 999,
|
||||
status: 0,
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
date: '2026-05-02',
|
||||
name: '22222-1',
|
||||
gender: 0,
|
||||
age: 45,
|
||||
city: '上海',
|
||||
price: 999,
|
||||
status: 0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
date: '2026-05-02',
|
||||
name: '22222-2',
|
||||
gender: 0,
|
||||
age: 45,
|
||||
city: '上海',
|
||||
price: 999,
|
||||
status: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
date: '2026-05-04',
|
||||
name: '33333',
|
||||
gender: 1,
|
||||
age: 25,
|
||||
city: '杭州',
|
||||
price: 1398,
|
||||
status: 1
|
||||
}
|
||||
])
|
||||
|
||||
// 分页配置
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const total = ref(100)
|
||||
|
||||
// 选中数据
|
||||
const selectData = ref([])
|
||||
|
||||
// 搜索配置
|
||||
const search = reactive({
|
||||
data: {},
|
||||
configs: [
|
||||
/**
|
||||
* 搜索内容,如果超过一行则只显示一行,多余搜索可折叠显示/隐藏
|
||||
* 支持input、select、tree-select、date-picker、time-select、time-picker、autocomplete类型
|
||||
* 例:输入框、下拉选择框、日期选择框、时间选择框、输入提示框
|
||||
* 如有其他类型搜索,可根据实际情况对组件进行完善
|
||||
* 具体使用请参照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 columns = computed(() => [
|
||||
// 1. 多选
|
||||
{ type: 'selection', width: 50 },
|
||||
// 2. 序号
|
||||
{ type: 'index', label: '序号', width: 60, align: 'center' },
|
||||
// 3. 普通列
|
||||
{ prop: 'date', label: '日期', width: 120, fixed: 'left' },
|
||||
// 4. 插槽列 (使用语法: [type]-[prop])
|
||||
{ prop: 'name', label: '姓名', width: 120, key: 'name' },
|
||||
// 5. 渲染函数列
|
||||
{
|
||||
prop: 'gender',
|
||||
label: '性别',
|
||||
width: 80,
|
||||
render: ({ row }) => h(ElTag, { type: row.gender ? 'primary' : 'danger' }, () => (row.gender ? '男' : '女'))
|
||||
},
|
||||
// 6. 字符串渲染 (HTML)
|
||||
{
|
||||
prop: 'age',
|
||||
label: '年龄',
|
||||
width: 80,
|
||||
render: ({ row }) => `<b style="color: #409eff">${row.age} 岁</b>`
|
||||
},
|
||||
// 7. 格式化列 (formatter)
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 100,
|
||||
// dict: 'sys_user_sex' // 使用字典
|
||||
formatter: row => (row.status ? "<span style='color: green'>正常</span>" : "<span style='color: red'>禁用</span>")
|
||||
},
|
||||
{ prop: 'city', label: '城市' },
|
||||
// 8. 价格列
|
||||
{
|
||||
prop: 'price',
|
||||
label: '价格',
|
||||
formatter: row => `¥ ${row.price.toFixed(2)}`
|
||||
},
|
||||
// 9. 多级表头
|
||||
{
|
||||
label: '更多信息',
|
||||
children: [
|
||||
{ prop: 'date', label: '购买日期', width: 120 },
|
||||
{ prop: 'price', label: '购买价格', formatter: row => `¥ ${row.price.toFixed(2)}` }
|
||||
]
|
||||
},
|
||||
// 10. JSX 语法
|
||||
{
|
||||
prop: 'city',
|
||||
label: '城市(JSX)',
|
||||
width: 120,
|
||||
render: ({ row }) => {
|
||||
return <ElTag type="info" onClick={() => console.log('点击城市:', row.city)}>{row.city}</ElTag>
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 操作按钮配置
|
||||
const operates = {
|
||||
width: 260,
|
||||
fixed: 'right',
|
||||
list: [
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary',
|
||||
icon: iconsVue.Edit,
|
||||
// permission: 'xxx:xxx:xxx', // 权限字符控制
|
||||
method: (row, index) => {
|
||||
ElMessage.info(`编辑第 ${index + 1} 行: ${row.name}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '查看',
|
||||
type: 'success',
|
||||
icon: iconsVue.View,
|
||||
show: row => row.status === 1, // 条件显示
|
||||
method: row => {
|
||||
console.log('查看详情:', row)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'danger',
|
||||
icon: iconsVue.Delete,
|
||||
method: row => {
|
||||
ElMessage.error(`删除: ${row.name}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '更多选项1',
|
||||
method: row => console.log('更多1', row)
|
||||
},
|
||||
{
|
||||
label: '更多选项2',
|
||||
method: row => console.log('更多2', row)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const handleSelectionChange = val => {
|
||||
console.log('选中项变化:', val)
|
||||
selectData.value = val
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
// 重置选中数据
|
||||
selectData.value = []
|
||||
console.log('搜索条件:', search.data)
|
||||
ElMessage.success('搜索触发')
|
||||
}
|
||||
|
||||
const onPagination = ({ page, limit }) => {
|
||||
// 重置选中数据
|
||||
selectData.value = []
|
||||
ElMessage.success(`分页变化: 第${page}页, 每页${limit}条`)
|
||||
}
|
||||
const reset = () => {
|
||||
ElMessage.info('重置搜索表单')
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
:deep(.el-table) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@ -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));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user