Files
SQLBot/frontend/src/views/ds/DataTable.vue
T

1082 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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_import_outlined from '@/assets/svg/icon_import_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'
import UploaderRemark from '@/views/system/excel-upload/UploaderRemark.vue'
import TableRelationship from '@/views/ds/TableRelationship.vue'
import icon_mindnote_outlined from '@/assets/svg/icon_mindnote_outlined.svg'
import { Refresh } from '@element-plus/icons-vue'
import { debounce } from 'lodash-es'
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 activeRelationship = 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 isDrag = ref(false)
const tableName = ref<any[]>([])
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
})
const handleRelationship = () => {
activeRelationship.value = !activeRelationship.value
tableName.value = []
currentTable.value = {}
}
const singleDragStartD = (e: DragEvent, ele: any) => {
isDrag.value = true
e.dataTransfer!.setData('table', JSON.stringify(ele))
}
const getTableName = (val: any) => {
tableName.value = val
}
const singleDragEnd = () => {
isDrag.value = false
}
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 fieldListTotalComputed.value.slice((currentPage - 1) * pageSize, currentPage * pageSize)
})
const fieldListTotalComputed = computed(() => {
return fieldList.value.filter((ele: any) =>
ele.field_name.toLowerCase().includes(fieldName.value.toLowerCase())
)
})
const init = (reset = false) => {
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
if (currentTable.value?.id && reset) {
tableList.value.forEach((ele) => {
if (ele.id === currentTable.value?.id) {
clickTable(ele)
}
})
}
})
.finally(() => {
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) => {
if (activeRelationship.value) return
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
fieldName.value = ''
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 changeChecked = () => {
datasourceApi.saveTable(currentTable.value).then(() => {
ElMessage({
message: t('common.save_success'),
type: 'success',
showClose: 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 syncFields = () => {
loading.value = true
datasourceApi
.syncFields(currentTable.value.id)
.then(() => {
btnSelectClick('d')
ElMessage.success(t('ds.sync_fields_success'))
loading.value = false
})
.catch(() => {
loading.value = false
ElMessage.warning(t('ds.sync_fields_failed'))
})
}
function downloadTemplate() {
datasourceApi
.exportDsSchema(props.info.id)
.then((res) => {
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = props.info.name + '.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.catch(async (error) => {
if (error.response) {
try {
let text = await error.response.data.text()
try {
text = JSON.parse(text)
} finally {
ElMessage({
message: text,
type: 'error',
showClose: true,
})
}
} catch (e) {
console.error('Error processing error response:', e)
}
} else {
console.error('Other error:', error)
ElMessage({
message: error,
type: 'error',
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 fieldNameSearch = debounce(() => {
pageInfo.currentPage = 1
pageInfo.total = fieldListTotalComputed.value.length
}, 100)
const fieldName = ref('')
const btnSelectClick = (val: any) => {
btnSelect.value = val
loading.value = true
if (val === 'd') {
datasourceApi
.fieldList(currentTable.value.id, { fieldName: '' })
.then((res) => {
fieldList.value = res
pageInfo.total = res.length
pageInfo.currentPage = 1
fieldName.value = ''
})
.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 class="export-remark">
<el-button style="margin-right: 12px" secondary @click="downloadTemplate">
<template #icon>
<icon_import_outlined></icon_import_outlined>
</template>
{{ $t('parameter.export_notes') }}
</el-button>
<UploaderRemark
:upload-path="`/datasource/uploadDsSchema/${info.id}`"
@upload-finished="init"
></UploaderRemark>
</div>
</div>
<div class="content">
<div class="side-list">
<div class="select-table_top">
{{ $t('ds.tables') }}
<el-tooltip
effect="dark"
offset="10"
:content="$t('ds.form.choose_tables')"
placement="top"
>
<el-button style="margin-right: -4px" text @click="handleSelectTableList">
<el-icon size="18">
<icon_form_outlined></icon_form_outlined>
</el-icon>
</el-button>
</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"
:draggable="activeRelationship && !tableName.includes(ele.id)"
class="model"
:class="[
currentTable.table_name === ele.table_name && 'isActive',
tableName.includes(ele.id) && activeRelationship && 'disabled-table',
]"
:title="ele.table_name"
@dragstart="($event: any) => singleDragStartD($event, ele)"
@dragend="singleDragEnd"
@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 class="table-relationship">
<div :class="activeRelationship && 'active'" class="btn" @click="handleRelationship">
<el-icon size="16">
<icon_mindnote_outlined></icon_mindnote_outlined>
</el-icon>
{{ t('training.table_relationship_management') }}
</div>
</div>
</div>
<div v-if="activeRelationship" class="relationship-content">
<div class="title">{{ t('training.table_relationship_management') }}</div>
<div class="content">
<TableRelationship
:id="info.id"
:dragging="isDrag"
@get-table-name="getTableName"
></TableRelationship>
</div>
</div>
<div
v-if="currentTable.table_name && !activeRelationship"
v-loading="loading"
class="info-table"
>
<div class="table-name">
<div class="name">
{{ currentTable.table_name }}
<div
style="
display: inline-flex;
align-items: center;
margin-left: 30px;
font-size: 14px;
font-weight: 400;
"
>
<el-switch
v-model="currentTable.checked"
@change="changeChecked"
size="small"
style="margin-right: 8px"
/>
{{ currentTable.checked ? t('user.disable') : t('user.enable') }}
</div>
</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="btnSelect === 'd'" class="field-name">
<el-input
v-model="fieldName"
style="width: 240px"
:placeholder="t('dashboard.search')"
autocomplete="off"
clearable
@input="fieldNameSearch"
/>
<el-button
v-if="ds.type !== 'excel'"
:icon="Refresh"
secondary
style="margin-left: 12px"
@click="syncFields()"
>
{{ t('ds.sync_fields') }}
</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="pageInfo.total && 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>
<div class="table-container">
<el-table :data="previewData.data" style="width: 100%; height: 100%">
<el-table-column
v-for="(c, index) in previewData.fields"
:key="index"
:prop="c"
:label="c"
min-width="150"
:render-header="renderHeader"
/>
</el-table>
</div>
</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;
position: relative;
.ed-button.is-text {
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);
}
}
.export-remark {
position: absolute;
right: 24px;
top: 12px;
}
.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;
.table-relationship {
height: 56px;
width: 100%;
display: flex;
align-items: center;
margin-top: 20px;
position: relative;
&::after {
content: '';
width: calc(100% + 32px);
position: absolute;
left: -16px;
background-color: #1f232926;
top: 0;
height: 1px;
}
.btn {
width: 248px;
height: 32px;
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
padding-left: 8px;
.ed-icon {
color: #646a73;
margin-right: 8px;
}
&.active {
color: var(--ed-color-primary);
.ed-icon {
color: var(--ed-color-primary);
}
background-color: var(--ed-color-primary-1a);
}
}
}
.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% - 156px);
.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;
&:not(:last-child) {
margin-bottom: 2px;
}
&.disabled-table {
background: #dee0e3 !important;
color: #646a73;
cursor: not-allowed;
}
.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);
}
}
}
.relationship-content {
position: absolute;
right: 0;
top: 0;
width: calc(100% - 280px);
height: 100%;
.content {
height: calc(100% - 56px);
width: 100%;
}
.title {
height: 56px;
padding-left: 24px;
line-height: 56px;
font-weight: 500;
font-size: 16px;
border-bottom: 1px solid #1f232926;
}
}
.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;
display: flex;
align-items: center;
}
.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);
position: relative;
.field-name {
position: absolute;
right: 24px;
top: 16px;
display: flex;
}
.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);
.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;
}
.table-container {
width: 100%;
height: calc(100% - 46px);
}
}
}
}
}
}
</style>
<style lang="less">
.notes-dialog {
.ed-textarea__inner {
line-height: 22px;
}
}
</style>