♻️ refactor(frontend-sync): 优化桌面交互细节并移除 main 回灌 dev 自动化

- 优化新建连接、主题设置、侧边栏工具区与 SQL 日志的界面表现
- 调整分页、筛选、透明模式与弹窗样式,统一整体交互层次
- 收口外观参数生效逻辑并补齐多组件适配
- 删除 sync-main-to-dev 工作流并同步维护者手动回灌说明
This commit is contained in:
Syngnat
2026-03-07 17:01:49 +08:00
parent 1c050aefd0
commit ed1f40e04a
14 changed files with 2003 additions and 1160 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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 */}

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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';

View File

@@ -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[])}

View File

@@ -2491,7 +2491,7 @@ END;`;
okText="应用"
cancelText="取消"
width={640}
destroyOnClose
destroyOnHidden
>
<Input.TextArea
value={commentEditorValue}