mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-07 06:59:32 +08:00
♻️ refactor(frontend-sync): 优化桌面交互细节并移除 main 回灌 dev 自动化
- 优化新建连接、主题设置、侧边栏工具区与 SQL 日志的界面表现 - 调整分页、筛选、透明模式与弹窗样式,统一整体交互层次 - 收口外观参数生效逻辑并补齐多组件适配 - 删除 sync-main-to-dev 工作流并同步维护者手动回灌说明
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useContext, useMemo, useCallback }
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, Select, Modal, Checkbox, Segmented, Tooltip, Popover } from 'antd';
|
||||
import type { SortOrder } from 'antd/es/table/interface';
|
||||
import { ReloadOutlined, ImportOutlined, ExportOutlined, DownOutlined, PlusOutlined, DeleteOutlined, SaveOutlined, UndoOutlined, FilterOutlined, CloseOutlined, ConsoleSqlOutlined, FileTextOutlined, CopyOutlined, ClearOutlined, EditOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons';
|
||||
import { ReloadOutlined, ImportOutlined, ExportOutlined, DownOutlined, PlusOutlined, DeleteOutlined, SaveOutlined, UndoOutlined, FilterOutlined, CloseOutlined, ConsoleSqlOutlined, FileTextOutlined, CopyOutlined, ClearOutlined, EditOutlined, VerticalAlignBottomOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { ImportData, ExportTable, ExportData, ExportQuery, ApplyChanges, DBGetColumns } from '../../wailsjs/go/app/App';
|
||||
import ImportPreviewModal from './ImportPreviewModal';
|
||||
@@ -11,7 +11,7 @@ import type { ColumnDefinition } from '../types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { buildOrderBySQL, buildWhereSQL, escapeLiteral, quoteIdentPart, quoteQualifiedIdent, withSortBufferTuningSQL, type FilterCondition } from '../utils/sql';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform } from '../utils/appearance';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
|
||||
|
||||
// --- Error Boundary ---
|
||||
@@ -639,7 +639,8 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const setQueryOptions = useStore(state => state.setQueryOptions);
|
||||
const isMacLike = useMemo(() => isMacLikePlatform(), []);
|
||||
const darkMode = theme === 'dark';
|
||||
const opacity = normalizeOpacityForPlatform(appearance.opacity);
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const canModifyData = !readOnly && !!tableName;
|
||||
const showColumnComment = queryOptions?.showColumnComment !== false;
|
||||
const showColumnType = queryOptions?.showColumnType !== false;
|
||||
@@ -706,6 +707,33 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const toolbarDividerColor = darkMode ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.10)';
|
||||
const columnMetaHintColor = darkMode ? darkHighlightTextColor : lightMetaHintColor;
|
||||
const columnMetaTooltipColor = darkMode ? darkHighlightTextColor : lightMetaTooltipColor;
|
||||
const paginationPageSizeOptions = ['100', '200', '500', '1000'];
|
||||
const paginationGlassMode = opacity < 0.999 || resolvedAppearance.blur > 0;
|
||||
const paginationShellBg = darkMode
|
||||
? `linear-gradient(135deg, rgba(17,22,34,${paginationGlassMode ? Math.max(0.22, opacity * 0.38) : 0.82}) 0%, rgba(10,14,24,${paginationGlassMode ? Math.max(0.28, opacity * 0.46) : 0.9}) 100%)`
|
||||
: `linear-gradient(135deg, rgba(255,255,255,${paginationGlassMode ? Math.max(0.24, opacity * 0.36) : 0.96}) 0%, rgba(246,248,252,${paginationGlassMode ? Math.max(0.32, opacity * 0.44) : 0.99}) 100%)`;
|
||||
const paginationShellBorderColor = darkMode
|
||||
? `rgba(255,255,255,${paginationGlassMode ? 0.10 : 0.08})`
|
||||
: `rgba(16,24,40,${paginationGlassMode ? 0.08 : 0.08})`;
|
||||
const paginationShellShadow = darkMode
|
||||
? `0 16px 34px rgba(0,0,0,${paginationGlassMode ? 0.10 : 0.22})`
|
||||
: `0 14px 30px rgba(15,23,42,${paginationGlassMode ? 0.03 : 0.08})`;
|
||||
const paginationChipBg = darkMode
|
||||
? `rgba(255,255,255,${paginationGlassMode ? Math.max(0.02, opacity * 0.035) : 0.04})`
|
||||
: `rgba(255,255,255,${paginationGlassMode ? Math.max(0.18, opacity * 0.26) : 0.86})`;
|
||||
const paginationChipBorderColor = darkMode
|
||||
? `rgba(255,255,255,${paginationGlassMode ? 0.10 : 0.08})`
|
||||
: `rgba(16,24,40,${paginationGlassMode ? 0.10 : 0.08})`;
|
||||
const paginationHoverBg = darkMode
|
||||
? `rgba(255,255,255,${paginationGlassMode ? Math.max(0.04, opacity * 0.06) : 0.07})`
|
||||
: `rgba(255,255,255,${paginationGlassMode ? Math.max(0.24, opacity * 0.34) : 0.96})`;
|
||||
const paginationPrimaryTextColor = darkMode ? '#f5f7ff' : '#162033';
|
||||
const paginationSecondaryTextColor = darkMode ? 'rgba(255,255,255,0.54)' : 'rgba(16,24,40,0.56)';
|
||||
const paginationAccentBg = darkMode ? 'rgba(255,214,102,0.14)' : 'rgba(24,144,255,0.10)';
|
||||
const paginationAccentBorderColor = darkMode ? 'rgba(255,214,102,0.38)' : 'rgba(24,144,255,0.22)';
|
||||
const paginationActiveItemBg = darkMode ? 'rgba(255,214,102,0.18)' : 'rgba(24,144,255,0.12)';
|
||||
const paginationActiveItemBorderColor = darkMode ? 'rgba(255,214,102,0.46)' : 'rgba(24,144,255,0.28)';
|
||||
const paginationActiveItemTextColor = darkMode ? '#fff7d6' : '#0958d9';
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [modal, contextHolder] = Modal.useModal();
|
||||
@@ -2970,6 +2998,49 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
};
|
||||
}, [viewMode, tableScrollX, mergedDisplayData.length, syncExternalScrollFromTargets, pickHorizontalScrollTargets]);
|
||||
|
||||
const paginationSummaryText = useMemo(() => {
|
||||
if (!pagination) return '';
|
||||
const total = Number.isFinite(pagination.total) ? pagination.total : 0;
|
||||
const rangeStart = Math.max(0, (pagination.current - 1) * pagination.pageSize + (total > 0 ? 1 : 0));
|
||||
const hasValidRange = total > 0 && rangeStart > 0;
|
||||
const rangeEnd = hasValidRange ? Math.min(total, rangeStart + pagination.pageSize - 1) : 0;
|
||||
const currentCount = hasValidRange ? Math.max(0, rangeEnd - rangeStart + 1) : 0;
|
||||
|
||||
if (pagination.totalKnown === false) {
|
||||
if (isDuckDBConnection) {
|
||||
if (pagination.totalCountLoading) return `当前 ${currentCount} 条 / 正在统计精确总数…`;
|
||||
if (pagination.totalApprox && Number.isFinite(total) && total > 0) return `当前 ${currentCount} 条 / 约 ${total} 条`;
|
||||
if (pagination.totalCountCancelled) return `当前 ${currentCount} 条 / 已取消统计`;
|
||||
return `当前 ${currentCount} 条 / 总数未统计`;
|
||||
}
|
||||
return `当前 ${currentCount} 条 / 正在统计总数…`;
|
||||
}
|
||||
|
||||
if (isDuckDBConnection && (!Number.isFinite(total) || total <= 0)) {
|
||||
return '当前 0 条 / 共 0 条';
|
||||
}
|
||||
|
||||
return `当前 ${currentCount} 条 / 共 ${total} 条`;
|
||||
}, [pagination, isDuckDBConnection]);
|
||||
|
||||
const paginationPageText = useMemo(() => {
|
||||
if (!pagination) return '';
|
||||
const total = Number.isFinite(pagination.total) ? pagination.total : 0;
|
||||
const canShowTotalPages = pagination.totalKnown !== false || (isDuckDBConnection && pagination.totalApprox && total > 0);
|
||||
if (!canShowTotalPages || total <= 0) return `第 ${pagination.current} 页`;
|
||||
const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pagination.pageSize)));
|
||||
return `第 ${pagination.current} / ${totalPages} 页`;
|
||||
}, [pagination, isDuckDBConnection]);
|
||||
|
||||
const handlePageSizeChange = useCallback((value: string) => {
|
||||
if (!pagination || !onPageChange) return;
|
||||
const nextSize = Number(value);
|
||||
if (!Number.isFinite(nextSize) || nextSize <= 0) return;
|
||||
const firstRowIndex = Math.max(0, (pagination.current - 1) * pagination.pageSize);
|
||||
const nextPage = Math.floor(firstRowIndex / nextSize) + 1;
|
||||
onPageChange(nextPage, nextSize);
|
||||
}, [pagination, onPageChange]);
|
||||
|
||||
return (
|
||||
<div className={`${gridId}${cellEditMode ? ' cell-edit-mode' : ''} data-grid-root`} style={{ flex: '1 1 auto', height: '100%', overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0, background: 'transparent' }}>
|
||||
{/* Toolbar + Filter Panel */}
|
||||
@@ -3697,33 +3768,41 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<div style={{ padding: '8px', borderTop: 'none', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Pagination
|
||||
current={pagination.current}
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
showTotal={(total, range) => {
|
||||
const hasValidRange = Array.isArray(range) && range[0] > 0 && range[1] >= range[0];
|
||||
const currentCount = hasValidRange ? Math.max(0, range[1] - range[0] + 1) : 0;
|
||||
if (pagination.totalKnown === false) {
|
||||
if (isDuckDBConnection) {
|
||||
if (pagination.totalCountLoading) return `当前 ${currentCount} 条 / 正在统计精确总数...`;
|
||||
if (pagination.totalApprox && Number.isFinite(total) && total > 0) return `当前 ${currentCount} 条 / 约 ${total} 条`;
|
||||
if (pagination.totalCountCancelled) return `当前 ${currentCount} 条 / 已取消统计`;
|
||||
return `当前 ${currentCount} 条 / 总数未统计`;
|
||||
<div style={{ padding: '12px 0 0', borderTop: 'none', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<div className="data-grid-pagination-shell">
|
||||
<div className="data-grid-pagination-summary" aria-live="polite">
|
||||
<span className="data-grid-pagination-kicker">结果集</span>
|
||||
<span className="data-grid-pagination-summary-value">{paginationSummaryText}</span>
|
||||
</div>
|
||||
<div className="data-grid-pagination-page-chip">{paginationPageText}</div>
|
||||
<Pagination
|
||||
current={pagination.current}
|
||||
pageSize={pagination.pageSize}
|
||||
total={pagination.total}
|
||||
showSizeChanger={false}
|
||||
onChange={onPageChange}
|
||||
showTitle={false}
|
||||
size="small"
|
||||
itemRender={(_page, type, originalElement) => {
|
||||
if (type === 'prev') {
|
||||
return <span className="data-grid-pagination-nav-icon" aria-hidden="true"><LeftOutlined /></span>;
|
||||
}
|
||||
return `当前 ${currentCount} 条 / 正在统计总数...`;
|
||||
}
|
||||
if (isDuckDBConnection && (!Number.isFinite(total) || total <= 0)) {
|
||||
return '当前 0 条 / 共 0 条';
|
||||
}
|
||||
return `当前 ${currentCount} 条 / 共 ${total} 条`;
|
||||
}}
|
||||
showSizeChanger
|
||||
pageSizeOptions={['100', '200', '500', '1000']}
|
||||
onChange={onPageChange}
|
||||
size="small"
|
||||
/>
|
||||
if (type === 'next') {
|
||||
return <span className="data-grid-pagination-nav-icon" aria-hidden="true"><RightOutlined /></span>;
|
||||
}
|
||||
return originalElement;
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
value={String(pagination.pageSize)}
|
||||
onChange={handlePageSizeChange}
|
||||
options={paginationPageSizeOptions.map((value) => ({ value, label: `${value} 条 / 页` }))}
|
||||
className="data-grid-pagination-size-select"
|
||||
aria-label="每页条数"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3899,6 +3978,266 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
.${gridId} .data-grid-external-hscroll-inner {
|
||||
height: 1px;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-shell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${paginationShellBorderColor};
|
||||
background: ${paginationShellBg};
|
||||
box-shadow: ${paginationShellShadow};
|
||||
backdrop-filter: ${opacity < 0.999 ? 'blur(14px)' : 'none'};
|
||||
-webkit-backdrop-filter: ${opacity < 0.999 ? 'blur(14px)' : 'none'};
|
||||
}
|
||||
.${gridId} .data-grid-pagination-summary,
|
||||
.${gridId} .data-grid-pagination-page-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid ${paginationChipBorderColor};
|
||||
background: ${paginationChipBg};
|
||||
color: ${paginationPrimaryTextColor};
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: ${paginationAccentBg};
|
||||
border: 1px solid ${paginationAccentBorderColor};
|
||||
color: ${paginationActiveItemTextColor};
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-summary-value {
|
||||
color: ${paginationPrimaryTextColor};
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-page-chip {
|
||||
color: ${paginationSecondaryTextColor};
|
||||
font-weight: 600;
|
||||
}
|
||||
.${gridId} .ant-pagination {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
color: ${paginationPrimaryTextColor};
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-item,
|
||||
.${gridId} .ant-pagination .ant-pagination-prev,
|
||||
.${gridId} .ant-pagination .ant-pagination-next,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next {
|
||||
min-width: 34px;
|
||||
height: 34px;
|
||||
margin-inline-end: 0;
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${paginationChipBorderColor};
|
||||
background: ${paginationChipBg};
|
||||
box-shadow: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-item a,
|
||||
.${gridId} .ant-pagination .ant-pagination-prev .ant-pagination-item-link,
|
||||
.${gridId} .ant-pagination .ant-pagination-next .ant-pagination-item-link,
|
||||
.${gridId} .ant-pagination .ant-pagination-prev > *,
|
||||
.${gridId} .ant-pagination .ant-pagination-next > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: ${paginationPrimaryTextColor};
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-item:hover,
|
||||
.${gridId} .ant-pagination .ant-pagination-prev:hover,
|
||||
.${gridId} .ant-pagination .ant-pagination-next:hover {
|
||||
background: ${paginationHoverBg};
|
||||
border-color: ${paginationActiveItemBorderColor};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-item-active {
|
||||
border-color: ${paginationActiveItemBorderColor};
|
||||
background: ${paginationActiveItemBg};
|
||||
box-shadow: inset 0 0 0 1px ${paginationAccentBorderColor};
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-item-active a {
|
||||
color: ${paginationActiveItemTextColor};
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-disabled,
|
||||
.${gridId} .ant-pagination .ant-pagination-disabled:hover {
|
||||
background: transparent;
|
||||
border-color: ${paginationChipBorderColor};
|
||||
transform: none;
|
||||
opacity: 0.42;
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next {
|
||||
padding: 0;
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-container,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
line-height: 1;
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-ellipsis,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-ellipsis,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
inset: 0 !important;
|
||||
width: fit-content !important;
|
||||
height: fit-content !important;
|
||||
min-width: 0 !important;
|
||||
min-height: 0 !important;
|
||||
margin: auto !important;
|
||||
padding: 0 !important;
|
||||
transform: none !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
line-height: 1 !important;
|
||||
color: ${paginationSecondaryTextColor};
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-ellipsis,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-ellipsis {
|
||||
letter-spacing: 0.18em;
|
||||
text-indent: 0.18em;
|
||||
text-align: center;
|
||||
}
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon .anticon,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon .anticon,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-prev .ant-pagination-item-link-icon svg,
|
||||
.${gridId} .ant-pagination .ant-pagination-jump-next .ant-pagination-item-link-icon svg {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-nav-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-nav-icon .anticon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select {
|
||||
min-width: 112px;
|
||||
height: 34px;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select.ant-select-single,
|
||||
.${gridId} .data-grid-pagination-size-select.ant-select-single.ant-select-sm {
|
||||
height: 34px;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selector {
|
||||
height: 34px !important;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid ${paginationChipBorderColor} !important;
|
||||
background: ${paginationChipBg} !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 12px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selection-wrap {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
height: 100%;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selection-search,
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selection-search-input {
|
||||
height: 100% !important;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selection-item,
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selection-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
line-height: 34px !important;
|
||||
color: ${paginationPrimaryTextColor};
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-selection-search {
|
||||
inset-inline-start: 12px !important;
|
||||
inset-inline-end: 32px !important;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-arrow {
|
||||
color: ${paginationSecondaryTextColor};
|
||||
inset-inline-end: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-top: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
.${gridId} .data-grid-pagination-size-select .ant-select-arrow .anticon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Ghost Resize Line for Columns */}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Alert, Button, Collapse, Input, Modal, Progress, Select, Space, Switch,
|
||||
import { DeleteOutlined, DownloadOutlined, FileSearchOutlined, FolderOpenOutlined, InfoCircleFilled, ReloadOutlined } from '@ant-design/icons';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { useStore } from '../store';
|
||||
import { normalizeOpacityForPlatform } from '../utils/appearance';
|
||||
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
import {
|
||||
CheckDriverNetworkStatus,
|
||||
DownloadDriverPackage,
|
||||
@@ -166,7 +166,8 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
const theme = useStore((state) => state.theme);
|
||||
const appearance = useStore((state) => state.appearance);
|
||||
const darkMode = theme === 'dark';
|
||||
const opacity = normalizeOpacityForPlatform(appearance.opacity);
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const modalContentRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const tableScrollTargetsRef = useRef<HTMLElement[]>([]);
|
||||
@@ -1223,7 +1224,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
paddingRight: 18,
|
||||
},
|
||||
}}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
footer={(
|
||||
<div className="driver-manager-footer">
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Table, Tag, Button, Tooltip } from 'antd';
|
||||
import { ClearOutlined, CloseOutlined, CaretRightOutlined, BugOutlined } from '@ant-design/icons';
|
||||
import { Table, Tag, Button, Tooltip, Empty } from 'antd';
|
||||
import { ClearOutlined, CloseOutlined, BugOutlined, ClockCircleOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { normalizeOpacityForPlatform } from '../utils/appearance';
|
||||
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
|
||||
interface LogPanelProps {
|
||||
height: number;
|
||||
@@ -16,7 +16,8 @@ const LogPanel: React.FC<LogPanelProps> = ({ height, onClose, onResizeStart }) =
|
||||
const theme = useStore(state => state.theme);
|
||||
const appearance = useStore(state => state.appearance);
|
||||
const darkMode = theme === 'dark';
|
||||
const opacity = normalizeOpacityForPlatform(appearance.opacity);
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
|
||||
// Background Helper
|
||||
const getBg = (darkHex: string) => {
|
||||
@@ -28,10 +29,25 @@ const LogPanel: React.FC<LogPanelProps> = ({ height, onClose, onResizeStart }) =
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
};
|
||||
const bgMain = getBg('#1d1d1d');
|
||||
const panelDividerColor = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
|
||||
const shellOpacity = darkMode ? Math.max(0.18, opacity * 0.82) : Math.max(0.28, opacity * 0.92);
|
||||
const shellOpacityStrong = darkMode ? Math.max(0.22, opacity * 0.9) : Math.max(0.34, opacity * 0.96);
|
||||
const panelDividerColor = darkMode
|
||||
? `rgba(255,255,255,${Math.max(0.04, opacity * 0.10)})`
|
||||
: `rgba(0,0,0,${Math.max(0.04, opacity * 0.08)})`;
|
||||
const panelMutedTextColor = darkMode ? 'rgba(255,255,255,0.62)' : 'rgba(0,0,0,0.58)';
|
||||
const logScrollbarThumb = darkMode ? 'rgba(255, 255, 255, 0.34)' : 'rgba(0, 0, 0, 0.26)';
|
||||
const logScrollbarThumbHover = darkMode ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.36)';
|
||||
const panelShellBg = darkMode
|
||||
? `linear-gradient(180deg, rgba(15,20,30,${shellOpacity}) 0%, rgba(9,13,22,${shellOpacityStrong}) 100%)`
|
||||
: `linear-gradient(180deg, rgba(255,255,255,${shellOpacityStrong}) 0%, rgba(246,248,252,${shellOpacity}) 100%)`;
|
||||
const panelAccentColor = darkMode ? '#ffd666' : '#1677ff';
|
||||
const panelShadow = darkMode
|
||||
? `0 12px 28px rgba(0,0,0,${Math.max(0.05, opacity * 0.18)})`
|
||||
: `0 12px 24px rgba(15,23,42,${Math.max(0.02, opacity * 0.08)})`;
|
||||
const logScrollbarThumb = darkMode
|
||||
? `rgba(255, 255, 255, ${Math.max(0.18, opacity * 0.34)})`
|
||||
: `rgba(0, 0, 0, ${Math.max(0.12, opacity * 0.26)})`;
|
||||
const logScrollbarThumbHover = darkMode
|
||||
? `rgba(255, 255, 255, ${Math.max(0.28, opacity * 0.48)})`
|
||||
: `rgba(0, 0, 0, ${Math.max(0.18, opacity * 0.36)})`;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -45,7 +61,7 @@ const LogPanel: React.FC<LogPanelProps> = ({ height, onClose, onResizeStart }) =
|
||||
dataIndex: 'status',
|
||||
width: 70,
|
||||
render: (status: string) => (
|
||||
<Tag color={status === 'success' ? 'success' : 'error'} style={{ marginRight: 0 }}>
|
||||
<Tag color={status === 'success' ? 'success' : 'error'} style={{ marginRight: 0, borderRadius: 999, paddingInline: 8, fontSize: 11, fontWeight: 700 }}>
|
||||
{status === 'success' ? 'OK' : 'ERR'}
|
||||
</Tag>
|
||||
)
|
||||
@@ -60,7 +76,7 @@ const LogPanel: React.FC<LogPanelProps> = ({ height, onClose, onResizeStart }) =
|
||||
title: 'SQL / Message',
|
||||
dataIndex: 'sql',
|
||||
render: (text: string, record: any) => (
|
||||
<div style={{ fontFamily: 'monospace', wordBreak: 'break-all', fontSize: '12px', lineHeight: '1.2' }}>
|
||||
<div style={{ fontFamily: 'monospace', wordBreak: 'break-all', fontSize: '12px', lineHeight: '1.45' }}>
|
||||
<div style={{ color: darkMode ? '#a6e22e' : '#005cc5' }}>{text}</div>
|
||||
{record.message && <div style={{ color: '#ff4d4f', marginTop: 2 }}>{record.message}</div>}
|
||||
{record.affectedRows !== undefined && <div style={{ color: panelMutedTextColor, marginTop: 1 }}>Affected: {record.affectedRows}</div>}
|
||||
@@ -72,12 +88,18 @@ const LogPanel: React.FC<LogPanelProps> = ({ height, onClose, onResizeStart }) =
|
||||
return (
|
||||
<div style={{
|
||||
height,
|
||||
borderTop: `1px solid ${panelDividerColor}`,
|
||||
background: bgMain,
|
||||
margin: 0,
|
||||
border: `1px solid ${panelDividerColor}`,
|
||||
borderRadius: 14,
|
||||
background: panelShellBg,
|
||||
WebkitBackdropFilter: opacity < 0.999 ? 'blur(14px)' : 'none',
|
||||
boxShadow: panelShadow,
|
||||
backdropFilter: darkMode && opacity < 0.999 ? 'blur(18px)' : 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
zIndex: 100 // Ensure above other content
|
||||
overflow: 'hidden',
|
||||
zIndex: 100
|
||||
}}>
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
@@ -95,38 +117,53 @@ const LogPanel: React.FC<LogPanelProps> = ({ height, onClose, onResizeStart }) =
|
||||
|
||||
{/* Toolbar */}
|
||||
<div style={{
|
||||
padding: '4px 8px',
|
||||
padding: '10px 14px',
|
||||
borderBottom: `1px solid ${panelDividerColor}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: 32
|
||||
gap: 12,
|
||||
minHeight: 48
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontWeight: 'bold', fontSize: '12px' }}>
|
||||
<BugOutlined /> SQL 执行日志
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 10, display: 'grid', placeItems: 'center', background: darkMode ? `rgba(255,214,102,${Math.max(0.10, Math.min(0.18, opacity * 0.18))})` : `rgba(24,144,255,${Math.max(0.08, Math.min(0.16, opacity * 0.16))})`, color: panelAccentColor, flexShrink: 0 }}>
|
||||
<BugOutlined />
|
||||
</div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13, color: darkMode ? '#f5f7ff' : '#162033' }}>SQL 执行日志</div>
|
||||
<div style={{ fontSize: 12, color: panelMutedTextColor }}>记录执行状态、耗时与错误信息,便于快速回溯。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Tooltip title="清空日志">
|
||||
<Button type="text" size="small" icon={<ClearOutlined />} onClick={clearSqlLogs} />
|
||||
<Button type="text" size="small" icon={<ClearOutlined />} onClick={clearSqlLogs} style={{ color: panelMutedTextColor }} />
|
||||
</Tooltip>
|
||||
<Tooltip title="关闭面板">
|
||||
<Button type="text" size="small" icon={<CloseOutlined />} onClick={onClose} />
|
||||
<Button type="text" size="small" icon={<CloseOutlined />} onClick={onClose} style={{ color: panelMutedTextColor }} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="log-panel-scroll" style={{ flex: 1, overflow: 'auto' }}>
|
||||
<Table
|
||||
className="log-panel-table"
|
||||
dataSource={sqlLogs}
|
||||
columns={columns}
|
||||
size="small"
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
showHeader={false}
|
||||
// scroll={{ y: height - 32 }} // Let flex handle it
|
||||
/>
|
||||
<div className="log-panel-scroll" style={{ flex: 1, overflow: 'auto', padding: '8px 10px 10px' }}>
|
||||
{sqlLogs.length === 0 ? (
|
||||
<div style={{ height: '100%', minHeight: 160, display: 'grid', placeItems: 'center' }}>
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={<span style={{ color: panelMutedTextColor }}>暂无 SQL 执行日志</span>}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Table
|
||||
className="log-panel-table"
|
||||
dataSource={sqlLogs}
|
||||
columns={columns}
|
||||
size="small"
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
showHeader={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<style>{`
|
||||
.log-panel-scroll {
|
||||
@@ -156,6 +193,16 @@ const LogPanel: React.FC<LogPanelProps> = ({ height, onClose, onResizeStart }) =
|
||||
.log-panel-table .ant-table-tbody > tr > td {
|
||||
background: transparent !important;
|
||||
}
|
||||
.log-panel-table .ant-table-tbody > tr > td {
|
||||
padding: 8px 10px !important;
|
||||
border-bottom: 1px solid ${panelDividerColor} !important;
|
||||
}
|
||||
.log-panel-table .ant-table-tbody > tr:last-child > td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.log-panel-table .ant-table-row:hover > td {
|
||||
background: ${darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(16,24,40,0.03)'} !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useStore } from '../store';
|
||||
import { RedisKeyInfo, RedisValue, StreamEntry } from '../types';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { normalizeOpacityForPlatform } from '../utils/appearance';
|
||||
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
@@ -399,7 +399,8 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
|
||||
const theme = useStore(state => state.theme);
|
||||
const appearance = useStore(state => state.appearance);
|
||||
const darkMode = theme === 'dark';
|
||||
const opacity = normalizeOpacityForPlatform(appearance.opacity);
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const connection = connections.find(c => c.id === connectionId);
|
||||
const keyAccentColor = darkMode ? '#ffd666' : '#1677ff';
|
||||
const jsonAccentColor = darkMode ? '#f6c453' : '#1890ff';
|
||||
|
||||
@@ -27,12 +27,15 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge,
|
||||
DisconnectOutlined,
|
||||
CloudOutlined,
|
||||
CheckSquareOutlined,
|
||||
CodeOutlined
|
||||
CodeOutlined,
|
||||
TagOutlined,
|
||||
CheckOutlined,
|
||||
FilterOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { SavedConnection } from '../types';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView } from '../../wailsjs/go/app/App';
|
||||
import { normalizeOpacityForPlatform } from '../utils/appearance';
|
||||
import { normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
@@ -73,6 +76,15 @@ const SEARCH_SCOPE_LABEL_MAP: Record<SearchScope, string> = SEARCH_SCOPE_OPTIONS
|
||||
return acc;
|
||||
}, {} as Record<SearchScope, string>);
|
||||
|
||||
|
||||
const SEARCH_SCOPE_ICON_MAP: Record<SearchScope, React.ReactNode> = {
|
||||
smart: <ThunderboltOutlined />,
|
||||
object: <TableOutlined />,
|
||||
database: <DatabaseOutlined />,
|
||||
host: <CloudOutlined />,
|
||||
tag: <TagOutlined />,
|
||||
};
|
||||
|
||||
const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> = ({ onEditConnection }) => {
|
||||
const connections = useStore(state => state.connections);
|
||||
const savedQueries = useStore(state => state.savedQueries);
|
||||
@@ -95,7 +107,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const recordTableAccess = useStore(state => state.recordTableAccess);
|
||||
const setTableSortPreference = useStore(state => state.setTableSortPreference);
|
||||
const darkMode = theme === 'dark';
|
||||
const opacity = normalizeOpacityForPlatform(appearance.opacity);
|
||||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||||
const [treeData, setTreeData] = useState<TreeNode[]>([]);
|
||||
|
||||
// Background Helper (Duplicate logic for now, ideally shared)
|
||||
@@ -108,6 +121,44 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
};
|
||||
const bgMain = getBg('#141414');
|
||||
const modalPanelStyle = useMemo(() => ({
|
||||
background: darkMode
|
||||
? 'linear-gradient(180deg, rgba(20,26,38,0.96) 0%, rgba(13,17,26,0.98) 100%)'
|
||||
: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(246,248,252,0.98) 100%)',
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(16,24,40,0.08)',
|
||||
boxShadow: darkMode ? '0 20px 48px rgba(0,0,0,0.38)' : '0 18px 42px rgba(15,23,42,0.12)',
|
||||
backdropFilter: darkMode ? 'blur(18px)' : 'none',
|
||||
}), [darkMode]);
|
||||
const modalSectionStyle = useMemo(() => ({
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(16,24,40,0.08)',
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.84)',
|
||||
}), [darkMode]);
|
||||
const modalScrollSectionStyle = useMemo(() => ({
|
||||
maxHeight: 400,
|
||||
overflow: 'auto' as const,
|
||||
border: darkMode ? '1px solid rgba(255,255,255,0.08)' : '1px solid rgba(16,24,40,0.08)',
|
||||
borderRadius: 14,
|
||||
padding: 12,
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.8)',
|
||||
}), [darkMode]);
|
||||
const modalHintTextStyle = useMemo(() => ({
|
||||
color: darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
}), [darkMode]);
|
||||
const renderSidebarModalTitle = (icon: React.ReactNode, title: string, description: string) => (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||||
<div style={{ width: 34, height: 34, borderRadius: 12, display: 'grid', placeItems: 'center', background: darkMode ? 'rgba(255,214,102,0.12)' : 'rgba(24,144,255,0.1)', color: darkMode ? '#ffd666' : '#1677ff', flexShrink: 0 }}>
|
||||
{icon}
|
||||
</div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 16, fontWeight: 700, color: darkMode ? '#f5f7ff' : '#162033' }}>{title}</div>
|
||||
<div style={{ marginTop: 4, color: darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)', fontSize: 12, lineHeight: 1.6 }}>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [searchScopes, setSearchScopes] = useState<SearchScope[]>(['smart']);
|
||||
const [isSearchScopePopoverOpen, setIsSearchScopePopoverOpen] = useState(false);
|
||||
@@ -2471,32 +2522,100 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const searchScopePopoverContent = useMemo(() => {
|
||||
const smartSelected = searchScopes.includes('smart');
|
||||
const scopedOptions = SEARCH_SCOPE_OPTIONS.filter((option) => option.value !== 'smart');
|
||||
const borderColor = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(16,24,40,0.08)';
|
||||
const mutedTextColor = darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)';
|
||||
const titleColor = darkMode ? 'rgba(255,255,255,0.92)' : '#162033';
|
||||
const panelBg = darkMode
|
||||
? 'linear-gradient(180deg, rgba(17,24,39,0.96) 0%, rgba(10,15,26,0.98) 100%)'
|
||||
: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(246,248,252,0.98) 100%)';
|
||||
const smartBg = smartSelected
|
||||
? (darkMode ? 'linear-gradient(135deg, rgba(255,214,102,0.22) 0%, rgba(255,179,71,0.16) 100%)' : 'linear-gradient(135deg, rgba(255,214,102,0.26) 0%, rgba(255,244,204,0.92) 100%)')
|
||||
: (darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.72)');
|
||||
const smartBorder = smartSelected
|
||||
? (darkMode ? 'rgba(255,214,102,0.42)' : 'rgba(245,176,65,0.34)')
|
||||
: borderColor;
|
||||
const getOptionCardStyle = (checked: boolean) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center' as const,
|
||||
justifyContent: 'space-between' as const,
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${checked ? (darkMode ? 'rgba(118,169,250,0.44)' : 'rgba(24,144,255,0.32)') : borderColor}`,
|
||||
background: checked
|
||||
? (darkMode ? 'rgba(64,124,255,0.18)' : 'rgba(24,144,255,0.08)')
|
||||
: (darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.76)'),
|
||||
transition: 'all 120ms ease',
|
||||
});
|
||||
return (
|
||||
<div style={{ minWidth: 220, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ fontSize: 12, color: '#8c8c8c' }}>搜索范围</div>
|
||||
<Checkbox
|
||||
checked={smartSelected}
|
||||
onChange={(e) => setSearchScopeChecked('smart', e.target.checked)}
|
||||
>
|
||||
智能(推荐)
|
||||
</Checkbox>
|
||||
<div style={{ paddingLeft: 12, display: 'grid', gap: 6 }}>
|
||||
{scopedOptions.map((option) => (
|
||||
<Checkbox
|
||||
key={option.value}
|
||||
checked={searchScopes.includes(option.value)}
|
||||
onChange={(e) => setSearchScopeChecked(option.value, e.target.checked)}
|
||||
>
|
||||
{option.label}
|
||||
</Checkbox>
|
||||
))}
|
||||
<div style={{ minWidth: 280, display: 'flex', flexDirection: 'column', background: panelBg, padding: 14, gap: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, letterSpacing: 0.4, color: mutedTextColor, textTransform: 'uppercase' }}>搜索范围</div>
|
||||
<div style={{ marginTop: 4, fontSize: 13, lineHeight: 1.5, color: mutedTextColor }}>“智能”自动匹配最可能的命中项;手动模式支持按维度组合筛选。</div>
|
||||
</div>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 10, display: 'grid', placeItems: 'center', background: darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(17,24,39,0.06)', color: darkMode ? '#ffd666' : '#1677ff', flexShrink: 0 }}>
|
||||
<FilterOutlined />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#8c8c8c' }}>
|
||||
智能与其他项互斥;其他项支持多选。
|
||||
|
||||
<label style={{ display: 'block', cursor: 'pointer' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', borderRadius: 14, border: `1px solid ${smartBorder}`, background: smartBg, boxShadow: smartSelected ? (darkMode ? '0 10px 24px rgba(0,0,0,0.24)' : '0 10px 24px rgba(245,176,65,0.14)') : 'none' }}>
|
||||
<Checkbox
|
||||
checked={smartSelected}
|
||||
onChange={(e) => setSearchScopeChecked('smart', e.target.checked)}
|
||||
/>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 10, display: 'grid', placeItems: 'center', background: darkMode ? 'rgba(255,214,102,0.16)' : 'rgba(255,214,102,0.3)', color: darkMode ? '#ffd666' : '#ad6800', flexShrink: 0 }}>
|
||||
{SEARCH_SCOPE_ICON_MAP.smart}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: titleColor }}>智能</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 999, fontSize: 11, fontWeight: 700, color: darkMode ? '#ffe58f' : '#ad6800', background: darkMode ? 'rgba(255,214,102,0.16)' : 'rgba(255,214,102,0.35)' }}>推荐</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 3, fontSize: 12, lineHeight: 1.5, color: mutedTextColor }}>适合日常检索,自动覆盖名称、库、Host 和标签等高频维度。</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div style={{ height: 1, background: borderColor, opacity: 0.9 }} />
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, letterSpacing: 0.3, color: mutedTextColor, textTransform: 'uppercase' }}>手动范围</div>
|
||||
<div style={{ fontSize: 12, color: mutedTextColor }}>支持多选组合</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{scopedOptions.map((option) => {
|
||||
const checked = searchScopes.includes(option.value);
|
||||
return (
|
||||
<label key={option.value} style={{ display: 'block', cursor: 'pointer' }}>
|
||||
<div style={getOptionCardStyle(checked)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, minWidth: 0 }}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(e) => setSearchScopeChecked(option.value, e.target.checked)}
|
||||
/>
|
||||
<div style={{ width: 28, height: 28, borderRadius: 9, display: 'grid', placeItems: 'center', background: checked ? (darkMode ? 'rgba(118,169,250,0.2)' : 'rgba(24,144,255,0.12)') : (darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(17,24,39,0.06)'), color: checked ? (darkMode ? '#91caff' : '#1677ff') : mutedTextColor, flexShrink: 0 }}>
|
||||
{SEARCH_SCOPE_ICON_MAP[option.value]}
|
||||
</div>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: titleColor, whiteSpace: 'nowrap' }}>{option.label}</span>
|
||||
</div>
|
||||
<div style={{ width: 18, display: 'flex', justifyContent: 'center', color: checked ? (darkMode ? '#91caff' : '#1677ff') : 'transparent', flexShrink: 0 }}>
|
||||
<CheckOutlined />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '10px 12px', borderRadius: 12, background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(17,24,39,0.04)', color: mutedTextColor, fontSize: 12, lineHeight: 1.6 }}>
|
||||
智能与其他项互斥。若你明确知道要搜的是对象、库、Host 或标签,建议切到手动范围以减少噪音结果。
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [searchScopes]);
|
||||
}, [darkMode, searchScopes]);
|
||||
|
||||
const parseHostOnlyToken = (value: unknown): string[] => {
|
||||
const raw = String(value || '').trim();
|
||||
@@ -3301,14 +3420,14 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{ padding: '4px 8px' }}>
|
||||
<Space.Compact block size="small">
|
||||
<div style={{ padding: '4px 10px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Search
|
||||
ref={searchInputRef}
|
||||
placeholder="搜索..."
|
||||
onChange={onSearch}
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
/>
|
||||
<Popover
|
||||
content={searchScopePopoverContent}
|
||||
@@ -3316,18 +3435,66 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
placement="bottomRight"
|
||||
open={isSearchScopePopoverOpen}
|
||||
onOpenChange={setIsSearchScopePopoverOpen}
|
||||
styles={{ body: { padding: 0, borderRadius: 18, overflow: 'hidden' } }}
|
||||
>
|
||||
<Tooltip title={`搜索范围:${searchScopeSummary}`}>
|
||||
<Button size="small" icon={<DownOutlined />} style={{ width: 86 }}>
|
||||
范围{searchScopes.includes('smart') ? '(智)' : `(${searchScopes.length})`}
|
||||
<Button
|
||||
size="small"
|
||||
style={{
|
||||
minWidth: 86,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
paddingInline: 10,
|
||||
borderRadius: 10,
|
||||
borderColor: darkMode ? 'rgba(255,255,255,0.12)' : 'rgba(16,24,40,0.12)',
|
||||
background: darkMode ? bgMain : 'rgba(255,255,255,0.92)',
|
||||
color: darkMode ? 'rgba(255,255,255,0.88)' : '#162033',
|
||||
boxShadow: isSearchScopePopoverOpen
|
||||
? (darkMode ? '0 0 0 1px rgba(255,214,102,0.22) inset' : '0 0 0 1px rgba(24,144,255,0.24) inset')
|
||||
: 'none',
|
||||
backdropFilter: darkMode ? 'blur(10px)' : 'none',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', color: searchScopes.includes('smart') ? '#ffd666' : (darkMode ? 'rgba(255,255,255,0.72)' : 'rgba(22,32,51,0.72)') }}>
|
||||
<FilterOutlined />
|
||||
</span>
|
||||
<span style={{ fontWeight: 700, color: darkMode ? 'rgba(255,255,255,0.88)' : '#162033' }}>筛选</span>
|
||||
<span
|
||||
style={{
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
padding: '0 5px',
|
||||
borderRadius: 999,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
background: searchScopes.includes('smart')
|
||||
? (darkMode ? 'rgba(255,214,102,0.16)' : 'rgba(24,144,255,0.12)')
|
||||
: (darkMode ? 'rgba(118,169,250,0.18)' : 'rgba(24,144,255,0.12)'),
|
||||
color: searchScopes.includes('smart')
|
||||
? (darkMode ? '#ffd666' : '#1677ff')
|
||||
: (darkMode ? '#91caff' : '#1677ff'),
|
||||
}}
|
||||
>
|
||||
{searchScopes.includes('smart') ? '智' : searchScopes.length}
|
||||
</span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', color: darkMode ? 'rgba(255,255,255,0.48)' : 'rgba(22,32,51,0.4)', fontSize: 12 }}>
|
||||
<DownOutlined />
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div style={{ padding: '4px 8px', borderBottom: 'none', display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
<div style={{ padding: '4px 10px', borderBottom: 'none', display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FolderOpenOutlined />}
|
||||
@@ -3395,8 +3562,14 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={renameViewTarget?.type === 'tag' ? "编辑标签" : "新建组"}
|
||||
title={renderSidebarModalTitle(
|
||||
<FolderOpenOutlined />,
|
||||
renameViewTarget?.type === 'tag' ? "编辑标签" : "新建组",
|
||||
renameViewTarget?.type === 'tag' ? "调整分组名称和包含的连接。" : "为连接树创建一个更清晰的分组视图。"
|
||||
)}
|
||||
open={isCreateTagModalOpen}
|
||||
centered
|
||||
styles={{ content: modalPanelStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 10 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 12 } }}
|
||||
onOk={() => {
|
||||
createTagForm.validateFields().then(values => {
|
||||
if (renameViewTarget?.type === 'tag') {
|
||||
@@ -3431,20 +3604,24 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
onCancel={() => setIsCreateTagModalOpen(false)}
|
||||
>
|
||||
<Form form={createTagForm} layout="vertical">
|
||||
<Form.Item name="name" label="标签名称" rules={[{ required: true, message: '请输入标签名称' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="connectionIds" label="选择连接">
|
||||
<Checkbox.Group style={{ width: '100%' }}>
|
||||
<Space direction="vertical" style={{ width: '100%', maxHeight: '400px', overflowY: 'auto' }}>
|
||||
{connections.map(conn => (
|
||||
<Checkbox key={conn.id} value={conn.id}>
|
||||
{conn.name} {conn.config.host ? `(${conn.config.host})` : ''}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
<div style={modalSectionStyle}>
|
||||
<Form.Item name="name" label="标签名称" rules={[{ required: true, message: '请输入标签名称' }]}>
|
||||
<Input placeholder="例如:线上环境 / 核心业务 / 临时调试" />
|
||||
</Form.Item>
|
||||
<Form.Item name="connectionIds" label="选择连接" style={{ marginBottom: 0 }}>
|
||||
<Checkbox.Group style={{ width: '100%' }}>
|
||||
<div style={modalScrollSectionStyle}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{connections.map(conn => (
|
||||
<Checkbox key={conn.id} value={conn.id}>
|
||||
{conn.name} {conn.config.host ? `(${conn.config.host})` : ''}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -3514,10 +3691,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="批量操作表"
|
||||
title={renderSidebarModalTitle(<TableOutlined />, "批量操作表", "按对象批量导出结构、数据或完整备份。")}
|
||||
open={isBatchModalOpen}
|
||||
onCancel={() => setIsBatchModalOpen(false)}
|
||||
width={680}
|
||||
width={720}
|
||||
centered
|
||||
styles={{ content: modalPanelStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 10 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 12 } }}
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<Button key="cancel" onClick={() => setIsBatchModalOpen(false)}>
|
||||
@@ -3553,7 +3732,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ ...modalSectionStyle, marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>选择连接:</label>
|
||||
<Select
|
||||
@@ -3585,10 +3764,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div style={modalHintTextStyle}>先选择连接与数据库,再决定导出范围和目标对象。</div>
|
||||
</div>
|
||||
|
||||
{batchTables.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ ...modalSectionStyle, marginBottom: 16 }}>
|
||||
<Space wrap size={8} style={{ width: '100%' }}>
|
||||
<Input
|
||||
allowClear
|
||||
@@ -3626,7 +3806,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
{batchTables.length > 0 && (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ ...modalSectionStyle, marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -3654,7 +3834,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ maxHeight: 400, overflow: 'auto', border: darkMode ? '1px solid #303030' : '1px solid #f0f0f0', borderRadius: 4, padding: 8 }}>
|
||||
<div style={modalScrollSectionStyle}>
|
||||
<Checkbox.Group
|
||||
value={checkedTableKeys}
|
||||
onChange={(values) => setCheckedTableKeys(values as string[])}
|
||||
@@ -3704,10 +3884,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="批量操作库"
|
||||
title={renderSidebarModalTitle(<DatabaseOutlined />, "批量操作库", "按数据库批量导出结构,或生成结构加数据的备份。")}
|
||||
open={isBatchDbModalOpen}
|
||||
onCancel={() => setIsBatchDbModalOpen(false)}
|
||||
width={600}
|
||||
width={640}
|
||||
centered
|
||||
styles={{ content: modalPanelStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 10 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 12 } }}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setIsBatchDbModalOpen(false)}>
|
||||
取消
|
||||
@@ -3731,8 +3913,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>选择连接:</label>
|
||||
<div style={{ ...modalSectionStyle, marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', marginBottom: 4, fontWeight: 600, color: darkMode ? '#f5f7ff' : '#162033' }}>选择连接:</label>
|
||||
<Select
|
||||
value={selectedDbConnection}
|
||||
onChange={handleDbConnectionChange}
|
||||
@@ -3745,11 +3927,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={{ ...modalHintTextStyle, marginTop: 10 }}>连接选定后会加载当前连接下可批量导出的数据库列表。</div>
|
||||
</div>
|
||||
|
||||
{batchDatabases.length > 0 && (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ ...modalSectionStyle, marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button
|
||||
size="small"
|
||||
@@ -3774,7 +3957,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
<div style={{ maxHeight: 400, overflow: 'auto', border: darkMode ? '1px solid #303030' : '1px solid #f0f0f0', borderRadius: 4, padding: 8 }}>
|
||||
<div style={modalScrollSectionStyle}>
|
||||
<Checkbox.Group
|
||||
value={checkedDbKeys}
|
||||
onChange={(values) => setCheckedDbKeys(values as string[])}
|
||||
|
||||
@@ -2491,7 +2491,7 @@ END;`;
|
||||
okText="应用"
|
||||
cancelText="取消"
|
||||
width={640}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
>
|
||||
<Input.TextArea
|
||||
value={commentEditorValue}
|
||||
|
||||
Reference in New Issue
Block a user