This commit is contained in:
2025-09-08 16:38:08 +08:00
parent 67b7de565f
commit ced903a6ba

View File

@@ -0,0 +1,787 @@
<script lang="ts" setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { datasourceApi } from '@/api/datasource'
import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg'
import icon_form_outlined from '@/assets/svg/icon_form_outlined.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import edit from '@/assets/svg/icon_edit_outlined.svg'
import { useI18n } from 'vue-i18n'
import ParamsForm from './ParamsForm.vue'
interface Table {
name: string
host: string
port: string
username: string
password: string
database: string
extraJdbc: string
dbSchema: string
filename: string
sheets: string
mode: string
timeout: string
configuration: string
id: number
}
const props = withDefaults(
defineProps<{
info: Table
}>(),
{
info: () => ({
name: '-',
host: '-',
port: '-',
username: '-',
password: '-',
database: '-',
extraJdbc: '-',
dbSchema: '-',
filename: '-',
sheets: '-',
mode: '-',
timeout: '-',
configuration: '-',
id: 0,
}),
}
)
const { t } = useI18n()
const paramsFormRef = ref()
const tableList = ref([] as any[])
const loading = ref(false)
const initLoading = ref(false)
const keywords = ref('')
const tableListWithSearch = computed(() => {
if (!keywords.value) return tableList.value
return tableList.value.filter((ele) =>
ele.table_name.toLowerCase().includes(keywords.value.toLowerCase())
)
})
const total = ref(1000)
const showNum = ref(100)
const currentTable = ref<any>({})
const ds = ref<any>({})
const btnSelect = ref('d')
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const handleSizeChange = (val: number) => {
pageInfo.currentPage = 1
pageInfo.pageSize = val
}
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
}
const fieldListComputed = computed(() => {
const { currentPage, pageSize } = pageInfo
return fieldList.value.slice((currentPage - 1) * pageSize, currentPage * pageSize)
})
const init = () => {
initLoading.value = true
datasourceApi.getDs(props.info.id).then((res) => {
ds.value = res
fieldList.value = []
pageInfo.total = 0
pageInfo.currentPage = 1
datasourceApi.tableList(props.info.id).then((res) => {
tableList.value = res
initLoading.value = false
})
})
}
onMounted(() => {
init()
})
const tableComment = ref('')
const fieldDialog = ref<boolean>(false)
const tableDialog = ref<boolean>(false)
const fieldComment = ref('')
const currentField = ref<any>({})
const previewData = ref<any>({})
const fieldList = ref<any>([])
const buildData = () => {
return { table: currentTable.value, fields: fieldList.value }
}
const handleSelectTableList = () => {
paramsFormRef.value.open(props.info)
}
const clickTable = (table: any) => {
loading.value = true
currentTable.value = table
fieldList.value = []
pageInfo.total = 0
previewData.value = []
datasourceApi
.fieldList(table.id)
.then((res) => {
fieldList.value = res
pageInfo.total = res.length
pageInfo.currentPage = 1
datasourceApi.previewData(props.info.id, buildData()).then((res) => {
previewData.value = res
})
})
.finally(() => {
loading.value = false
})
}
const closeTable = () => {
tableDialog.value = false
}
const editTable = () => {
tableComment.value = currentTable.value.custom_comment
tableDialog.value = true
}
const saveTable = () => {
currentTable.value.custom_comment = tableComment.value
datasourceApi.saveTable(currentTable.value).then(() => {
closeTable()
ElMessage({
message: t('common.save_success'),
type: 'success',
showClose: true,
})
})
}
const closeField = () => {
fieldDialog.value = false
}
const refresh = () => {
emits('refresh')
datasourceApi.tableList(props.info.id).then((res) => {
tableList.value = res
if (!currentTable.value.table_name) return
const nameArr = tableList.value.map((ele: any) => ele.table_name)
if (!nameArr.includes(currentTable.value.table_name)) {
currentTable.value = {}
}
})
}
const saveField = () => {
currentField.value.custom_comment = fieldComment.value
datasourceApi.saveField(currentField.value).then(() => {
closeField()
ElMessage({
message: t('common.save_success'),
type: 'success',
showClose: true,
})
})
}
const editField = (row: any) => {
currentField.value = row
fieldComment.value = currentField.value.custom_comment
fieldDialog.value = true
}
const changeStatus = (row: any) => {
currentField.value = row
datasourceApi.saveField(currentField.value).then(() => {
closeField()
ElMessage({
message: t('common.save_success'),
type: 'success',
showClose: true,
})
})
}
const emits = defineEmits(['back', 'refresh'])
const back = () => {
emits('back')
}
const renderHeader = ({ column }: any) => {
//创建一个元素用于存放表头信息
const span = document.createElement('span')
// 将表头信息渲染到元素上
span.innerText = column.label
// 在界面中添加该元素
document.body.appendChild(span)
//获取该元素的宽度包含内外边距等信息
const spanWidth = span.getBoundingClientRect().width + 20 //渲染后的 div 内左右 padding 都是 10所以 +20
//判断是否小于element的最小宽度两者取最大值
column.minWidth = column.minWidth > spanWidth ? column.minWidth : spanWidth
// 计算完成后删除该元素
document.body.removeChild(span)
return column.label
}
const btnSelectClick = (val: any) => {
btnSelect.value = val
loading.value = true
if (val === 'd') {
datasourceApi
.fieldList(currentTable.value.id)
.then((res) => {
fieldList.value = res
pageInfo.total = res.length
pageInfo.currentPage = 1
})
.finally(() => {
loading.value = false
})
} else {
datasourceApi
.previewData(props.info.id, buildData())
.then((res) => {
previewData.value = res
})
.finally(() => {
loading.value = false
})
}
}
</script>
<template>
<div class="data-table no-padding">
<div class="info">
<el-button text @click="back">{{ $t('ds.title') }}</el-button>
<el-icon size="12">
<icon_right_outlined></icon_right_outlined>
</el-icon>
<div class="name">{{ info.name }}</div>
</div>
<div class="content">
<div class="side-list">
<div class="select-table_top">
{{ $t('ds.tables') }}
<el-tooltip effect="dark" :content="$t('ds.form.choose_tables')" placement="top">
<el-icon size="18" @click="handleSelectTableList">
<icon_form_outlined></icon_form_outlined>
</el-icon>
</el-tooltip>
</div>
<el-input
v-model="keywords"
clearable
style="width: 232px"
:placeholder="$t('datasource.search')"
>
<template #prefix>
<el-icon>
<icon_searchOutline_outlined class="svg-icon" />
</el-icon>
</template>
</el-input>
<div v-loading="initLoading" class="list-content">
<el-scrollbar v-if="tableListWithSearch.length">
<div
v-for="ele in tableListWithSearch"
:key="ele.table_name"
class="model"
:class="currentTable.table_name === ele.table_name && 'isActive'"
:title="ele.table_name"
@click="clickTable(ele)"
>
<el-icon size="16">
<icon_form_outlined></icon_form_outlined>
</el-icon>
<span class="name">{{ ele.table_name }}</span>
</div>
</el-scrollbar>
<EmptyBackground
v-if="!!keywords && !tableListWithSearch.length"
:description="$t('datasource.relevant_content_found')"
img-type="tree"
style="width: 100%"
/>
<div v-else-if="!initLoading && !tableListWithSearch.length" class="no-data">
<div class="no-data-msg">
<div>
{{ $t('datasource.no_table') }}
</div>
<el-button type="primary" link @click="handleSelectTableList">
{{ $t('datasource.go_add') }}
</el-button>
</div>
</div>
</div>
</div>
<div v-if="currentTable.table_name" v-loading="loading" class="info-table">
<div class="table-name">
<div class="name">{{ currentTable.table_name }}</div>
<div class="notes">
{{ $t('about.remark') }}:
<span :title="currentTable.custom_comment" class="field-notes">{{
currentTable.custom_comment || '-'
}}</span>
<el-tooltip :offset="14" effect="dark" :content="$t('datasource.edit')" placement="top">
<el-icon style="margin-left: 8px; cursor: pointer" size="16" @click="editTable">
<edit></edit>
</el-icon>
</el-tooltip>
</div>
</div>
<div class="table-content">
<div class="btn-select">
<el-button
:class="[btnSelect === 'd' && 'is-active']"
text
@click="btnSelectClick('d')"
>
{{ t('ds.table_schema') }}
</el-button>
<el-button
:class="[btnSelect === 'q' && 'is-active']"
text
@click="btnSelectClick('q')"
>
{{ t('ds.preview') }}
</el-button>
</div>
<div
v-if="!loading"
class="preview-or-schema"
:class="btnSelect === 'q' && 'overflow-preview'"
>
<div v-if="btnSelect === 'd'" class="table-content_preview">
<el-table
row-class-name="hover-icon_edit"
:data="fieldListComputed"
style="width: 100%"
>
<el-table-column
prop="field_name"
:label="t('datasource.field_name')"
width="180"
/>
<el-table-column
prop="field_type"
:label="t('datasource.field_type')"
width="180"
/>
<el-table-column
prop="field_comment"
:label="t('datasource.field_original_notes')"
/>
<el-table-column :label="t('datasource.field_notes_1')">
<template #default="scope">
<div class="field-comment">
<span :title="scope.row.custom_comment" class="notes-in_table">{{
scope.row.custom_comment
}}</span>
<el-tooltip
:offset="14"
effect="dark"
:content="$t('datasource.edit')"
placement="top"
>
<el-icon class="action-btn" size="16" @click="editField(scope.row)">
<edit></edit>
</el-icon>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column :label="t('datasource.enabled_status')" width="180">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-switch
v-model="scope.row.checked"
size="small"
@change="changeStatus(scope.row)"
/>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div v-if="fieldList.length && btnSelect === 'd'" class="pagination-container">
<el-pagination
v-model:current-page="pageInfo.currentPage"
v-model:page-size="pageInfo.pageSize"
:page-sizes="[10, 20, 30]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageInfo.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<template v-if="btnSelect === 'q'">
<div class="preview-num">
{{ t('ds.pieces_in_total', { msg: total, ms: showNum }) }}
</div>
<el-table :data="previewData.data" style="width: 100%">
<el-table-column
v-for="(c, index) in previewData.fields"
:key="index"
:prop="c"
:label="c"
:render-header="renderHeader"
/>
</el-table>
</template>
</div>
</div>
</div>
</div>
</div>
<el-dialog
v-model="tableDialog"
:title="t('datasource.table_notes')"
width="600"
:destroy-on-close="true"
:close-on-click-modal="false"
modal-class="notes-dialog"
@closed="closeTable"
>
<el-input
v-model="tableComment"
:placeholder="$t('datasource.please_enter')"
:autosize="{ minRows: 3.64, maxRows: 11.095 }"
type="textarea"
clearable
/>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button secondary @click="closeTable">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveTable">{{ t('common.save') }}</el-button>
</div>
</el-dialog>
<el-dialog
v-model="fieldDialog"
:title="t('datasource.field_notes')"
width="600"
:destroy-on-close="true"
:close-on-click-modal="false"
modal-class="notes-dialog"
@closed="closeField"
>
<el-input
v-model="fieldComment"
:placeholder="$t('datasource.please_enter')"
:autosize="{ minRows: 3.64, maxRows: 11.095 }"
clearable
type="textarea"
/>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button secondary @click="closeField">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveField">{{ t('common.save') }}</el-button>
</div>
</el-dialog>
<ParamsForm ref="paramsFormRef" @refresh="refresh"></ParamsForm>
</template>
<style lang="less" scoped>
.data-table {
height: 100%;
.info {
height: 56px;
width: 100%;
padding-left: 20px;
display: flex;
align-items: center;
font-family: PingFang SC;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
border-bottom: 1px solid #1f232926;
.ed-button {
height: 22px;
line-height: 22px;
color: #646a73;
&:hover {
background: var(--ed-color-primary-1a, #1cba901a);
color: var(--ed-color-primary);
}
&:active {
color: var(--ed-color-primary-dark-2);
background: var(--ed-color-primary-33, #1cba9033);
}
}
.name {
color: #1f2329;
margin-left: 4px;
}
}
.content {
height: calc(100% - 56px);
position: relative;
.side-list {
width: 280px;
padding: 8px 16px;
height: 100%;
border-right: 1px solid #1f232926;
.select-table_top {
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
font-weight: 500;
.ed-icon {
cursor: pointer;
color: var(--ed-color-primary);
}
}
.ed-input {
margin: 8px;
}
.list-content {
height: calc(100% - 100px);
.no-result {
margin-top: 72px;
font-weight: 400;
font-size: 14px;
line-height: 22px;
text-align: center;
color: #646a73;
}
.model {
width: 100%;
height: 32px;
display: flex;
align-items: center;
padding-left: 8px;
border-radius: 4px;
cursor: pointer;
.name {
margin-left: 8px;
font-weight: 500;
font-size: 14px;
line-height: 22px;
max-width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
background: #1f23291a;
}
&.isActive {
background: var(--ed-color-primary-1a, #1cba901a);
color: var(--ed-color-primary);
}
}
}
.no-data {
height: 100%;
text-align: center;
display: flex;
align-items: center;
width: 100%;
.no-data-msg {
display: inline;
width: 100%;
color: var(--ed-text-color-secondary);
font-size: var(--ed-font-size-base);
}
}
}
.info-table {
position: absolute;
right: 0;
top: 0;
width: calc(100% - 280px);
height: 100%;
.table-name {
height: 80px;
padding: 16px 0 0 24px;
border-bottom: 1px solid #1f232926;
.name {
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.ed-icon {
position: relative;
cursor: pointer;
margin-top: 4px;
margin-left: 8px;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
.notes {
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
display: flex;
align-items: center;
.field-notes {
display: inline-block;
max-width: calc(100% - 75px);
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
}
.table-content {
padding: 16px 24px;
height: calc(100% - 80px);
.btn-select {
height: 32px;
padding-left: 4px;
padding-right: 4px;
display: inline-flex;
background: #ffffff;
align-items: center;
border: 1px solid #d0d3d6;
border-radius: 4px;
.is-active {
background: var(--ed-color-primary-1a, #1cba901a);
}
.ed-button:not(.is-active) {
color: #1f2329;
}
.ed-button.is-text {
height: 24px;
width: auto;
padding: 0 8px;
line-height: 24px;
}
.ed-button + .ed-button {
margin-left: 4px;
}
}
.preview-or-schema {
margin-top: 16px;
height: calc(100% - 50px);
&.overflow-preview {
overflow-y: auto;
}
.table-content_preview {
max-height: calc(100% - 50px);
overflow-y: auto;
margin-bottom: 16px;
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
.hover-icon_edit:hover {
.ed-icon {
display: block;
}
}
.field-comment {
display: flex;
align-items: center;
min-height: 24px;
.notes-in_table {
max-width: 100%;
display: -webkit-box;
max-height: 66px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; /* 限制行数为3 */
overflow: hidden;
text-overflow: ellipsis;
}
.ed-icon {
position: relative;
cursor: pointer;
margin-left: 8px;
display: none;
color: #646a73;
&::after {
content: '';
background-color: #1f23291a;
position: absolute;
border-radius: 6px;
width: 24px;
height: 24px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
display: none;
}
&:hover {
&::after {
display: block;
}
}
}
}
.preview-num {
margin: 12px 0;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
}
}
}
}
}
</style>
<style lang="less">
.notes-dialog {
.ed-textarea__inner {
line-height: 22px;
}
}
</style>