feat(ui): 重设计文件管理器书签与传输面板

新增书签作用域与连接关联,后端为 favorite_paths
补充 scope 和 connection_id 字段及查询写入支持

前端重构书签弹窗与编辑表单,支持本地/云端筛选、
作用域选择与多语言文案更新

文件管理器工具栏改为紧凑图标样式,上传入口合并为
下拉菜单,并新增底部传输面板统一展示上传任务

同时优化 SSH 终端运行态为显式状态机,并为短命令
补充最短可见时间,避免运行中标记闪烁难以感知
This commit is contained in:
yinjianm
2026-05-01 22:54:29 +08:00
parent 96d9950c6b
commit 2233e3fa4f
33 changed files with 1868 additions and 1541 deletions
@@ -370,6 +370,18 @@ const definedMigrations: Migration[] = [
ALTER TABLE quick_command_tag_associations ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
UPDATE quick_command_tag_associations SET sort_order = rowid WHERE sort_order = 0;
`
},
{
id: 15,
name: 'Add scope and connection_id columns to favorite_paths table',
check: async (db: Database): Promise<boolean> => {
const scopeExists = await columnExists(db, 'favorite_paths', 'scope');
return !scopeExists;
},
sql: `
ALTER TABLE favorite_paths ADD COLUMN scope TEXT NOT NULL DEFAULT 'global';
ALTER TABLE favorite_paths ADD COLUMN connection_id INTEGER NULL;
`
}
];
+5 -3
View File
@@ -261,9 +261,11 @@ CREATE TABLE IF NOT EXISTS appearance_settings (
export const createFavoritePathsTableSQL = `
CREATE TABLE IF NOT EXISTS favorite_paths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NULL,
path TEXT NOT NULL,
last_used_at INTEGER NULL,
name TEXT NULL,
path TEXT NOT NULL,
scope TEXT NOT NULL DEFAULT 'global',
connection_id INTEGER NULL,
last_used_at INTEGER NULL,
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
@@ -6,7 +6,7 @@ import { FavoritePathSortBy } from '../favorite-paths/favorite-paths.service';
* 处理添加新收藏路径的请求
*/
export const createFavoritePath = async (req: Request, res: Response): Promise<void> => {
const { name, path } = req.body;
const { name, path, scope, connectionId } = req.body;
if (!path || typeof path !== 'string' || path.trim().length === 0) {
res.status(400).json({ message: '路径内容不能为空' });
@@ -18,7 +18,7 @@ export const createFavoritePath = async (req: Request, res: Response): Promise<v
}
try {
const newId = await FavoritePathsService.addFavoritePath(name, path);
const newId = await FavoritePathsService.addFavoritePath(name, path, scope || 'global', connectionId ?? null);
const newFavoritePath = await FavoritePathsService.getFavoritePathById(newId);
if (newFavoritePath) {
res.status(201).json({ message: '收藏路径已添加', favoritePath: newFavoritePath });
@@ -37,11 +37,14 @@ export const createFavoritePath = async (req: Request, res: Response): Promise<v
*/
export const getAllFavoritePaths = async (req: Request, res: Response): Promise<void> => {
const sortBy = req.query.sortBy as FavoritePathSortBy | undefined;
const scope = req.query.scope as string | undefined;
const connectionIdStr = req.query.connectionId as string | undefined;
const connectionId = connectionIdStr ? parseInt(connectionIdStr, 10) : undefined;
const validSortByOptions: FavoritePathSortBy[] = ['name', 'last_used_at'];
const validSortBy: FavoritePathSortBy = sortBy && validSortByOptions.includes(sortBy) ? sortBy : 'name';
try {
const favoritePaths = await FavoritePathsService.getAllFavoritePaths(validSortBy);
const favoritePaths = await FavoritePathsService.getAllFavoritePaths(validSortBy, scope, connectionId);
res.status(200).json(favoritePaths);
} catch (error: any) {
console.error('获取收藏路径控制器出错:', error);
@@ -79,7 +82,7 @@ export const getFavoritePathById = async (req: Request, res: Response): Promise<
*/
export const updateFavoritePath = async (req: Request, res: Response): Promise<void> => {
const id = parseInt(req.params.id, 10);
const { name, path } = req.body;
const { name, path, scope, connectionId } = req.body;
if (isNaN(id)) {
res.status(400).json({ message: '无效的 ID' });
@@ -95,7 +98,7 @@ export const updateFavoritePath = async (req: Request, res: Response): Promise<v
}
try {
const success = await FavoritePathsService.updateFavoritePath(id, name, path);
const success = await FavoritePathsService.updateFavoritePath(id, name, path, scope, connectionId);
if (success) {
const updatedFavoritePath = await FavoritePathsService.getFavoritePathById(id);
if (updatedFavoritePath) {
@@ -3,11 +3,13 @@ import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/conn
// 定义收藏路径接口
export interface FavoritePath {
id: number;
name: string | null; // 名称可选
name: string | null;
path: string;
last_used_at?: number | null; // 上次使用时间,允许为空
created_at: number; // Unix 时间戳 (秒)
updated_at: number; // Unix 时间戳 (秒)
scope: string;
connection_id: number | null;
last_used_at?: number | null;
created_at: number;
updated_at: number;
}
/**
@@ -16,11 +18,11 @@ export interface FavoritePath {
* @param path - 路径内容
* @returns 返回插入记录的 ID
*/
export const addFavoritePath = async (name: string | null, path: string): Promise<number> => {
const sql = `INSERT INTO favorite_paths (name, path, created_at, updated_at) VALUES (?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
export const addFavoritePath = async (name: string | null, path: string, scope: string = 'global', connectionId: number | null = null): Promise<number> => {
const sql = `INSERT INTO favorite_paths (name, path, scope, connection_id, created_at, updated_at) VALUES (?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now'))`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, path]);
const result = await runDb(db, sql, [name, path, scope, connectionId]);
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
throw new Error('添加收藏路径后未能获取有效的 lastID');
}
@@ -38,11 +40,22 @@ export const addFavoritePath = async (name: string | null, path: string): Promis
* @param path - 新的路径内容
* @returns 返回是否成功更新 (true/false)
*/
export const updateFavoritePath = async (id: number, name: string | null, path: string): Promise<boolean> => {
const sql = `UPDATE favorite_paths SET name = ?, path = ?, updated_at = strftime('%s', 'now') WHERE id = ?`;
export const updateFavoritePath = async (id: number, name: string | null, path: string, scope?: string, connectionId?: number | null): Promise<boolean> => {
const fields = ['name = ?', 'path = ?', "updated_at = strftime('%s', 'now')"];
const params: any[] = [name, path];
if (scope !== undefined) {
fields.push('scope = ?');
params.push(scope);
}
if (connectionId !== undefined) {
fields.push('connection_id = ?');
params.push(connectionId);
}
params.push(id);
const sql = `UPDATE favorite_paths SET ${fields.join(', ')} WHERE id = ?`;
try {
const db = await getDbInstance();
const result = await runDb(db, sql, [name, path, id]);
const result = await runDb(db, sql, params);
return result.changes > 0;
} catch (err: any) {
console.error('更新收藏路径时出错:', err.message);
@@ -72,15 +85,26 @@ export const deleteFavoritePath = async (id: number): Promise<boolean> => {
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回包含所有收藏路径条目的数组
*/
export const getAllFavoritePaths = async (sortBy: 'name' | 'last_used_at' = 'name'): Promise<FavoritePath[]> => {
let orderByClause = 'ORDER BY name ASC'; // 默认按名称升序
export const getAllFavoritePaths = async (sortBy: 'name' | 'last_used_at' = 'name', scope?: string, connectionId?: number): Promise<FavoritePath[]> => {
let orderByClause = 'ORDER BY name ASC';
if (sortBy === 'last_used_at') {
orderByClause = 'ORDER BY last_used_at DESC, name ASC'; // 按上次使用时间降序,同时间的按名称升序
orderByClause = 'ORDER BY last_used_at DESC, name ASC';
}
const sql = `SELECT id, name, path, last_used_at, created_at, updated_at FROM favorite_paths ${orderByClause}`;
const conditions: string[] = [];
const params: any[] = [];
if (scope) {
conditions.push('scope = ?');
params.push(scope);
}
if (connectionId !== undefined) {
conditions.push('connection_id = ?');
params.push(connectionId);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const sql = `SELECT id, name, path, scope, connection_id, last_used_at, created_at, updated_at FROM favorite_paths ${whereClause} ${orderByClause}`;
try {
const db = await getDbInstance();
const rows = await allDb<FavoritePath>(db, sql);
const rows = await allDb<FavoritePath>(db, sql, params);
return rows;
} catch (err: any) {
console.error('获取收藏路径时出错:', err.message);
@@ -10,13 +10,12 @@ export type FavoritePathSortBy = 'name' | 'last_used_at';
* @param path - 路径内容
* @returns 返回添加记录的 ID
*/
export const addFavoritePath = async (name: string | null, path: string): Promise<number> => {
export const addFavoritePath = async (name: string | null, path: string, scope: string = 'global', connectionId: number | null = null): Promise<number> => {
if (!path || path.trim().length === 0) {
throw new Error('路径内容不能为空');
}
// 如果 name 是空字符串,则视为 null
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const favoritePathId = await FavoritePathsRepository.addFavoritePath(finalName, path.trim());
const favoritePathId = await FavoritePathsRepository.addFavoritePath(finalName, path.trim(), scope, connectionId);
return favoritePathId;
};
@@ -27,12 +26,12 @@ export const addFavoritePath = async (name: string | null, path: string): Promis
* @param path - 新的路径内容
* @returns 返回是否成功更新 (更新行数 > 0)
*/
export const updateFavoritePath = async (id: number, name: string | null, path: string): Promise<boolean> => {
export const updateFavoritePath = async (id: number, name: string | null, path: string, scope?: string, connectionId?: number | null): Promise<boolean> => {
if (!path || path.trim().length === 0) {
throw new Error('路径内容不能为空');
}
const finalName = name && name.trim().length > 0 ? name.trim() : null;
const pathUpdated = await FavoritePathsRepository.updateFavoritePath(id, finalName, path.trim());
const pathUpdated = await FavoritePathsRepository.updateFavoritePath(id, finalName, path.trim(), scope, connectionId);
return pathUpdated;
};
@@ -51,8 +50,8 @@ export const deleteFavoritePath = async (id: number): Promise<boolean> => {
* @param sortBy - 排序字段 ('name' 或 'usage_count')
* @returns 返回排序后的收藏路径数组
*/
export const getAllFavoritePaths = async (sortBy: FavoritePathSortBy = 'name'): Promise<FavoritePath[]> => {
return FavoritePathsRepository.getAllFavoritePaths(sortBy);
export const getAllFavoritePaths = async (sortBy: FavoritePathSortBy = 'name', scope?: string, connectionId?: number): Promise<FavoritePath[]> => {
return FavoritePathsRepository.getAllFavoritePaths(sortBy, scope, connectionId);
};
/**