feat(Data Source): SQLBot supports custom data types in data sources #768

This commit is contained in:
dataeaseShu
2026-04-03 16:25:04 +08:00
committed by dataeaseShu
parent 7f543cf194
commit f75770f945
10 changed files with 613 additions and 78 deletions
+1
View File
@@ -6,6 +6,7 @@ export const datasourceApi = {
relationGet: (id: any) => request.post(`/table_relation/get/${id}`),
relationSave: (dsId: any, data: any) => request.post(`/table_relation/save/${dsId}`, data),
add: (data: any) => request.post('/datasource/add', data),
importToDb: (data: any) => request.post('/datasource/importToDb', data),
list: () => request.get('/datasource/list'),
update: (data: any) => request.post('/datasource/update', data),
delete: (id: number, name: string) => request.post(`/datasource/delete/${id}/${name}`),
@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.64869 6.28798L8.09994 10.6605C8.24844 10.7797 8.48944 10.7797 8.63818 10.6605C8.70969 10.6032 8.74969 10.5255 8.74969 10.4447V1.55496C8.74994 1.38672 8.57944 1.24997 8.36919 1.24997C8.26819 1.24997 8.17169 1.28222 8.10019 1.33947L2.64869 5.71197C2.45044 5.87097 2.45044 6.12897 2.64869 6.28797V6.28798Z" fill="#8F959E"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

@@ -1,3 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.79272 5.24009L3.25001 8.88384C3.12626 8.98321 2.92543 8.98321 2.80148 8.88384C2.74189 8.83613 2.70856 8.77134 2.70856 8.70405V1.29591C2.70834 1.1557 2.85043 1.04175 3.02564 1.04175C3.10981 1.04175 3.19022 1.06862 3.24981 1.11633L7.79272 4.76008C7.95793 4.89258 7.95793 5.10758 7.79272 5.24008V5.24009Z" fill="#646A73"/>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.35148 6.28798L3.90023 10.6605C3.75173 10.7797 3.51074 10.7797 3.36199 10.6605C3.29048 10.6032 3.25049 10.5255 3.25049 10.4447V1.55496C3.25023 1.38672 3.42074 1.24997 3.63098 1.24997C3.73199 1.24997 3.82848 1.28222 3.89999 1.33947L9.35148 5.71197C9.54974 5.87097 9.54974 6.12897 9.35148 6.28797V6.28798Z" fill="#8F959E"/>
</svg>

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 436 B

+6
View File
@@ -33,6 +33,9 @@
"enter_variable_value": "Please Enter Variable Value"
},
"sync": {
"records":"Displaying {num} records out of {total}",
"confirm_upload":"Confirm Upload",
"field_details":"Field Details",
"integration": "Platform integration needs to be enabled.",
"the_existing_user": "If the user already exists, overwrite the existing user.",
"sync_users": "Sync Users",
@@ -442,6 +445,9 @@
"get_schema": "Get Schema"
},
"model": {
"int": "Integer",
"float": "Float",
"string": "String",
"default_model": "Default model",
"model_type": "Model type",
"basic_model": "Basic model",
+23 -17
View File
@@ -7,22 +7,6 @@
"AI Model Configuration": "모델 구성",
"Details": "세부"
},
"sync": {
"integration": "플랫폼 통합을 활성화해야 합니다.",
"the_existing_user": "해당 사용자가 이미 존재하는 경우 기존 사용자를 덮어씁니다.",
"sync_users": "사용자 동기화",
"sync_wechat_users": "위챗 사용자 동기화",
"sync_dingtalk_users": "딩톡 사용자 동기화",
"sync_lark_users": "라크 사용자 동기화",
"sync_complete": "동기화 완료",
"synced_10_users": "{num}명 동기화 성공",
"failed_3_users": "{failed}명 동기화 성공, {success}명 동기화 실패",
"download_failure_list": "실패 목록 다운로드",
"sync_failed": "동기화 실패",
"failed_10_users": "{failed}명 동기화 실패",
"continue_syncing": "동기화 계속",
"return_to_view": "화면으로 돌아가기"
},
"variables": {
"please_select_date": "날짜를 선택하십시오",
"number_variable_error": "올바른 숫자 범위를 입력하십시오.",
@@ -48,6 +32,25 @@
"enter_variable_name": "변수 이름을 입력하세요",
"enter_variable_value": "변수 값을 입력하세요"
},
"sync": {
"records": "{total}개 중 {num}개의 레코드 표시",
"confirm_upload": "업로드 확인",
"field_details": "필드 세부 정보",
"integration": "플랫폼 통합을 활성화해야 합니다.",
"the_existing_user": "해당 사용자가 이미 존재하는 경우 기존 사용자를 덮어씁니다.",
"sync_users": "사용자 동기화",
"sync_wechat_users": "위챗 사용자 동기화",
"sync_dingtalk_users": "딩톡 사용자 동기화",
"sync_lark_users": "라크 사용자 동기화",
"sync_complete": "동기화 완료",
"synced_10_users": "{num}명 동기화 성공",
"failed_3_users": "{failed}명 동기화 성공, {success}명 동기화 실패",
"download_failure_list": "실패 목록 다운로드",
"sync_failed": "동기화 실패",
"failed_10_users": "{failed}명 동기화 실패",
"continue_syncing": "동기화 계속",
"return_to_view": "화면으로 돌아가기"
},
"parameter": {
"execution_details": "실행 세부 정보",
"overview": "개요",
@@ -442,6 +445,9 @@
"get_schema": "스키마 가져오기"
},
"model": {
"int": "정수",
"float": "소수",
"string": "문자열",
"default_model": "기본 모델",
"model_type": "모델 유형",
"basic_model": "기본 모델",
@@ -948,4 +954,4 @@
"to_doc": "API 보기",
"trigger_limit": "최대 {0}개의 API 키 생성 지원"
}
}
}
+22 -16
View File
@@ -7,22 +7,6 @@
"AI Model Configuration": "模型配置",
"Details": "详情"
},
"sync": {
"integration": "需开启平台对接",
"the_existing_user": "若用户已存在,覆盖旧用户",
"sync_users": "同步用户",
"sync_wechat_users": "同步企业微信用户",
"sync_dingtalk_users": "同步钉钉用户",
"sync_lark_users": "同步飞书用户",
"sync_complete": "同步完成",
"synced_10_users": "成功同步 {num} 个用户",
"failed_3_users": "成功同步 {success} 个用户,同步失败 {failed} 个用户,",
"download_failure_list": "下载失败列表",
"sync_failed": "同步失败",
"failed_10_users": "同步失败 {num} 个用户,",
"continue_syncing": "继续同步",
"return_to_view": "返回查看"
},
"variables": {
"please_select_date": "请选择日期",
"number_variable_error": "请输入正确的数值范围",
@@ -48,6 +32,25 @@
"enter_variable_name": "请输入变量名称",
"enter_variable_value": "请输入变量值"
},
"sync": {
"records":"显示 {num} 条数据,共 {total} 条",
"confirm_upload":"确定上传",
"field_details":"字段详情",
"integration": "需开启平台对接",
"the_existing_user": "若用户已存在,覆盖旧用户",
"sync_users": "同步用户",
"sync_wechat_users": "同步企业微信用户",
"sync_dingtalk_users": "同步钉钉用户",
"sync_lark_users": "同步飞书用户",
"sync_complete": "同步完成",
"synced_10_users": "成功同步 {num} 个用户",
"failed_3_users": "成功同步 {success} 个用户,同步失败 {failed} 个用户,",
"download_failure_list": "下载失败列表",
"sync_failed": "同步失败",
"failed_10_users": "同步失败 {num} 个用户,",
"continue_syncing": "继续同步",
"return_to_view": "返回查看"
},
"parameter": {
"execution_details": "执行详情",
"overview": "概览",
@@ -442,6 +445,9 @@
"get_schema": "获取Schema"
},
"model": {
"int": "整数",
"float": "小数",
"string": "字符串",
"default_model": "默认模型",
"model_type": "模型类型",
"basic_model": "基础模型",
+5 -2
View File
@@ -420,10 +420,13 @@ strong {
}
}
.text-variables.text-variables {
.text-variables.text-variables,
.string-variables.string-variables {
color: #3370ff;
}
.number-variables.number-variables {
.number-variables.number-variables,
.int-variables.int-variables,
.float-variables.float-variables {
color: #04b49c;
}
.datetime-variables.datetime-variables {
+59 -41
View File
@@ -17,6 +17,8 @@ import EmptyBackground from '@/views/dashboard/common/EmptyBackground.vue'
import icon_fileExcel_colorful from '@/assets/datasource/icon_excel.png'
import IconOpeDelete from '@/assets/svg/icon_delete.svg'
import { useCache } from '@/utils/useCache'
import ExcelDetailDialog from './ExcelDetailDialog.vue'
import icon_visible_outlined from '@/assets/embedded/icon_visible_outlined.svg'
const props = withDefaults(
defineProps<{
@@ -34,6 +36,7 @@ const props = withDefaults(
)
const dsFormRef = ref<FormInstance>()
const excelDetailDialogRef = ref<InstanceType<typeof ExcelDetailDialog>>()
const emit = defineEmits(['refresh', 'changeActiveStep', 'close'])
const isCreate = ref(true)
const isEditTable = ref(false)
@@ -44,7 +47,7 @@ const tableListLoading = ref(false)
const tableListLoadingV1 = ref(false)
const checkLoading = ref(false)
const dialogTitle = ref('')
const getUploadURL = import.meta.env.VITE_API_BASE_URL + '/datasource/uploadExcel'
const getUploadURL = import.meta.env.VITE_API_BASE_URL + '/datasource/parseExcel'
const saveLoading = ref<boolean>(false)
const uploadLoading = ref(false)
const { t } = useI18n()
@@ -54,7 +57,7 @@ const rules = reactive<FormRules>({
name: [
{
required: true,
message: t('datasource.please_enter') + t('common.empty') + t('ds.form.name'),
message: t('datasource.please_enter') + t('common.empty') + t('ds.name'),
trigger: 'blur',
},
{ min: 1, max: 50, message: t('ds.form.validate.name_length'), trigger: 'blur' },
@@ -166,9 +169,7 @@ const initForm = (item: any, editTable: boolean = false) => {
? configuration.lowVersion
: true
form.value.ssl =
configuration.ssl !== null && configuration.ssl !== undefined
? configuration.ssl
: false
configuration.ssl !== null && configuration.ssl !== undefined ? configuration.ssl : false
}
if (editTable) {
@@ -431,15 +432,26 @@ const beforeUpload = (rawFile: any) => {
uploadLoading.value = true
return true
}
let fileDetail: any = null
const onSuccess = (response: any) => {
form.value.filename = response.data.filename
form.value.sheets = response.data.sheets
tableList.value = response.data.sheets
fileDetail = response.data
excelDetailDialogRef.value?.init(response.data)
excelUploadSuccess.value = true
uploadLoading.value = false
}
const openFile = () => {
onSuccess({
data: fileDetail,
})
}
const saveExcel = (excel: any) => {
form.value.filename = excel.filename
form.value.sheets = excel.sheets
tableList.value = excel.sheets
}
const onError = (e: any) => {
ElMessage.error(e.toString())
uploadLoading.value = false
@@ -552,6 +564,26 @@ defineExpose({
:rules="rules"
@submit.prevent
>
<el-form-item :label="t('ds.name')" prop="name">
<el-input
v-model="form.name"
clearable
:placeholder="$t('datasource.please_enter') + $t('common.empty') + t('ds.name')"
/>
</el-form-item>
<el-form-item :label="t('ds.form.description')">
<el-input
v-model="form.description"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + t('ds.form.description')
"
:rows="2"
show-word-limit
maxlength="200"
clearable
type="textarea"
/>
</el-form-item>
<div v-if="form.type === 'excel'">
<el-form-item prop="sheets" :label="t('ds.form.file')">
<div v-if="form.filename" class="pdf-card">
@@ -560,9 +592,22 @@ defineExpose({
<div class="name">{{ form.filename }}</div>
<div class="size">{{ form.filename.split('.')[1] }} - {{ fileSize }}</div>
</div>
<el-icon v-if="!form.id" class="action-btn" size="16" @click="clearFile">
<IconOpeDelete></IconOpeDelete>
</el-icon>
<div
style="
width: 40px;
margin-left: auto;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<el-icon v-if="!form.id" class="action-btn" size="16" @click="openFile">
<icon_visible_outlined></icon_visible_outlined>
</el-icon>
<el-icon v-if="!form.id" class="action-btn" size="16" @click="clearFile">
<IconOpeDelete></IconOpeDelete>
</el-icon>
</div>
</div>
<el-upload
v-if="form.filename && !form.id"
@@ -602,26 +647,6 @@ defineExpose({
<span v-if="!form.filename" class="not_exceed">{{ $t('common.not_exceed_50mb') }}</span>
</el-form-item>
</div>
<el-form-item :label="t('ds.form.name')" prop="name">
<el-input
v-model="form.name"
clearable
:placeholder="$t('datasource.please_enter') + $t('common.empty') + t('ds.form.name')"
/>
</el-form-item>
<el-form-item :label="t('ds.form.description')">
<el-input
v-model="form.description"
:placeholder="
$t('datasource.please_enter') + $t('common.empty') + t('ds.form.description')
"
:rows="2"
show-word-limit
maxlength="200"
clearable
type="textarea"
/>
</el-form-item>
<div v-if="form.type !== 'excel'" style="margin-top: 16px">
<el-form-item
:label="form.type !== 'es' ? t('ds.form.host') : t('ds.form.address')"
@@ -694,11 +719,7 @@ defineExpose({
>
<el-checkbox v-model="form.lowVersion" :label="t('ds.form.low_version')" />
</el-form-item>
<el-form-item
v-if="form.type === 'mysql'"
:label="t('ds.form.ssl')"
prop="ssl"
>
<el-form-item v-if="form.type === 'mysql'" :label="t('ds.form.ssl')" prop="ssl">
<el-switch v-model="form.ssl" />
</el-form-item>
<el-form-item v-if="form.type !== 'es'" :label="t('ds.form.extra_jdbc')">
@@ -829,6 +850,7 @@ defineExpose({
</el-button>
</div>
</div>
<ExcelDetailDialog ref="excelDetailDialogRef" @finish="saveExcel" />
</template>
<style lang="less" scoped>
@@ -898,10 +920,6 @@ defineExpose({
}
}
.action-btn {
margin-left: auto;
}
.ed-icon {
position: relative;
cursor: pointer;
+276
View File
@@ -0,0 +1,276 @@
<script setup lang="ts">
import { ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import SheetTabs from './SheetTabs.vue'
import field_text from '@/assets/svg/field_text.svg'
import field_time from '@/assets/svg/field_time.svg'
import field_value from '@/assets/svg/field_value.svg'
import { datasourceApi } from '@/api/datasource'
const { t } = useI18n()
const emits = defineEmits(['finish'])
const tabList = shallowRef([])
const fieldList = shallowRef([])
const iconMap = {
string: field_text,
int: field_value,
float: field_value,
datetime: field_time,
}
const dialogShow = ref(false)
const loading = ref(false)
const previewData = ref<any>({})
const variables = [
{
name: t('model.text'),
var_type: 'string',
},
{
name: t('model.int'),
var_type: 'int',
},
{
name: t('model.float'),
var_type: 'float',
},
{
name: t('dashboard.time'),
var_type: 'datetime',
},
]
let arr: any = []
let filePath = ''
const init = (response: any) => {
arr = response.data || []
filePath = response.filePath
previewData.value = response.data[0]
activeTab.value = previewData.value.sheetName
fieldList.value = previewData.value.fields
tabList.value = response.data.map((item: any) => {
return {
label: item.sheetName,
value: item.sheetName,
}
})
dialogShow.value = true
}
const closeDialog = () => {
arr = []
previewData.value = {
sheetName: '',
fields: [],
data: [],
rows: 0,
}
tabList.value = []
fieldList.value = []
activeTab.value = ''
btnSelect.value = 'd'
dialogShow.value = false
}
const save = () => {
loading.value = true
datasourceApi
.importToDb({
sheets: arr.map((item: any) => {
return { fields: item.fields, sheetName: item.sheetName }
}),
filePath,
})
.then((res: any) => {
closeDialog()
emits('finish', res)
})
.finally(() => {
loading.value = false
})
}
const btnSelect = ref('d')
const btnSelectClick = (val: any) => {
btnSelect.value = val
}
const activeTab = ref('')
const handleTabClick = (tab: any) => {
btnSelectClick('d')
activeTab.value = tab.value
arr.forEach((item: any) => {
if (item.sheetName === activeTab.value) {
previewData.value = item
}
})
fieldList.value = previewData.value.fields
}
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
}
defineExpose({
init,
})
</script>
<template>
<el-dialog
v-model="dialogShow"
:title="$t('ds.preview')"
width="1200"
modal-class="excel-detail-dialog"
destroy-on-close
:close-on-click-modal="false"
@before-closed="closeDialog"
>
<SheetTabs :active-tab="activeTab" :tab-list="tabList" @tab-click="handleTabClick"></SheetTabs>
<div v-loading="loading" class="content">
<div class="btn-select" style="margin: 8px 12px">
<el-button :class="[btnSelect === 'd' && 'is-active']" text @click="btnSelectClick('d')">
{{ t('ds.preview') }}
</el-button>
<el-button :class="[btnSelect === 'q' && 'is-active']" text @click="btnSelectClick('q')">
{{ t('sync.field_details') }}
</el-button>
</div>
<div class="preview-num">
{{ t('sync.records', { num: previewData.data.length, total: previewData.rows }) }}
</div>
<div v-if="btnSelect === 'q'" class="field-details">
<el-table row-class-name="hover-icon_edit" :data="fieldList" style="width: 100%">
<el-table-column prop="fieldName" :label="t('datasource.field_name')" />
<el-table-column prop="fieldType" :label="t('datasource.field_type')" width="240">
<template #default="scope">
<el-icon
:class="`${scope.row.fieldType}-variables`"
size="16"
style="position: absolute; top: 20px; left: 32px; z-index: 10"
>
<component :is="iconMap[scope.row.fieldType as keyof typeof iconMap]"></component>
</el-icon>
<el-select
v-model="scope.row.fieldType"
style="max-width: 197px; margin-left: 8px"
:placeholder="t('datasource.Please_select')"
>
<el-option
v-for="ele in variables"
:key="ele.var_type"
:label="ele.name"
:value="ele.var_type"
>
<div style="width: 100%; display: flex; align-items: center">
<el-icon
:class="`${ele.var_type}-variables`"
size="16"
style="margin-right: 4px"
>
<component :is="iconMap[ele.var_type as keyof typeof iconMap]"></component>
</el-icon>
{{ ele.name }}
</div>
</el-option>
</el-select>
</template>
</el-table-column>
</el-table>
</div>
<div v-else class="preview">
<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.fieldName"
:label="c.fieldName"
min-width="150"
:render-header="renderHeader"
/>
</el-table>
</div>
</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 20px">
<el-button secondary @click="closeDialog">{{ $t('common.cancel') }}</el-button>
<el-button v-loading="loading" type="primary" @click="save">{{
$t('sync.confirm_upload')
}}</el-button>
</div>
</el-dialog>
</template>
<style lang="less">
.excel-detail-dialog {
.content {
height: 593px;
background-color: #f5f6f7;
border: 1px solid #dee0e3;
border-radius: 6px;
margin-top: -1px;
border-top-left-radius: 0;
position: relative;
z-index: 1;
.field-details {
.ed-select__wrapper {
padding-left: 32px;
}
}
.btn-select {
height: 32px;
padding-left: 4px;
padding-right: 4px;
display: inline-flex;
background: #ffffff;
align-items: center;
border: 1px solid #d0d3d6;
border-radius: 6px;
.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-num {
margin: 0 0 8px 12px;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #646a73;
}
.table-container {
width: 100%;
height: calc(100% - 46px);
}
}
}
</style>
+216
View File
@@ -0,0 +1,216 @@
<script lang="ts" setup>
import icon_expandLeft_filled from '@/assets/svg/icon_expand-left_filled.svg'
import icon_expandRight_filled from '@/assets/svg/icon_expand-right_filled.svg'
import { toRefs, ref, watch, nextTick } from 'vue'
import { propTypes } from '@/utils/propTypes'
const props = defineProps({
tabList: propTypes.arrayOf(
propTypes.shape({
label: String,
value: String,
})
),
activeTab: propTypes.string.def(''),
})
const activeTabIndex = ref(0)
const emits = defineEmits(['TabClick'])
const { activeTab } = toRefs(props)
const handleTabClick = (tab: any) => {
let tabDom = document.getElementById(`tab-${tab.value}`)
if (tabDom!.offsetLeft + tabDom!.offsetWidth > tabWrapper.value.offsetWidth) {
tabWrapper.value.scrollLeft =
tabDom!.offsetLeft + tabDom!.offsetWidth - tabWrapper.value.offsetWidth
} else {
tabWrapper.value.scrollLeft = 0
}
emits('TabClick', tab)
}
const tabWrapper = ref()
const showBtn = ref(false)
watch(
() => activeTab.value,
(val) => {
activeTabIndex.value = props.tabList!.findIndex((ele) => ele.value === val)
},
{ immediate: true }
)
watch(
() => props.tabList,
() => {
nextTick(() => {
showBtn.value = tabWrapper.value.scrollWidth > tabWrapper.value.offsetWidth
})
},
{ immediate: true }
)
const prevClick = () => {
let domWrapper = tabWrapper.value
if (!domWrapper.scrollLeft) return
domWrapper.scrollLeft -= 30
}
const nextClick = () => {
let domWrapper = tabWrapper.value
domWrapper.scrollLeft += 30
}
</script>
<template>
<div class="sheet-tabs">
<div ref="tabWrapper" class="tab-wrapper">
<div
v-for="tab in tabList"
:id="`tab-${tab.value}`"
:key="tab.label"
:title="tab.label"
:class="[{ active: activeTab === tab.value }, 'sheet-tab']"
@click="handleTabClick(tab)"
>
<span class="ellipsis">
{{ tab.label }}
</span>
</div>
</div>
<div v-if="showBtn" class="tab-btn">
<el-icon size="12px" @click="prevClick">
<icon_expandLeft_filled />
</el-icon>
<el-icon size="12px" @click="nextClick">
<icon_expandRight_filled />
</el-icon>
</div>
</div>
</template>
<style lang="less" scoped>
.sheet-tabs {
border-top-left-radius: 3px;
width: 100%;
position: relative;
padding-right: 60px;
z-index: 10;
.tab-wrapper {
height: 100%;
display: flex;
overflow-x: auto;
&::-webkit-scrollbar {
display: none;
}
}
.tab-btn {
padding: 8px 12px;
display: flex;
justify-content: space-between;
width: 60px;
height: 28px;
position: absolute;
right: 0;
top: 4px;
background: #fff;
.ed-icon {
color: #8d9199;
cursor: pointer;
&.disabled {
cursor: not-allowed;
}
&:not(.disabled):hover {
color: var(--ed-color-primary);
}
& + .ed-icon {
margin-left: 12px;
}
}
}
.sheet-tab {
color: #1f2329;
cursor: pointer;
position: relative;
padding: 0 20px;
display: flex;
align-items: center;
height: 36px;
max-width: 200px;
&:hover {
color: var(--ed-color-primary);
}
.ellipsis {
max-width: 200px;
font-size: 14px;
}
&::after,
&::before {
content: '';
position: absolute;
height: 24px;
width: 1px;
top: 50%;
transform: translateY(-50%);
background: rgba(31, 35, 41, 0.15);
}
&::after {
right: 0;
}
&::before {
left: 0;
}
& + .active {
::before {
content: '';
left: -3px;
height: 30px;
width: 2px;
position: absolute;
top: 0;
background: #fff;
}
}
}
.active {
box-shadow: 0px -1px 0px 0px #f5f6f7 inset;
color: var(--ed-color-primary);
border: 1px solid rgba(31, 35, 41, 0.15);
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background: #f5f6f7;
position: relative;
&::before,
&::after {
display: none;
}
&::after {
content: '';
height: 3px;
width: 100%;
position: absolute;
top: calc(100% - 1px);
left: 0;
background: #f5f6f7;
display: block;
}
& + .sheet-tab {
&::before {
display: none;
}
}
}
}
</style>