feat(sidebar): 新增侧栏表自定义排序功能

- 支持按名称排序(字母顺序,默认)
- 支持按使用频率排序(打开次数降序)
- 右键表分组节点选择排序方式
- 排序偏好和访问统计持久化保存
- 每个数据库可独立设置排序方式
This commit is contained in:
Syngnat
2026-02-09 15:53:35 +08:00
parent c8575c315b
commit 012c99be9e
2 changed files with 112 additions and 13 deletions

View File

@@ -54,6 +54,10 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const removeConnection = useStore(state => state.removeConnection);
const theme = useStore(state => state.theme);
const appearance = useStore(state => state.appearance);
const tableAccessCount = useStore(state => state.tableAccessCount);
const tableSortPreference = useStore(state => state.tableSortPreference);
const recordTableAccess = useStore(state => state.recordTableAccess);
const setTableSortPreference = useStore(state => state.setTableSortPreference);
const darkMode = theme === 'dark';
const opacity = normalizeOpacityForPlatform(appearance.opacity);
const [treeData, setTreeData] = useState<TreeNode[]>([]);
@@ -490,8 +494,28 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
loadDatabaseTriggers(conn, conn.dbName),
]);
// Sort tables by display name (case-insensitive)
tables.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
// 获取当前数据库的排序偏好
const sortPreferenceKey = `${conn.id}-${conn.dbName}`;
const sortBy = tableSortPreference[sortPreferenceKey] || 'name';
// 根据排序偏好排序表
if (sortBy === 'frequency') {
// 按使用频率排序(降序)
tables.sort((a, b) => {
const keyA = `${conn.id}-${conn.dbName}-${a.dataRef.tableName}`;
const keyB = `${conn.id}-${conn.dbName}-${b.dataRef.tableName}`;
const countA = tableAccessCount[keyA] || 0;
const countB = tableAccessCount[keyB] || 0;
if (countA !== countB) {
return countB - countA; // 降序
}
// 频率相同时按名称排序
return a.title.toLowerCase().localeCompare(b.title.toLowerCase());
});
} else {
// 按名称排序(字母顺序)
tables.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
}
// Sort views by name (case-insensitive)
views.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
@@ -662,6 +686,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const onDoubleClick = (e: any, node: any) => {
if (node.type === 'table') {
const { tableName, dbName, id } = node.dataRef;
// 记录表访问
recordTableAccess(id, dbName, tableName);
addTab({
id: node.key,
title: tableName,
@@ -718,10 +744,10 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const key = node.key;
const isExpanded = expandedKeys.includes(key);
const newExpandedKeys = isExpanded
? expandedKeys.filter(k => k !== key)
const newExpandedKeys = isExpanded
? expandedKeys.filter(k => k !== key)
: [...expandedKeys, key];
setExpandedKeys(newExpandedKeys);
if (!isExpanded) setAutoExpandParent(false);
};
@@ -1321,6 +1347,42 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const conn = node.dataRef as SavedConnection;
const isRedis = conn?.config?.type === 'redis';
// 表分组节点的右键菜单
if (node.type === 'object-group' && node.dataRef?.groupKey === 'tables') {
const groupData = node.dataRef; // { ...conn, dbName, groupKey }
const sortPreferenceKey = `${groupData.id}-${groupData.dbName}`;
const currentSort = tableSortPreference[sortPreferenceKey] || 'name';
return [
{
key: 'sort-by-name',
label: '按名称排序',
icon: currentSort === 'name' ? <CheckSquareOutlined /> : null,
onClick: () => {
setTableSortPreference(groupData.id, groupData.dbName, 'name');
const dbNode = {
key: `${groupData.id}-${groupData.dbName}`,
dataRef: groupData
};
loadTables(dbNode);
}
},
{
key: 'sort-by-frequency',
label: '按使用频率排序',
icon: currentSort === 'frequency' ? <CheckSquareOutlined /> : null,
onClick: () => {
setTableSortPreference(groupData.id, groupData.dbName, 'frequency');
const dbNode = {
key: `${groupData.id}-${groupData.dbName}`,
dataRef: groupData
};
loadTables(dbNode);
}
}
];
}
if (node.type === 'connection') {
// Redis connection menu
if (isRedis) {
@@ -1687,7 +1749,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
if (connectionStates[node.key] === 'success') status = 'success';
else if (connectionStates[node.key] === 'error') status = 'error';
}
const statusBadge = node.type === 'connection' || node.type === 'database' ? (
<Badge status={status} style={{ marginRight: 8 }} />
) : null;

View File

@@ -37,11 +37,13 @@ interface AppState {
sqlFormatOptions: { keywordCase: 'upper' | 'lower' };
queryOptions: { maxRows: number };
sqlLogs: SqlLog[];
tableAccessCount: Record<string, number>;
tableSortPreference: Record<string, 'name' | 'frequency'>;
addConnection: (conn: SavedConnection) => void;
updateConnection: (conn: SavedConnection) => void;
removeConnection: (id: string) => void;
addTab: (tab: TabData) => void;
closeTab: (id: string) => void;
closeOtherTabs: (id: string) => void;
@@ -58,9 +60,12 @@ interface AppState {
setAppearance: (appearance: Partial<{ opacity: number; blur: number }>) => void;
setSqlFormatOptions: (options: { keywordCase: 'upper' | 'lower' }) => void;
setQueryOptions: (options: Partial<{ maxRows: number }>) => void;
addSqlLog: (log: SqlLog) => void;
clearSqlLogs: () => void;
recordTableAccess: (connectionId: string, dbName: string, tableName: string) => void;
setTableSortPreference: (connectionId: string, dbName: string, sortBy: 'name' | 'frequency') => void;
}
export const useStore = create<AppState>()(
@@ -76,10 +81,12 @@ export const useStore = create<AppState>()(
sqlFormatOptions: { keywordCase: 'upper' },
queryOptions: { maxRows: 5000 },
sqlLogs: [],
tableAccessCount: {},
tableSortPreference: {},
addConnection: (conn) => set((state) => ({ connections: [...state.connections, conn] })),
updateConnection: (conn) => set((state) => ({
connections: state.connections.map(c => c.id === conn.id ? conn : c)
updateConnection: (conn) => set((state) => ({
connections: state.connections.map(c => c.id === conn.id ? conn : c)
})),
removeConnection: (id) => set((state) => ({ connections: state.connections.filter(c => c.id !== id) })),
@@ -145,9 +152,30 @@ export const useStore = create<AppState>()(
setAppearance: (appearance) => set((state) => ({ appearance: { ...state.appearance, ...appearance } })),
setSqlFormatOptions: (options) => set({ sqlFormatOptions: options }),
setQueryOptions: (options) => set((state) => ({ queryOptions: { ...state.queryOptions, ...options } })),
addSqlLog: (log) => set((state) => ({ sqlLogs: [log, ...state.sqlLogs].slice(0, 1000) })), // Keep last 1000 logs
clearSqlLogs: () => set({ sqlLogs: [] }),
recordTableAccess: (connectionId, dbName, tableName) => set((state) => {
const key = `${connectionId}-${dbName}-${tableName}`;
const currentCount = state.tableAccessCount[key] || 0;
return {
tableAccessCount: {
...state.tableAccessCount,
[key]: currentCount + 1
}
};
}),
setTableSortPreference: (connectionId, dbName, sortBy) => set((state) => {
const key = `${connectionId}-${dbName}`;
return {
tableSortPreference: {
...state.tableSortPreference,
[key]: sortBy
}
};
}),
}),
{
name: 'lite-db-storage', // name of the item in the storage (must be unique)
@@ -178,7 +206,16 @@ export const useStore = create<AppState>()(
return nextState as AppState;
},
partialize: (state) => ({ connections: state.connections, savedQueries: state.savedQueries, theme: state.theme, appearance: state.appearance, sqlFormatOptions: state.sqlFormatOptions, queryOptions: state.queryOptions }), // Don't persist logs
partialize: (state) => ({
connections: state.connections,
savedQueries: state.savedQueries,
theme: state.theme,
appearance: state.appearance,
sqlFormatOptions: state.sqlFormatOptions,
queryOptions: state.queryOptions,
tableAccessCount: state.tableAccessCount,
tableSortPreference: state.tableSortPreference
}), // Don't persist logs
}
)
);