mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
pref: improve Table sort
This commit is contained in:
@@ -73,4 +73,4 @@ const changeLanguage = (lang: string) => {
|
||||
.selected-lang {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
+11
-14
@@ -27,16 +27,13 @@ export function highlightKeyword(
|
||||
highlightClass: string = 'highlight'
|
||||
): string {
|
||||
if (!keyword) return escapeHtml(text)
|
||||
|
||||
|
||||
const escapedText = escapeHtml(text)
|
||||
const escapedKeyword = escapeHtml(keyword)
|
||||
|
||||
|
||||
// Use case-insensitive replace
|
||||
const regex = new RegExp(escapedKeyword, 'gi')
|
||||
return escapedText.replace(
|
||||
regex,
|
||||
(match) => `<span class="${highlightClass}">${match}</span>`
|
||||
)
|
||||
return escapedText.replace(regex, (match) => `<span class="${highlightClass}">${match}</span>`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,18 +45,18 @@ export function sanitizeHtml(html: string): string {
|
||||
// Create a temporary div to parse HTML
|
||||
const temp = document.createElement('div')
|
||||
temp.innerHTML = html
|
||||
|
||||
|
||||
// List of allowed tags
|
||||
const allowedTags = ['b', 'i', 'u', 'strong', 'em', 'span', 'p', 'br', 'a']
|
||||
|
||||
|
||||
// List of allowed attributes
|
||||
const allowedAttrs = ['class', 'href', 'title']
|
||||
|
||||
|
||||
// Remove disallowed tags and attributes
|
||||
const sanitize = (node: Node): void => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element
|
||||
|
||||
|
||||
// Check if tag is allowed
|
||||
if (!allowedTags.includes(element.tagName.toLowerCase())) {
|
||||
// Replace with text content
|
||||
@@ -67,14 +64,14 @@ export function sanitizeHtml(html: string): string {
|
||||
element.parentNode?.replaceChild(textNode, element)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Remove disallowed attributes
|
||||
Array.from(element.attributes).forEach((attr) => {
|
||||
if (!allowedAttrs.includes(attr.name.toLowerCase())) {
|
||||
element.removeAttribute(attr.name)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// For links, ensure they don't use javascript: protocol
|
||||
if (element.tagName.toLowerCase() === 'a') {
|
||||
const href = element.getAttribute('href') || ''
|
||||
@@ -83,11 +80,11 @@ export function sanitizeHtml(html: string): string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Recursively sanitize child nodes
|
||||
Array.from(node.childNodes).forEach(sanitize)
|
||||
}
|
||||
|
||||
|
||||
sanitize(temp)
|
||||
return temp.innerHTML
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ function getLogList() {
|
||||
<span>{{ $t('parameter.tokens_required') }} {{ totalTokens }}</span>
|
||||
<span style="margin-left: 12px">{{ $t('parameter.time_execution') }} {{ duration }} s</span>
|
||||
|
||||
<div @click="getLogList" class="detail">
|
||||
<div class="detail" @click="getLogList">
|
||||
<el-icon style="margin-right: 4px" size="16">
|
||||
<icon_logs_outlined></icon_logs_outlined>
|
||||
</el-icon>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BaseChart, type ChartAxis, type ChartData } from '@/views/chat/component/BaseChart.ts'
|
||||
import {
|
||||
copyToClipboard,
|
||||
type Node,
|
||||
type S2DataConfig,
|
||||
S2Event,
|
||||
type S2MountContainer,
|
||||
@@ -71,106 +70,51 @@ export class Table extends BaseChart {
|
||||
data: this.data,
|
||||
}
|
||||
|
||||
const sortState: Record<string, string> = {}
|
||||
|
||||
const handleSortClick = (params: any) => {
|
||||
const { meta } = params
|
||||
const s2 = meta.spreadsheet
|
||||
if (s2 && meta.isLeaf) {
|
||||
const fieldId = meta.field
|
||||
const currentMethod = sortState[fieldId] || 'none'
|
||||
const sortOrder = ['none', 'desc', 'asc']
|
||||
const nextMethod = sortOrder[(sortOrder.indexOf(currentMethod) + 1) % sortOrder.length]
|
||||
sortState[fieldId] = nextMethod
|
||||
s2.groupSortByMethod(nextMethod === 'none' ? 'none' : (nextMethod as SortMethod), meta)
|
||||
s2.render()
|
||||
}
|
||||
}
|
||||
|
||||
const s2Options: S2Options = {
|
||||
width: 600,
|
||||
height: 360,
|
||||
showDefaultHeaderActionIcon: true,
|
||||
showDefaultHeaderActionIcon: false,
|
||||
headerActionIcons: [
|
||||
{
|
||||
icons: ['GlobalDesc'],
|
||||
belongsCell: 'colCell',
|
||||
displayCondition: (node: any) => node.isLeaf && sortState[node.field] === 'desc',
|
||||
onClick: handleSortClick,
|
||||
},
|
||||
{
|
||||
icons: ['GlobalAsc'],
|
||||
belongsCell: 'colCell',
|
||||
displayCondition: (node: any) => node.isLeaf && sortState[node.field] === 'asc',
|
||||
onClick: handleSortClick,
|
||||
},
|
||||
{
|
||||
icons: ['SortDown'],
|
||||
belongsCell: 'colCell',
|
||||
displayCondition: (node: any) =>
|
||||
node.isLeaf && (!sortState[node.field] || sortState[node.field] === 'none'),
|
||||
onClick: handleSortClick,
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
operation: {
|
||||
// 开启组内排序
|
||||
sort: true,
|
||||
},
|
||||
dataCell: {
|
||||
enable: true,
|
||||
content: (cell) => {
|
||||
const meta = cell.getMeta()
|
||||
const container = document.createElement('div')
|
||||
container.style.padding = '8px 0'
|
||||
container.style.minWidth = '100px'
|
||||
container.style.maxWidth = '400px'
|
||||
container.style.display = 'flex'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.padding = '8px 16px'
|
||||
container.style.cursor = 'pointer'
|
||||
container.style.color = '#606266'
|
||||
container.style.fontSize = '14px'
|
||||
container.style.whiteSpace = 'pre-wrap'
|
||||
|
||||
const text = document.createTextNode(meta.fieldValue)
|
||||
container.appendChild(text)
|
||||
|
||||
return container
|
||||
},
|
||||
},
|
||||
colCell: {
|
||||
enable: true,
|
||||
content: (cell) => {
|
||||
const meta = cell.getMeta()
|
||||
const { spreadsheet: s2 } = meta
|
||||
if (!meta.isLeaf) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 创建类似Element Plus下拉菜单的结构
|
||||
const container = document.createElement('div')
|
||||
container.className = 'el-dropdown'
|
||||
container.style.padding = '8px 0'
|
||||
container.style.minWidth = '100px'
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: t('chat.sort_desc'),
|
||||
method: 'desc' as SortMethod,
|
||||
icon: 'el-icon-sort-down',
|
||||
},
|
||||
{ label: t('chat.sort_asc'), method: 'asc' as SortMethod, icon: 'el-icon-sort-up' },
|
||||
{ label: t('chat.sort_none'), method: 'none' as SortMethod, icon: 'el-icon-close' },
|
||||
]
|
||||
|
||||
menuItems.forEach((item) => {
|
||||
const itemEl = document.createElement('div')
|
||||
itemEl.className = 'el-dropdown-menu__item'
|
||||
itemEl.style.display = 'flex'
|
||||
itemEl.style.alignItems = 'center'
|
||||
itemEl.style.padding = '8px 16px'
|
||||
itemEl.style.cursor = 'pointer'
|
||||
itemEl.style.color = '#606266'
|
||||
itemEl.style.fontSize = '14px'
|
||||
|
||||
// 鼠标悬停效果
|
||||
itemEl.addEventListener('mouseenter', () => {
|
||||
itemEl.style.backgroundColor = '#f5f7fa'
|
||||
itemEl.style.color = '#409eff'
|
||||
})
|
||||
itemEl.addEventListener('mouseleave', () => {
|
||||
itemEl.style.backgroundColor = 'transparent'
|
||||
itemEl.style.color = '#606266'
|
||||
})
|
||||
|
||||
// 添加图标(如果需要)
|
||||
if (item.icon) {
|
||||
const icon = document.createElement('i')
|
||||
icon.className = item.icon
|
||||
icon.style.marginRight = '8px'
|
||||
icon.style.fontSize = '16px'
|
||||
itemEl.appendChild(icon)
|
||||
}
|
||||
|
||||
const text = document.createTextNode(item.label)
|
||||
itemEl.appendChild(text)
|
||||
|
||||
itemEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
s2.groupSortByMethod(item.method, meta as Node)
|
||||
// 可以在这里添加关闭tooltip的逻辑
|
||||
})
|
||||
|
||||
container.appendChild(itemEl)
|
||||
})
|
||||
|
||||
return container
|
||||
},
|
||||
},
|
||||
},
|
||||
// 如果有省略号, 复制到的是完整文本
|
||||
interaction: {
|
||||
|
||||
@@ -364,7 +364,7 @@ const btnSelectClick = (val: any) => {
|
||||
</el-icon>
|
||||
<div class="name">{{ info.name }}</div>
|
||||
<div class="export-remark">
|
||||
<el-button style="margin-right: 12px" @click="downloadTemplate" secondary>
|
||||
<el-button style="margin-right: 12px" secondary @click="downloadTemplate">
|
||||
<template #icon>
|
||||
<icon_import_outlined></icon_import_outlined>
|
||||
</template>
|
||||
@@ -387,7 +387,7 @@ const btnSelectClick = (val: any) => {
|
||||
:content="$t('ds.form.choose_tables')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button style="margin-right: -4px" @click="handleSelectTableList" text>
|
||||
<el-button style="margin-right: -4px" text @click="handleSelectTableList">
|
||||
<el-icon size="18">
|
||||
<icon_form_outlined></icon_form_outlined>
|
||||
</el-icon>
|
||||
|
||||
@@ -419,12 +419,12 @@ const save = () => {
|
||||
<svg style="position: fixed; top: -9999px" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<filter
|
||||
id="filter-dropShadow-v0-3329848037"
|
||||
x="-1"
|
||||
y="-1"
|
||||
width="3"
|
||||
height="3"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="filter-dropShadow-v0-3329848037"
|
||||
>
|
||||
<feDropShadow
|
||||
stdDeviation="4"
|
||||
@@ -436,18 +436,18 @@ const save = () => {
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
<div v-loading="loading" v-if="!nodeIds.length" class="relationship-empty">
|
||||
<div v-if="!nodeIds.length" v-loading="loading" class="relationship-empty">
|
||||
{{ t('training.add_it_here') }}
|
||||
</div>
|
||||
<div v-loading="loading" v-else id="container"></div>
|
||||
<div v-else id="container" v-loading="loading"></div>
|
||||
<div
|
||||
@dragover.prevent.stop="dragover"
|
||||
@drop.prevent.stop="drop"
|
||||
v-show="dragging"
|
||||
class="drag-mask"
|
||||
@dragover.prevent.stop="dragover"
|
||||
@drop.prevent.stop="drop"
|
||||
></div>
|
||||
<div class="save-btn">
|
||||
<el-button type="primary" v-if="nodeIds.length" @click="save">
|
||||
<el-button v-if="nodeIds.length" type="primary" @click="save">
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
<div class="config-area">
|
||||
<div class="login-logo">
|
||||
<div class="login-logo-icon">
|
||||
<img height="52" v-if="pageLogin" :src="pageLogin" alt="" />
|
||||
<el-icon size="52" v-else
|
||||
<img v-if="pageLogin" height="52" :src="pageLogin" alt="" />
|
||||
<el-icon v-else size="52"
|
||||
><custom_small v-if="themeColor !== 'default'"></custom_small>
|
||||
<LOGO_fold v-else></LOGO_fold
|
||||
></el-icon>
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<div class="navigate-preview" style="height: 425px">
|
||||
<div class="navigate-head">
|
||||
<div class="header-sql">
|
||||
<img height="30" width="30" v-if="pageLogin" :src="pageLogin" alt="" />
|
||||
<img v-if="pageLogin" height="30" width="30" :src="pageLogin" alt="" />
|
||||
<custom_small v-else-if="themeColor !== 'default'" class="logo" />
|
||||
<logo v-else></logo>
|
||||
<span style="margin-left: 8px">{{ loginForm.name }}</span>
|
||||
|
||||
@@ -69,7 +69,9 @@ defineExpose({ showErrorMask })
|
||||
</div>
|
||||
<div class="type-value">
|
||||
<span class="type">{{ $t('model.model_type') }}</span>
|
||||
<span class="value"> {{ modelType.startsWith('modelType.') ? $t(modelType) : modelType }}</span>
|
||||
<span class="value">
|
||||
{{ modelType.startsWith('modelType.') ? $t(modelType) : modelType }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="type-value">
|
||||
<span class="type">{{ $t('model.basic_model') }}</span>
|
||||
|
||||
@@ -426,7 +426,9 @@ const submit = (item: any) => {
|
||||
>
|
||||
<template #header="{ close }">
|
||||
<span style="white-space: nowrap">{{
|
||||
editModel ? $t('dashboard.edit') + $t('common.empty') + $t(activeNameI18nKey) : t('model.add_model')
|
||||
editModel
|
||||
? $t('dashboard.edit') + $t('common.empty') + $t(activeNameI18nKey)
|
||||
: t('model.add_model')
|
||||
}}</span>
|
||||
<div v-if="!editModel" class="flex-center" style="width: 100%">
|
||||
<el-steps custom style="max-width: 500px; flex: 1" :active="activeStep" align-center>
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
:filter-node-method="filterNode"
|
||||
show-checkbox
|
||||
:default-checked-keys="defaultCheckedKeys"
|
||||
@check="handleCheck"
|
||||
:props="defaultProps"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
@check="handleCheck"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="custom-tree-node flex">
|
||||
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-checkbox style="float: left" v-model="existingUser">
|
||||
<el-checkbox v-model="existingUser" style="float: left">
|
||||
{{ $t('sync.the_existing_user') }}
|
||||
</el-checkbox>
|
||||
<el-button secondary @click="centerDialogVisible = false">
|
||||
|
||||
@@ -453,9 +453,9 @@ const handleCurrentChange = (val: number) => {
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('variables.variable_type')">
|
||||
<el-radio-group
|
||||
v-model="pageForm.var_type"
|
||||
:disabled="!!pageForm.id"
|
||||
@change="varTypeChange"
|
||||
v-model="pageForm.var_type"
|
||||
>
|
||||
<el-radio value="text">{{ $t('model.text') }}</el-radio>
|
||||
<el-radio value="number">
|
||||
@@ -481,8 +481,8 @@ const handleCurrentChange = (val: number) => {
|
||||
</template>
|
||||
<div class="value-list">
|
||||
<el-form-item
|
||||
:key="index"
|
||||
v-for="(_, index) in pageForm.value"
|
||||
:key="index"
|
||||
:prop="'value.' + index"
|
||||
:rules="{
|
||||
required: true,
|
||||
@@ -510,7 +510,7 @@ const handleCurrentChange = (val: number) => {
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item prop="value" v-else :label="t('variables.variable_value')">
|
||||
<el-form-item v-else prop="value" :label="t('variables.variable_value')">
|
||||
<div class="value-number_date">
|
||||
<template v-if="pageForm.var_type === 'number'">
|
||||
<el-input
|
||||
|
||||
Reference in New Issue
Block a user