项目初始化
This commit is contained in:
300
web/src/components/table/comSearch/index.vue
Normal file
300
web/src/components/table/comSearch/index.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="table-com-search-wrapper">
|
||||
<div class="table-com-search">
|
||||
<el-form
|
||||
@submit.prevent=""
|
||||
@keyup.enter="baTable.onTableAction('com-search', { event: 'submit-com-search-form' })"
|
||||
label-position="top"
|
||||
:model="baTable.comSearch.form"
|
||||
>
|
||||
<el-row>
|
||||
<template v-for="(item, idx) in baTable.table.column" :key="idx">
|
||||
<template v-if="item.operator !== false">
|
||||
<!-- 自定义渲染 component、slot -->
|
||||
<el-col
|
||||
v-if="item.comSearchRender == 'customRender' || item.comSearchRender == 'slot'"
|
||||
v-bind="{
|
||||
xs: item.comSearchColAttr?.xs ? item.comSearchColAttr?.xs : 24,
|
||||
sm: item.comSearchColAttr?.sm ? item.comSearchColAttr?.sm : 6,
|
||||
...item.comSearchColAttr,
|
||||
}"
|
||||
>
|
||||
<!-- 外部可以使用 :deep() 选择器修改css样式 -->
|
||||
<div class="com-search-col" :class="item.prop">
|
||||
<div class="com-search-col-label" v-if="item.comSearchShowLabel !== false">{{ item.label }}</div>
|
||||
<div class="com-search-col-input">
|
||||
<!-- 自定义组件/函数渲染 -->
|
||||
<component
|
||||
v-if="item.comSearchRender == 'customRender'"
|
||||
:is="item.comSearchCustomRender"
|
||||
:renderRow="item"
|
||||
:renderField="item.prop!"
|
||||
:renderValue="baTable.comSearch.form[item.prop!]"
|
||||
/>
|
||||
|
||||
<!-- 自定义渲染-slot -->
|
||||
<slot v-else-if="item.comSearchRender == 'slot'" :name="item.comSearchSlotName"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 时间日期范围 -->
|
||||
<el-col
|
||||
v-else-if="
|
||||
(item.render == 'datetime' || item.comSearchRender == 'datetime' || item.comSearchRender == 'date') &&
|
||||
(item.operator == 'RANGE' || item.operator == 'NOT RANGE')
|
||||
"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
>
|
||||
<div class="com-search-col" :class="item.prop">
|
||||
<div class="com-search-col-label w16" v-if="item.comSearchShowLabel !== false">{{ item.label }}</div>
|
||||
<div class="com-search-col-input-range w83">
|
||||
<el-date-picker
|
||||
class="datetime-picker w100"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
:type="item.comSearchRender == 'date' ? 'daterange' : 'datetimerange'"
|
||||
:range-separator="$t('To')"
|
||||
:start-placeholder="getPlaceholder(item.operatorPlaceholder, 0, $t('el.datepicker.startDate'))"
|
||||
:end-placeholder="getPlaceholder(item.operatorPlaceholder, 1, $t('el.datepicker.endDate'))"
|
||||
:value-format="item.comSearchRender == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
:teleported="false"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 时间范围 -->
|
||||
<el-col
|
||||
v-else-if="item.comSearchRender == 'time' && (item.operator == 'RANGE' || item.operator == 'NOT RANGE')"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
>
|
||||
<div class="com-search-col" :class="item.prop">
|
||||
<div class="com-search-col-label w16" v-if="item.comSearchShowLabel !== false">{{ item.label }}</div>
|
||||
<div class="com-search-col-input-range w83">
|
||||
<el-time-picker
|
||||
class="time-picker w100"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
is-range
|
||||
:default-value="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
:range-separator="$t('To')"
|
||||
:start-placeholder="getPlaceholder(item.operatorPlaceholder, 0, $t('el.datepicker.startTime'))"
|
||||
:end-placeholder="getPlaceholder(item.operatorPlaceholder, 1, $t('el.datepicker.endTime'))"
|
||||
value-format="HH:mm:ss"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- 其他 -->
|
||||
<el-col v-else :xs="24" :sm="6">
|
||||
<div class="com-search-col" :class="item.prop">
|
||||
<div class="com-search-col-label" v-if="item.comSearchShowLabel !== false">{{ item.label }}</div>
|
||||
<!-- 数字范围 -->
|
||||
<div v-if="item.operator == 'RANGE' || item.operator == 'NOT RANGE'" class="com-search-col-input-range">
|
||||
<el-input
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder)"
|
||||
type="string"
|
||||
v-model="baTable.comSearch.form[item.prop! + '-start']"
|
||||
:clearable="true"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
></el-input>
|
||||
<div class="range-separator">{{ $t('To') }}</div>
|
||||
<el-input
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder, 1)"
|
||||
type="string"
|
||||
v-model="baTable.comSearch.form[item.prop! + '-end']"
|
||||
:clearable="true"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
></el-input>
|
||||
</div>
|
||||
<!-- 是否 [NOT] NULL -->
|
||||
<div v-else-if="item.operator == 'NULL' || item.operator == 'NOT NULL'" class="com-search-col-input">
|
||||
<el-checkbox
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:label="item.operator"
|
||||
size="large"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
></el-checkbox>
|
||||
</div>
|
||||
<div v-else-if="item.operator" class="com-search-col-input">
|
||||
<!-- 时间日期筛选 -->
|
||||
<el-date-picker
|
||||
class="datetime-picker w100"
|
||||
v-if="item.render == 'datetime' || item.comSearchRender == 'date' || item.comSearchRender == 'datetime'"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:type="item.comSearchRender == 'date' ? 'date' : 'datetime'"
|
||||
:value-format="item.comSearchRender == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder)"
|
||||
:teleported="false"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
/>
|
||||
|
||||
<!-- 时间筛选 -->
|
||||
<el-time-picker
|
||||
class="time-picker w100"
|
||||
v-if="item.comSearchRender == 'time'"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder)"
|
||||
value-format="HH:mm:ss"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
/>
|
||||
|
||||
<!-- tag、tags、select -->
|
||||
<el-select
|
||||
class="w100"
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder)"
|
||||
v-else-if="
|
||||
(item.render == 'tag' || item.render == 'tags' || item.comSearchRender == 'select') &&
|
||||
item.replaceValue
|
||||
"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:multiple="item.operator == 'IN' || item.operator == 'NOT IN'"
|
||||
:clearable="true"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
>
|
||||
<el-option v-for="(opt, okey) in item.replaceValue" :key="item.prop! + okey" :label="opt" :value="okey" />
|
||||
</el-select>
|
||||
|
||||
<!-- 远程 select -->
|
||||
<BaInput
|
||||
v-else-if="item.comSearchRender == 'remoteSelect'"
|
||||
type="remoteSelect"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:attr="{ ...item.remote, ...item.comSearchInputAttr }"
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder)"
|
||||
/>
|
||||
|
||||
<!-- 开关 -->
|
||||
<el-select
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder)"
|
||||
v-else-if="item.render == 'switch'"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:clearable="true"
|
||||
class="w100"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
>
|
||||
<template v-if="!isEmpty(item.replaceValue)">
|
||||
<el-option
|
||||
v-for="(opt, okey) in item.replaceValue"
|
||||
:key="item.prop! + okey"
|
||||
:label="opt"
|
||||
:value="okey"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-option :label="$t('utils.open')" value="1" />
|
||||
<el-option :label="$t('utils.close')" value="0" />
|
||||
</template>
|
||||
</el-select>
|
||||
|
||||
<!-- 字符串 -->
|
||||
<el-input
|
||||
:placeholder="getPlaceholder(item.operatorPlaceholder)"
|
||||
v-else
|
||||
type="string"
|
||||
v-model="baTable.comSearch.form[item.prop!]"
|
||||
:clearable="true"
|
||||
v-bind="item.comSearchInputAttr"
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</template>
|
||||
</template>
|
||||
<el-col :xs="24" :sm="6">
|
||||
<div class="com-search-col pl-20">
|
||||
<el-button v-blur @click="baTable.onTableAction('com-search', { event: 'submit-com-search-form' })" type="primary">
|
||||
{{ $t('Search') }}
|
||||
</el-button>
|
||||
<el-button @click="onResetForm()">{{ $t('Reset') }}</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
import { isArray, isEmpty, isUndefined } from 'lodash-es'
|
||||
import BaInput from '/@/components/baInput/index.vue'
|
||||
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
|
||||
const onResetForm = () => {
|
||||
/**
|
||||
* 封装好的 /utils/common.js/onResetForm 工具在此处不能使用,因为未使用 el-form-item
|
||||
* 改用公共搜索重新初始化函数
|
||||
*/
|
||||
baTable.initComSearch()
|
||||
|
||||
// 通知 baTable 发起公共搜索
|
||||
baTable.onTableAction('com-search', { event: 'reset-com-search-form' })
|
||||
}
|
||||
|
||||
const getPlaceholder = (placeholder: string | string[] | undefined, key = 0, defaultValue = '') => {
|
||||
if (isUndefined(placeholder)) {
|
||||
return defaultValue
|
||||
} else if (isArray(placeholder)) {
|
||||
return placeholder[key]
|
||||
} else {
|
||||
return placeholder
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-com-search {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
border: 1px solid var(--ba-border-color);
|
||||
border-bottom: none;
|
||||
padding: 13px 15px;
|
||||
font-size: 14px;
|
||||
.com-search-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
}
|
||||
.com-search-col-label {
|
||||
width: 33.33%;
|
||||
padding: 0 15px;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.com-search-col-input {
|
||||
padding: 0 15px;
|
||||
width: 66.66%;
|
||||
}
|
||||
.com-search-col-input-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
width: 66.66%;
|
||||
.range-separator {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.pl-20 {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.w16 {
|
||||
width: 16.5% !important;
|
||||
}
|
||||
.w83 {
|
||||
width: 83.5% !important;
|
||||
}
|
||||
</style>
|
||||
160
web/src/components/table/fieldRender/buttons.vue
Normal file
160
web/src/components/table/fieldRender/buttons.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div v-memo="[field]">
|
||||
<template v-for="(btn, idx) in field.buttons" :key="idx">
|
||||
<template v-if="btn.display ? btn.display(row, field) : true">
|
||||
<!-- 常规按钮 -->
|
||||
<el-button
|
||||
v-if="btn.render == 'basicButton'"
|
||||
v-blur
|
||||
@click="onButtonClick(btn)"
|
||||
:class="btn.class"
|
||||
size="small"
|
||||
class="ba-table-render-buttons-item buttons-ml-6"
|
||||
:type="btn.type"
|
||||
:loading="btn.loading && btn.loading(row, field)"
|
||||
:disabled="btn.disabled && btn.disabled(row, field)"
|
||||
v-bind="invokeTableContextDataFun(btn.attr, { row, field, cellValue: btn, column, index })"
|
||||
>
|
||||
<Icon v-if="btn.icon" size="14" color="var(--ba-bg-color-overlay)" :name="btn.icon" />
|
||||
<div v-if="btn.text" class="text">{{ getTranslation(btn.text) }}</div>
|
||||
</el-button>
|
||||
|
||||
<!-- 带提示信息的按钮 -->
|
||||
<el-tooltip
|
||||
v-if="btn.render == 'tipButton' && ((btn.name == 'edit' && baTable.auth('edit')) || btn.name != 'edit')"
|
||||
:disabled="btn.title && !btn.disabledTip ? false : true"
|
||||
:content="getTranslation(btn.title)"
|
||||
placement="top"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.tooltip, { row, field, cellValue: btn, column, index })"
|
||||
>
|
||||
<el-button
|
||||
v-blur
|
||||
@click="onButtonClick(btn)"
|
||||
:class="btn.class"
|
||||
size="small"
|
||||
class="ba-table-render-buttons-item buttons-ml-6"
|
||||
:type="btn.type"
|
||||
:loading="btn.loading && btn.loading(row, field)"
|
||||
:disabled="btn.disabled && btn.disabled(row, field)"
|
||||
v-bind="invokeTableContextDataFun(btn.attr, { row, field, cellValue: btn, column, index })"
|
||||
>
|
||||
<Icon v-if="btn.icon" size="14" color="var(--ba-bg-color-overlay)" :name="btn.icon" />
|
||||
<div v-if="btn.text" class="text">{{ getTranslation(btn.text) }}</div>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 带确认框的按钮 -->
|
||||
<el-popconfirm
|
||||
v-if="btn.render == 'confirmButton' && ((btn.name == 'delete' && baTable.auth('del')) || btn.name != 'delete')"
|
||||
:disabled="btn.disabled && btn.disabled(row, field)"
|
||||
v-bind="invokeTableContextDataFun(btn.popconfirm, { row, field, cellValue: btn, column, index })"
|
||||
@confirm="onButtonClick(btn)"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="buttons-popconfirm-reference-box buttons-ml-6">
|
||||
<el-tooltip
|
||||
:disabled="btn.title ? false : true"
|
||||
:content="getTranslation(btn.title)"
|
||||
placement="top"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.tooltip, { row, field, cellValue: btn, column, index })"
|
||||
>
|
||||
<el-button
|
||||
v-blur
|
||||
:class="btn.class"
|
||||
size="small"
|
||||
class="ba-table-render-buttons-item"
|
||||
:type="btn.type"
|
||||
:loading="btn.loading && btn.loading(row, field)"
|
||||
:disabled="btn.disabled && btn.disabled(row, field)"
|
||||
v-bind="invokeTableContextDataFun(btn.attr, { row, field, cellValue: btn, column, index })"
|
||||
>
|
||||
<Icon v-if="btn.icon" size="14" color="var(--ba-bg-color-overlay)" :name="btn.icon" />
|
||||
<div v-if="btn.text" class="text">{{ getTranslation(btn.text) }}</div>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
|
||||
<!-- 带提示的可拖拽按钮 -->
|
||||
<el-tooltip
|
||||
v-if="btn.render == 'moveButton' && ((btn.name == 'weigh-sort' && baTable.auth('sortable')) || btn.name != 'weigh-sort')"
|
||||
:disabled="btn.title && !btn.disabledTip ? false : true"
|
||||
:content="getTranslation(btn.title)"
|
||||
placement="top"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.tooltip, { row, field, cellValue: btn, column, index })"
|
||||
>
|
||||
<el-button
|
||||
:class="btn.class"
|
||||
size="small"
|
||||
class="ba-table-render-buttons-item move-button buttons-ml-6"
|
||||
:type="btn.type"
|
||||
:loading="btn.loading && btn.loading(row, field)"
|
||||
:disabled="btn.disabled && btn.disabled(row, field)"
|
||||
v-bind="invokeTableContextDataFun(btn.attr, { row, field, cellValue: btn, column, index })"
|
||||
>
|
||||
<Icon v-if="btn.icon" size="14" color="var(--ba-bg-color-overlay)" :name="btn.icon" />
|
||||
<div v-if="btn.text" class="text">{{ getTranslation(btn.text) }}</div>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { invokeTableContextDataFun } from '/@/components/table/index'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const { t, te } = useI18n()
|
||||
const props = defineProps<Props>()
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
|
||||
const onButtonClick = (btn: OptButton) => {
|
||||
if (typeof btn.click === 'function') {
|
||||
btn.click(props.row, props.field)
|
||||
return
|
||||
}
|
||||
baTable.onTableAction(btn.name as BaTableActionEventName, props)
|
||||
}
|
||||
|
||||
const getTranslation = (key?: string) => {
|
||||
if (!key) return ''
|
||||
return te(key) ? t(key) : key
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-table-render-buttons-item {
|
||||
.text {
|
||||
font-size: 14px;
|
||||
}
|
||||
.icon + .text {
|
||||
padding-left: 5px;
|
||||
}
|
||||
&.el-button--small {
|
||||
padding: 4px 5px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.ba-table-render-buttons-move {
|
||||
cursor: move;
|
||||
}
|
||||
.buttons-popconfirm-reference-box {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.buttons-ml-6 + .buttons-ml-6 {
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
28
web/src/components/table/fieldRender/color.vue
Normal file
28
web/src/components/table/fieldRender/color.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :style="{ background: cellValue }" class="ba-table-render-color"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { getCellValue } from '/@/components/table/index'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-table-render-color {
|
||||
height: 25px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
28
web/src/components/table/fieldRender/customRender.vue
Normal file
28
web/src/components/table/fieldRender/customRender.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<component
|
||||
:is="field.customRender"
|
||||
:renderRow="row"
|
||||
:renderField="field"
|
||||
:renderValue="cellValue"
|
||||
:renderColumn="column"
|
||||
:renderIndex="index"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { getCellValue } from '/@/components/table/index'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
</script>
|
||||
21
web/src/components/table/fieldRender/customTemplate.vue
Normal file
21
web/src/components/table/fieldRender/customTemplate.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-html="field.customTemplate ? field.customTemplate(row, field, cellValue, column, index) : ''"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { getCellValue } from '/@/components/table/index'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
</script>
|
||||
22
web/src/components/table/fieldRender/datetime.vue
Normal file
22
web/src/components/table/fieldRender/datetime.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ !cellValue ? '-' : timeFormat(cellValue, field.timeFormat ?? 'yyyy-mm-dd hh:MM:ss') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { getCellValue } from '/@/components/table/index'
|
||||
import { timeFormat } from '/@/utils/common'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
</script>
|
||||
5
web/src/components/table/fieldRender/default.vue
Normal file
5
web/src/components/table/fieldRender/default.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tag effect="dark" type="danger">Field renderer not found</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
25
web/src/components/table/fieldRender/icon.vue
Normal file
25
web/src/components/table/fieldRender/icon.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<Icon
|
||||
color="var(--el-text-color-primary)"
|
||||
:name="cellValue"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.icon, { row, field, cellValue, column, index })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { getCellValue, invokeTableContextDataFun } from '/@/components/table/index'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
</script>
|
||||
37
web/src/components/table/fieldRender/image.vue
Normal file
37
web/src/components/table/fieldRender/image.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-image
|
||||
v-if="cellValue"
|
||||
:hide-on-click-modal="true"
|
||||
:preview-teleported="true"
|
||||
:preview-src-list="[fullUrl(cellValue)]"
|
||||
:src="fullUrl(cellValue)"
|
||||
class="ba-table-render-image"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.image, { row, field, cellValue, column, index })"
|
||||
></el-image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { getCellValue, invokeTableContextDataFun } from '/@/components/table/index'
|
||||
import { fullUrl } from '/@/utils/common'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-table-render-image {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
</style>
|
||||
43
web/src/components/table/fieldRender/images.vue
Normal file
43
web/src/components/table/fieldRender/images.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="isArray(cellValue) && cellValue.length">
|
||||
<el-image
|
||||
v-for="(item, idx) in cellValue"
|
||||
:key="idx"
|
||||
:initial-index="idx"
|
||||
:preview-teleported="true"
|
||||
:preview-src-list="arrayFullUrl(cellValue)"
|
||||
class="ba-table-render-images-item"
|
||||
:src="fullUrl(item)"
|
||||
:hide-on-click-modal="true"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.image, { row, field, cellValue, column, index })"
|
||||
></el-image>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { isArray } from 'lodash-es'
|
||||
import { getCellValue, invokeTableContextDataFun } from '/@/components/table/index'
|
||||
import { arrayFullUrl, fullUrl } from '/@/utils/common'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-table-render-images-item {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
||||
52
web/src/components/table/fieldRender/switch.vue
Normal file
52
web/src/components/table/fieldRender/switch.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-switch
|
||||
v-if="field.prop"
|
||||
@change="onChange"
|
||||
:model-value="cellValue"
|
||||
:loading="loading"
|
||||
active-value="1"
|
||||
inactive-value="0"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.switch, { row, field, cellValue, column, index })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { inject, ref } from 'vue'
|
||||
import { getCellValue, invokeTableContextDataFun } from '/@/components/table/index'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const props = defineProps<Props>()
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
const cellValue = ref(getCellValue(props.row, props.field, props.column, props.index))
|
||||
|
||||
if (typeof cellValue.value === 'number') {
|
||||
cellValue.value = cellValue.value.toString()
|
||||
}
|
||||
|
||||
const onChange = (value: string | number | boolean) => {
|
||||
loading.value = true
|
||||
baTable.api
|
||||
.postData('edit', {
|
||||
[baTable.table.pk!]: props.row[baTable.table.pk!],
|
||||
[props.field.prop!]: value,
|
||||
})
|
||||
.then(() => {
|
||||
cellValue.value = value
|
||||
baTable.onTableAction('field-change', { value: value, ...props })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
34
web/src/components/table/fieldRender/tag.vue
Normal file
34
web/src/components/table/fieldRender/tag.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tag
|
||||
v-if="![null, undefined, ''].includes(cellValue)"
|
||||
:type="getTagType(cellValue, field.custom)"
|
||||
:effect="field.effect ?? 'light'"
|
||||
:size="field.size ?? 'default'"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.tag, { row, field, cellValue, column, index })"
|
||||
>
|
||||
{{ !isEmpty(field.replaceValue) ? (field.replaceValue[cellValue] ?? cellValue) : cellValue }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx, TagProps } from 'element-plus'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { getCellValue, invokeTableContextDataFun } from '/@/components/table/index'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
|
||||
const getTagType = (value: string, custom: any): TagProps['type'] => {
|
||||
return !isEmpty(custom) && custom[value] ? custom[value] : 'primary'
|
||||
}
|
||||
</script>
|
||||
56
web/src/components/table/fieldRender/tags.vue
Normal file
56
web/src/components/table/fieldRender/tags.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="isArray(cellValue)">
|
||||
<template v-for="(tag, idx) in cellValue" :key="idx">
|
||||
<el-tag
|
||||
v-if="![null, undefined, ''].includes(tag)"
|
||||
class="m-4"
|
||||
:type="getTagType(tag, field.custom)"
|
||||
:effect="field.effect ?? 'light'"
|
||||
:size="field.size ?? 'default'"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.tag, { row, field, cellValue, column, index })"
|
||||
>
|
||||
{{ !isEmpty(field.replaceValue) ? (field.replaceValue[tag] ?? tag) : tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag
|
||||
v-if="![null, undefined, ''].includes(cellValue)"
|
||||
:type="getTagType(cellValue, field.custom)"
|
||||
:effect="field.effect ?? 'light'"
|
||||
:size="field.size ?? 'default'"
|
||||
v-bind="invokeTableContextDataFun(field.customRenderAttr?.tag, { row, field, cellValue, column, index })"
|
||||
>
|
||||
{{ !isEmpty(field.replaceValue) ? (field.replaceValue[cellValue] ?? cellValue) : cellValue }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx, TagProps } from 'element-plus'
|
||||
import { isArray, isEmpty } from 'lodash-es'
|
||||
import { getCellValue, invokeTableContextDataFun } from '/@/components/table/index'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
|
||||
const getTagType = (value: string, custom: any): TagProps['type'] => {
|
||||
return !isEmpty(custom) && custom[value] ? custom[value] : 'primary'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.m-4 {
|
||||
margin: 4px;
|
||||
}
|
||||
</style>
|
||||
39
web/src/components/table/fieldRender/url.vue
Normal file
39
web/src/components/table/fieldRender/url.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-input :model-value="cellValue" :placeholder="$t('Link address')">
|
||||
<template #append>
|
||||
<el-button @click="openUrl(cellValue, field)">
|
||||
<Icon color="#606266" name="el-icon-Position" />
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { getCellValue } from '/@/components/table/index'
|
||||
|
||||
interface Props {
|
||||
row: TableRow
|
||||
field: TableColumn
|
||||
column: TableColumnCtx<TableRow>
|
||||
index: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
if (props.field.click) {
|
||||
console.warn('baTable.table.column.click 即将废弃,请使用 el-table 的 @cell-click 或单元格自定义渲染代替')
|
||||
}
|
||||
|
||||
const cellValue = getCellValue(props.row, props.field, props.column, props.index)
|
||||
|
||||
const openUrl = (url: string, field: TableColumn) => {
|
||||
if (field.target == '_blank') {
|
||||
window.open(url)
|
||||
} else {
|
||||
window.location.href = url
|
||||
}
|
||||
}
|
||||
</script>
|
||||
243
web/src/components/table/header/index.vue
Normal file
243
web/src/components/table/header/index.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<!-- 公共搜索 -->
|
||||
<el-collapse-transition>
|
||||
<ComSearch v-if="props.buttons.includes('comSearch') && baTable.table.showComSearch">
|
||||
<template v-for="(slot, idx) in $slots" :key="idx" #[idx]>
|
||||
<slot :name="idx"></slot>
|
||||
</template>
|
||||
</ComSearch>
|
||||
</el-collapse-transition>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div v-bind="$attrs" class="table-header ba-scroll-style">
|
||||
<slot name="refreshPrepend"></slot>
|
||||
<el-tooltip v-if="props.buttons.includes('refresh')" :content="t('Refresh')" placement="top">
|
||||
<el-button v-blur @click="onAction('refresh', { loading: true })" color="#40485b" class="table-header-operate btns-ml-12" type="info">
|
||||
<Icon name="fa fa-refresh" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<slot name="refreshAppend"></slot>
|
||||
<el-tooltip v-if="props.buttons.includes('add') && baTable.auth('add')" :content="t('Add')" placement="top">
|
||||
<el-button v-blur @click="onAction('add')" class="table-header-operate btns-ml-12" type="primary">
|
||||
<Icon name="fa fa-plus" />
|
||||
<span class="table-header-operate-text">{{ t('Add') }}</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="props.buttons.includes('edit') && baTable.auth('edit')" :content="t('Edit selected row')" placement="top">
|
||||
<el-button v-blur @click="onAction('edit')" :disabled="!enableBatchOpt" class="table-header-operate btns-ml-12" type="primary">
|
||||
<Icon name="fa fa-pencil" />
|
||||
<span class="table-header-operate-text">{{ t('Edit') }}</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-popconfirm
|
||||
v-if="props.buttons.includes('delete') && baTable.auth('del')"
|
||||
@confirm="onAction('delete')"
|
||||
:confirm-button-text="t('Delete')"
|
||||
:cancel-button-text="t('Cancel')"
|
||||
confirmButtonType="danger"
|
||||
:title="t('Are you sure to delete the selected record?')"
|
||||
:disabled="!enableBatchOpt"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="btns-ml-12">
|
||||
<el-tooltip :content="t('Delete selected row')" placement="top">
|
||||
<el-button v-blur :disabled="!enableBatchOpt" class="table-header-operate" type="danger">
|
||||
<Icon name="fa fa-trash" />
|
||||
<span class="table-header-operate-text">{{ t('Delete') }}</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-tooltip
|
||||
v-if="props.buttons.includes('unfold')"
|
||||
:content="(baTable.table.expandAll ? t('Shrink') : t('Open')) + t('All submenus')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
v-blur
|
||||
@click="baTable.onTableHeaderAction('unfold', { unfold: !baTable.table.expandAll })"
|
||||
class="table-header-operate btns-ml-12"
|
||||
:type="baTable.table.expandAll ? 'danger' : 'warning'"
|
||||
>
|
||||
<span class="table-header-operate-text">{{ baTable.table.expandAll ? t('Shrink all') : t('Expand all') }}</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- slot -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- 右侧搜索框和工具按钮 -->
|
||||
<div class="table-search">
|
||||
<slot name="quickSearchPrepend"></slot>
|
||||
<el-input
|
||||
v-if="props.buttons.includes('quickSearch')"
|
||||
v-model="baTable.table.filter!.quickSearch"
|
||||
class="xs-hidden quick-search"
|
||||
@input="onSearchInput"
|
||||
:placeholder="quickSearchPlaceholder ? quickSearchPlaceholder : t('Search')"
|
||||
clearable
|
||||
/>
|
||||
<div class="table-search-button-group" v-if="props.buttons.includes('columnDisplay') || props.buttons.includes('comSearch')">
|
||||
<el-dropdown v-if="props.buttons.includes('columnDisplay')" :max-height="380" :hide-on-click="false">
|
||||
<el-button
|
||||
class="table-search-button-item"
|
||||
:class="props.buttons.includes('comSearch') ? 'right-border' : ''"
|
||||
color="#dcdfe6"
|
||||
plain
|
||||
v-blur
|
||||
>
|
||||
<Icon size="14" name="el-icon-Grid" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="(item, idx) in columnDisplay" :key="idx">
|
||||
<el-checkbox
|
||||
v-if="item.prop"
|
||||
@change="onChangeShowColumn($event, item.prop!)"
|
||||
:checked="!item.show"
|
||||
:model-value="item.show"
|
||||
size="small"
|
||||
:label="item.label"
|
||||
/>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-tooltip
|
||||
v-if="props.buttons.includes('comSearch')"
|
||||
:disabled="baTable.table.showComSearch"
|
||||
:content="t('Expand generic search')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
class="table-search-button-item"
|
||||
@click="baTable.table.showComSearch = !baTable.table.showComSearch"
|
||||
color="#dcdfe6"
|
||||
plain
|
||||
v-blur
|
||||
>
|
||||
<Icon size="14" name="el-icon-Search" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { debounce } from 'lodash-es'
|
||||
import { computed, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ComSearch from '/@/components/table/comSearch/index.vue'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
|
||||
const { t } = useI18n()
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
|
||||
interface Props {
|
||||
buttons: HeaderOptButton[]
|
||||
quickSearchPlaceholder?: string
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
buttons: () => {
|
||||
return ['refresh', 'add', 'edit', 'delete']
|
||||
},
|
||||
quickSearchPlaceholder: '',
|
||||
})
|
||||
|
||||
const columnDisplay = computed(() => {
|
||||
let columnDisplayArr = []
|
||||
for (let item of baTable.table.column) {
|
||||
item.type === 'selection' || item.render === 'buttons' || item.enableColumnDisplayControl === false ? '' : columnDisplayArr.push(item)
|
||||
}
|
||||
return columnDisplayArr
|
||||
})
|
||||
|
||||
const enableBatchOpt = computed(() => (baTable.table.selection!.length > 0 ? true : false))
|
||||
|
||||
const onAction = (event: BaTableHeaderActionEventName, data: anyObj = {}) => {
|
||||
baTable.onTableHeaderAction(event, data)
|
||||
}
|
||||
|
||||
const onSearchInput = debounce(() => {
|
||||
baTable.onTableHeaderAction('quick-search', { keyword: baTable.table.filter!.quickSearch })
|
||||
}, 500)
|
||||
|
||||
const onChangeShowColumn = (value: string | number | boolean, field: string) => {
|
||||
baTable.onTableHeaderAction('change-show-column', { field: field, value: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.table-header {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
border: 1px solid var(--ba-border-color);
|
||||
border-bottom: none;
|
||||
padding: 13px 15px;
|
||||
font-size: 14px;
|
||||
.table-header-operate-text {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
.btns-ml-12 + .btns-ml-12 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.table-search {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
.quick-search {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
.table-search-button-group {
|
||||
display: flex;
|
||||
margin-left: 12px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
overflow: hidden;
|
||||
button:focus,
|
||||
button:active {
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
}
|
||||
button:hover {
|
||||
background-color: var(--el-color-info-light-7);
|
||||
}
|
||||
.table-search-button-item {
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.el-button + .el-button {
|
||||
margin: 0;
|
||||
}
|
||||
.right-border {
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
.table-search-button-group {
|
||||
button:focus,
|
||||
button:active {
|
||||
background-color: var(--el-color-info-dark-2);
|
||||
}
|
||||
button:hover {
|
||||
background-color: var(--el-color-info-light-7);
|
||||
}
|
||||
button {
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
el-icon {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
141
web/src/components/table/index.ts
Normal file
141
web/src/components/table/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { TableColumnCtx } from 'element-plus'
|
||||
import { isUndefined } from 'lodash-es'
|
||||
import { i18n } from '/@/lang/index'
|
||||
|
||||
/**
|
||||
* 获取单元格值
|
||||
*/
|
||||
export const getCellValue = (row: TableRow, field: TableColumn, column: TableColumnCtx<TableRow>, index: number) => {
|
||||
if (!field.prop) return ''
|
||||
|
||||
const prop = field.prop
|
||||
let cellValue: any = row[prop]
|
||||
|
||||
// 字段 prop 带 . 比如 user.nickname
|
||||
if (prop.indexOf('.') > -1) {
|
||||
const fieldNameArr = prop.split('.')
|
||||
cellValue = row[fieldNameArr[0]]
|
||||
for (let index = 1; index < fieldNameArr.length; index++) {
|
||||
cellValue = cellValue ? (cellValue[fieldNameArr[index]] ?? '') : ''
|
||||
}
|
||||
}
|
||||
|
||||
// 若无值,尝试取默认值
|
||||
if ([undefined, null, ''].includes(cellValue) && field.default !== undefined) {
|
||||
cellValue = field.default
|
||||
}
|
||||
|
||||
// 渲染前格式化
|
||||
if (field.renderFormatter && typeof field.renderFormatter == 'function') {
|
||||
cellValue = field.renderFormatter(row, field, cellValue, column, index)
|
||||
console.warn('baTable.table.column.renderFormatter 即将废弃,请直接使用兼容 el-table 的 baTable.table.column.formatter 代替')
|
||||
}
|
||||
if (field.formatter && typeof field.formatter == 'function') {
|
||||
cellValue = field.formatter(row, column, cellValue, index)
|
||||
}
|
||||
|
||||
return cellValue
|
||||
}
|
||||
|
||||
/*
|
||||
* 默认按钮组
|
||||
*/
|
||||
export const defaultOptButtons = (optButType: DefaultOptButType[] = ['weigh-sort', 'edit', 'delete']): OptButton[] => {
|
||||
const optButtonsPre: Map<string, OptButton> = new Map([
|
||||
[
|
||||
'weigh-sort',
|
||||
{
|
||||
render: 'moveButton',
|
||||
name: 'weigh-sort',
|
||||
title: 'Drag sort',
|
||||
text: '',
|
||||
type: 'info',
|
||||
icon: 'fa fa-arrows',
|
||||
class: 'table-row-weigh-sort',
|
||||
disabledTip: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'edit',
|
||||
{
|
||||
render: 'tipButton',
|
||||
name: 'edit',
|
||||
title: 'Edit',
|
||||
text: '',
|
||||
type: 'primary',
|
||||
icon: 'fa fa-pencil',
|
||||
class: 'table-row-edit',
|
||||
disabledTip: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'delete',
|
||||
{
|
||||
render: 'confirmButton',
|
||||
name: 'delete',
|
||||
title: 'Delete',
|
||||
text: '',
|
||||
type: 'danger',
|
||||
icon: 'fa fa-trash',
|
||||
class: 'table-row-delete',
|
||||
popconfirm: {
|
||||
confirmButtonText: i18n.global.t('Delete'),
|
||||
cancelButtonText: i18n.global.t('Cancel'),
|
||||
confirmButtonType: 'danger',
|
||||
title: i18n.global.t('Are you sure to delete the selected record?'),
|
||||
},
|
||||
disabledTip: false,
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
const optButtons: OptButton[] = []
|
||||
for (const key in optButType) {
|
||||
if (optButtonsPre.has(optButType[key])) {
|
||||
optButtons.push(optButtonsPre.get(optButType[key])!)
|
||||
}
|
||||
}
|
||||
return optButtons
|
||||
}
|
||||
|
||||
/**
|
||||
* 将带children的数组降维,然后寻找index所在的行
|
||||
*/
|
||||
export const findIndexRow = (data: TableRow[], findIdx: number, keyIndex: number | TableRow = -1): number | TableRow => {
|
||||
for (const key in data) {
|
||||
if (typeof keyIndex == 'number') {
|
||||
keyIndex++
|
||||
}
|
||||
|
||||
if (keyIndex == findIdx) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
if (data[key].children) {
|
||||
keyIndex = findIndexRow(data[key].children!, findIdx, keyIndex)
|
||||
if (typeof keyIndex != 'number') {
|
||||
return keyIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keyIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用一个接受表格上下文数据的任意属性计算函数
|
||||
*/
|
||||
export const invokeTableContextDataFun = <T>(
|
||||
fun: TableContextDataFun<T> | undefined,
|
||||
context: TableContextData,
|
||||
defaultValue: any = {}
|
||||
): Partial<T> => {
|
||||
if (isUndefined(fun)) {
|
||||
return defaultValue
|
||||
} else if (typeof fun === 'function') {
|
||||
return fun(context)
|
||||
}
|
||||
return fun
|
||||
}
|
||||
|
||||
type DefaultOptButType = 'weigh-sort' | 'edit' | 'delete'
|
||||
246
web/src/components/table/index.vue
Normal file
246
web/src/components/table/index.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot name="neck"></slot>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="ba-data-table w100"
|
||||
header-cell-class-name="table-header-cell"
|
||||
:default-expand-all="baTable.table.expandAll"
|
||||
:data="baTable.table.data"
|
||||
:row-key="baTable.table.pk"
|
||||
:border="true"
|
||||
v-loading="baTable.table.loading"
|
||||
stripe
|
||||
@select-all="onSelectAll"
|
||||
@select="onSelect"
|
||||
@selection-change="onSelectionChange"
|
||||
@sort-change="onSortChange"
|
||||
@row-dblclick="baTable.onTableDblclick"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot name="columnPrepend"></slot>
|
||||
<template v-for="(item, key) in baTable.table.column">
|
||||
<template v-if="item.show !== false">
|
||||
<!-- 渲染为 slot -->
|
||||
<slot v-if="item.render == 'slot'" :name="item.slotName"></slot>
|
||||
|
||||
<el-table-column
|
||||
v-else
|
||||
:key="key + '-column'"
|
||||
v-bind="item"
|
||||
:column-key="(item['columnKey'] ? item['columnKey'] : `table-column-${item.prop}`) || shortUuid()"
|
||||
>
|
||||
<!-- ./fieldRender/ 文件夹内的每个组件为一种字段渲染器,组件名称为渲染器名称 -->
|
||||
<template v-if="item.render" #default="scope">
|
||||
<component
|
||||
:row="scope.row"
|
||||
:field="item"
|
||||
:column="scope.column"
|
||||
:index="scope.$index"
|
||||
:is="fieldRenderer[item.render] ?? fieldRenderer['default']"
|
||||
:key="getRenderKey(key, item, scope)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</template>
|
||||
<slot name="columnAppend"></slot>
|
||||
</el-table>
|
||||
<div v-if="props.pagination" class="table-pagination">
|
||||
<el-pagination
|
||||
:currentPage="baTable.table.filter!.page"
|
||||
:page-size="baTable.table.filter!.limit"
|
||||
:page-sizes="pageSizes"
|
||||
background
|
||||
:layout="config.layout.shrink ? 'prev, next, jumper' : 'sizes,total, ->, prev, pager, next, jumper'"
|
||||
:total="baTable.table.total"
|
||||
@size-change="onTableSizeChange"
|
||||
@current-change="onTableCurrentChange"
|
||||
></el-pagination>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ElTable } from 'element-plus'
|
||||
import type { Component } from 'vue'
|
||||
import { computed, inject, nextTick, useTemplateRef } from 'vue'
|
||||
import { useConfig } from '/@/stores/config'
|
||||
import type baTableClass from '/@/utils/baTable'
|
||||
import { shortUuid } from '/@/utils/random'
|
||||
|
||||
const config = useConfig()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const baTable = inject('baTable') as baTableClass
|
||||
type ElTableProps = Partial<InstanceType<typeof ElTable>['$props']>
|
||||
|
||||
interface Props extends /* @vue-ignore */ ElTableProps {
|
||||
pagination?: boolean
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
pagination: true,
|
||||
})
|
||||
|
||||
const fieldRenderer: Record<string, Component> = {}
|
||||
const fieldRendererComponents: Record<string, any> = import.meta.glob('./fieldRender/**.vue', { eager: true })
|
||||
for (const key in fieldRendererComponents) {
|
||||
const fileName = key.replace('./fieldRender/', '').replace('.vue', '')
|
||||
fieldRenderer[fileName] = fieldRendererComponents[key].default
|
||||
}
|
||||
|
||||
const getRenderKey = (key: number, item: TableColumn, scope: any) => {
|
||||
if (item.getRenderKey && typeof item.getRenderKey == 'function') {
|
||||
return item.getRenderKey(scope.row, item, scope.column, scope.$index)
|
||||
}
|
||||
if (item.render == 'switch') {
|
||||
return item.render + item.prop
|
||||
}
|
||||
return key + scope.$index + '-' + item.render + '-' + (item.prop ? '-' + item.prop + '-' + scope.row[item.prop] : '')
|
||||
}
|
||||
|
||||
const onTableSizeChange = (val: number) => {
|
||||
baTable.onTableAction('page-size-change', { size: val })
|
||||
}
|
||||
|
||||
const onTableCurrentChange = (val: number) => {
|
||||
baTable.onTableAction('current-page-change', { page: val })
|
||||
}
|
||||
|
||||
const onSortChange = ({ order, prop }: { order: string; prop: string }) => {
|
||||
baTable.onTableAction('sort-change', { prop: prop, order: order ? (order == 'ascending' ? 'asc' : 'desc') : '' })
|
||||
}
|
||||
|
||||
const pageSizes = computed(() => {
|
||||
let defaultSizes = [10, 20, 50, 100]
|
||||
if (baTable.table.filter!.limit) {
|
||||
if (!defaultSizes.includes(baTable.table.filter!.limit)) {
|
||||
defaultSizes.push(baTable.table.filter!.limit)
|
||||
}
|
||||
}
|
||||
return defaultSizes
|
||||
})
|
||||
|
||||
/*
|
||||
* 全选和取消全选
|
||||
* 实现子级同时选择和取消选中
|
||||
*/
|
||||
const onSelectAll = (selection: TableRow[]) => {
|
||||
if (isSelectAll(selection.map((row: TableRow) => row[baTable.table.pk!].toString()))) {
|
||||
selection.map((row: TableRow) => {
|
||||
if (row.children) {
|
||||
selectChildren(row.children, true)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
tableRef.value?.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 是否是全选操作
|
||||
* 只检查第一个元素是否被选择
|
||||
* 全选时:selectIds为所有元素的id
|
||||
* 取消全选时:selectIds为所有子元素的id
|
||||
*/
|
||||
const isSelectAll = (selectIds: string[]) => {
|
||||
let data = baTable.table.data as TableRow[]
|
||||
for (const key in data) {
|
||||
return selectIds.includes(data[key][baTable.table.pk!].toString())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
* 选择子项-递归
|
||||
*/
|
||||
const selectChildren = (children: TableRow[], type: boolean) => {
|
||||
children.map((j: TableRow) => {
|
||||
toggleSelection(j, type)
|
||||
if (j.children) {
|
||||
selectChildren(j.children, type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* 执行选择操作
|
||||
*/
|
||||
const toggleSelection = (row: TableRow, type: boolean) => {
|
||||
if (row) {
|
||||
nextTick(() => {
|
||||
tableRef.value?.toggleRowSelection(row, type)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 手动选择时,同时选择子级
|
||||
*/
|
||||
const onSelect = (selection: TableRow[], row: TableRow) => {
|
||||
if (
|
||||
selection.some((item: TableRow) => {
|
||||
return row[baTable.table.pk!] === item[baTable.table.pk!]
|
||||
})
|
||||
) {
|
||||
if (row.children) {
|
||||
selectChildren(row.children, true)
|
||||
}
|
||||
} else {
|
||||
if (row.children) {
|
||||
selectChildren(row.children, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 记录选择的项
|
||||
*/
|
||||
const onSelectionChange = (selection: TableRow[]) => {
|
||||
baTable.onTableAction('selection-change', selection)
|
||||
}
|
||||
|
||||
/*
|
||||
* 设置折叠所有-递归
|
||||
*/
|
||||
const setUnFoldAll = (children: TableRow[], unfold: boolean) => {
|
||||
for (const key in children) {
|
||||
tableRef.value?.toggleRowExpansion(children[key], unfold)
|
||||
if (children[key].children) {
|
||||
setUnFoldAll(children[key].children!, unfold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 折叠所有
|
||||
*/
|
||||
const unFoldAll = (unfold: boolean) => {
|
||||
setUnFoldAll(baTable.table.data!, unfold)
|
||||
}
|
||||
|
||||
const getRef = () => {
|
||||
return tableRef.value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
unFoldAll,
|
||||
getRef,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ba-data-table :deep(.table-header-cell) .cell {
|
||||
color: var(--el-text-color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table-pagination {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-color: var(--ba-bg-color-overlay);
|
||||
padding: 13px 15px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user