项目初始化

This commit is contained in:
2026-03-18 17:19:03 +08:00
commit ac6079b9ff
602 changed files with 58291 additions and 0 deletions

View 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">
<!-- 自定义渲染 componentslot -->
<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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
<template>
<div>
<el-tag effect="dark" type="danger">Field renderer not found</el-tag>
</div>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'

View 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>