feat(ui): 重设计文件管理器书签与传输面板
新增书签作用域与连接关联,后端为 favorite_paths 补充 scope 和 connection_id 字段及查询写入支持 前端重构书签弹窗与编辑表单,支持本地/云端筛选、 作用域选择与多语言文案更新 文件管理器工具栏改为紧凑图标样式,上传入口合并为 下拉菜单,并新增底部传输面板统一展示上传任务 同时优化 SSH 终端运行态为显式状态机,并为短命令 补充最短可见时间,避免运行中标记闪烁难以感知
This commit is contained in:
@@ -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;
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user