pref: improve Table sort

This commit is contained in:
ulleo
2026-02-28 18:24:02 +08:00
committed by ulleo
parent 82de276aad
commit d6f4e6b43f
12 changed files with 73 additions and 128 deletions
@@ -73,4 +73,4 @@ const changeLanguage = (lang: string) => {
.selected-lang {
color: var(--el-color-primary);
}
</style>
</style>
+11 -14
View File
@@ -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
}
+1 -1
View File
@@ -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: {
+2 -2
View File
@@ -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>
+6 -6
View File
@@ -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>
+3 -1
View File
@@ -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>
+3 -1
View File
@@ -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