feat: 添加路径收藏功能
This commit is contained in:
@@ -265,6 +265,24 @@ const definedMigrations: Migration[] = [
|
|||||||
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: 'Create favorite_paths table',
|
||||||
|
check: async (db: Database): Promise<boolean> => {
|
||||||
|
const tableAlreadyExists = await tableExists(db, 'favorite_paths');
|
||||||
|
return !tableAlreadyExists; // Only run if the table does NOT exist
|
||||||
|
},
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE IF NOT EXISTS favorite_paths (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
last_used_at INTEGER NULL;
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const tableDefinitions: TableDefinition[] = [
|
|||||||
{ name: 'command_history', sql: schemaSql.createCommandHistoryTableSQL },
|
{ name: 'command_history', sql: schemaSql.createCommandHistoryTableSQL },
|
||||||
{ name: 'path_history', sql: schemaSql.createPathHistoryTableSQL },
|
{ name: 'path_history', sql: schemaSql.createPathHistoryTableSQL },
|
||||||
{ name: 'quick_commands', sql: schemaSql.createQuickCommandsTableSQL },
|
{ name: 'quick_commands', sql: schemaSql.createQuickCommandsTableSQL },
|
||||||
|
{ name: 'favorite_paths', sql: schemaSql.createFavoritePathsTableSQL }, // Added Favorite Paths table
|
||||||
|
|
||||||
// Appearance related tables (often depend on others or have init logic)
|
// Appearance related tables (often depend on others or have init logic)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -240,3 +240,13 @@ CREATE TABLE IF NOT EXISTS appearance_settings (
|
|||||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
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,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
`;
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as FavoritePathsService from '../services/favorite-paths.service';
|
||||||
|
import { FavoritePathSortBy } from '../services/favorite-paths.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理添加新收藏路径的请求
|
||||||
|
*/
|
||||||
|
export const createFavoritePath = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const { name, path } = req.body;
|
||||||
|
|
||||||
|
if (!path || typeof path !== 'string' || path.trim().length === 0) {
|
||||||
|
res.status(400).json({ message: '路径内容不能为空' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name !== null && typeof name !== 'string') {
|
||||||
|
res.status(400).json({ message: '名称必须是字符串或 null' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newId = await FavoritePathsService.addFavoritePath(name, path);
|
||||||
|
const newFavoritePath = await FavoritePathsService.getFavoritePathById(newId);
|
||||||
|
if (newFavoritePath) {
|
||||||
|
res.status(201).json({ message: '收藏路径已添加', favoritePath: newFavoritePath });
|
||||||
|
} else {
|
||||||
|
console.error(`[Controller] 添加收藏路径后未能找到 ID: ${newId}`);
|
||||||
|
res.status(201).json({ message: '收藏路径已添加,但无法检索新记录', id: newId });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Controller] 添加收藏路径失败:', error.message);
|
||||||
|
res.status(500).json({ message: error.message || '无法添加收藏路径' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理获取所有收藏路径的请求 (支持排序)
|
||||||
|
*/
|
||||||
|
export const getAllFavoritePaths = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const sortBy = req.query.sortBy as FavoritePathSortBy | undefined;
|
||||||
|
const validSortByOptions: FavoritePathSortBy[] = ['name', 'last_used_at'];
|
||||||
|
const validSortBy: FavoritePathSortBy = sortBy && validSortByOptions.includes(sortBy) ? sortBy : 'name';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const favoritePaths = await FavoritePathsService.getAllFavoritePaths(validSortBy);
|
||||||
|
res.status(200).json(favoritePaths);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取收藏路径控制器出错:', error);
|
||||||
|
res.status(500).json({ message: error.message || '无法获取收藏路径' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理根据 ID 获取单个收藏路径的请求
|
||||||
|
*/
|
||||||
|
export const getFavoritePathById = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ message: '无效的 ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const favoritePath = await FavoritePathsService.getFavoritePathById(id);
|
||||||
|
if (favoritePath) {
|
||||||
|
res.status(200).json(favoritePath);
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ message: '未找到指定的收藏路径' });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取单个收藏路径控制器出错:', error);
|
||||||
|
res.status(500).json({ message: error.message || '无法获取收藏路径' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更新收藏路径的请求
|
||||||
|
*/
|
||||||
|
export const updateFavoritePath = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
const { name, path } = req.body;
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ message: '无效的 ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!path || typeof path !== 'string' || path.trim().length === 0) {
|
||||||
|
res.status(400).json({ message: '路径内容不能为空' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name !== null && typeof name !== 'string') {
|
||||||
|
res.status(400).json({ message: '名称必须是字符串或 null' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await FavoritePathsService.updateFavoritePath(id, name, path);
|
||||||
|
if (success) {
|
||||||
|
const updatedFavoritePath = await FavoritePathsService.getFavoritePathById(id);
|
||||||
|
if (updatedFavoritePath) {
|
||||||
|
res.status(200).json({ message: '收藏路径已更新', favoritePath: updatedFavoritePath });
|
||||||
|
} else {
|
||||||
|
console.error(`[Controller] 更新收藏路径后未能找到 ID: ${id}`);
|
||||||
|
res.status(200).json({ message: '收藏路径已更新,但无法检索更新后的记录' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pathExists = await FavoritePathsService.getFavoritePathById(id);
|
||||||
|
if (!pathExists) {
|
||||||
|
res.status(404).json({ message: '未找到要更新的收藏路径' });
|
||||||
|
} else {
|
||||||
|
console.error(`[Controller] 更新收藏路径 ${id} 失败,但路径存在。`);
|
||||||
|
res.status(500).json({ message: '更新收藏路径时发生未知错误' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新收藏路径控制器出错:', error);
|
||||||
|
res.status(500).json({ message: error.message || '无法更新收藏路径' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除收藏路径的请求
|
||||||
|
*/
|
||||||
|
export const deleteFavoritePath = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ message: '无效的 ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await FavoritePathsService.deleteFavoritePath(id);
|
||||||
|
if (success) {
|
||||||
|
res.status(200).json({ message: '收藏路径已删除' });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ message: '未找到要删除的收藏路径' });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('删除收藏路径控制器出错:', error);
|
||||||
|
res.status(500).json({ message: error.message || '无法删除收藏路径' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更新收藏路径上次使用时间的请求
|
||||||
|
*/
|
||||||
|
export const incrementUsage = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ message: '无效的 ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新上次使用时间
|
||||||
|
const lastUsedSuccess = await FavoritePathsService.updateFavoritePathLastUsed(id);
|
||||||
|
|
||||||
|
if (lastUsedSuccess) {
|
||||||
|
const updatedPath = await FavoritePathsService.getFavoritePathById(id);
|
||||||
|
if (updatedPath) {
|
||||||
|
res.status(200).json({ message: '上次使用时间已更新', favoritePath: updatedPath });
|
||||||
|
} else {
|
||||||
|
// 这种情况理论上不应该发生,因为 updateFavoritePathLastUsed 内部应该处理了路径不存在的情况
|
||||||
|
// 收藏路径 (ID: ${id}) 上次使用时间已更新,但无法检索该路径。
|
||||||
|
console.error(`[Controller] 收藏路径 (ID: ${id}) 上次使用时间已更新,但无法检索该路径。`);
|
||||||
|
res.status(404).json({ message: '上次使用时间已更新,但无法检索更新后的收藏路径' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 更新失败,检查路径是否存在以提供更具体的错误信息
|
||||||
|
const pathExists = await FavoritePathsService.getFavoritePathById(id);
|
||||||
|
if (!pathExists) {
|
||||||
|
res.status(404).json({ message: '未找到要更新上次使用时间的收藏路径' });
|
||||||
|
} else {
|
||||||
|
// 路径存在,但更新操作失败
|
||||||
|
console.warn(`[Controller] 尝试更新收藏路径 (ID: ${id}) 的上次使用时间失败,但路径存在。`);
|
||||||
|
res.status(500).json({ message: '更新上次使用时间失败' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新收藏路径上次使用时间控制器出错:', error);
|
||||||
|
res.status(500).json({ message: error.message || '无法更新上次使用时间' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更新收藏路径上次使用时间戳的请求 (PUT)
|
||||||
|
*/
|
||||||
|
export const updateLastUsedTimestamp = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
|
||||||
|
if (isNaN(id)) {
|
||||||
|
res.status(400).json({ message: '无效的 ID' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新上次使用时间
|
||||||
|
const success = await FavoritePathsService.updateFavoritePathLastUsed(id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const updatedPath = await FavoritePathsService.getFavoritePathById(id);
|
||||||
|
if (updatedPath) {
|
||||||
|
res.status(200).json({ message: '上次使用时间戳已更新', favoritePath: updatedPath });
|
||||||
|
} else {
|
||||||
|
// 这种情况理论上不应该发生,因为 updateFavoritePathLastUsed 内部应该处理了路径不存在的情况
|
||||||
|
console.error(`[Controller] 收藏路径 (ID: ${id}) 上次使用时间戳已更新,但无法检索该路径。`);
|
||||||
|
res.status(404).json({ message: '上次使用时间戳已更新,但无法检索更新后的收藏路径' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 更新失败,检查路径是否存在以提供更具体的错误信息
|
||||||
|
const pathExists = await FavoritePathsService.getFavoritePathById(id);
|
||||||
|
if (!pathExists) {
|
||||||
|
res.status(404).json({ message: '未找到要更新上次使用时间戳的收藏路径' });
|
||||||
|
} else {
|
||||||
|
// 路径存在,但更新操作失败
|
||||||
|
console.warn(`[Controller] 尝试更新收藏路径 (ID: ${id}) 的上次使用时间戳失败,但路径存在。`);
|
||||||
|
res.status(500).json({ message: '更新上次使用时间戳失败' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新收藏路径上次使用时间戳控制器出错:', error);
|
||||||
|
res.status(500).json({ message: error.message || '无法更新上次使用时间戳' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import * as FavoritePathsController from './favorite-paths.controller';
|
||||||
|
import { isAuthenticated } from '../auth/auth.middleware';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// 应用认证中间件,确保所有路径收藏相关的API都需要用户认证
|
||||||
|
router.use(isAuthenticated);
|
||||||
|
|
||||||
|
// 定义路由
|
||||||
|
router.post('/', FavoritePathsController.createFavoritePath); // POST /api/favorite-paths
|
||||||
|
router.get('/', FavoritePathsController.getAllFavoritePaths); // GET /api/favorite-paths?sortBy=name|usage_count
|
||||||
|
router.get('/:id', FavoritePathsController.getFavoritePathById); // GET /api/favorite-paths/:id
|
||||||
|
router.put('/:id', FavoritePathsController.updateFavoritePath); // PUT /api/favorite-paths/:id
|
||||||
|
router.delete('/:id', FavoritePathsController.deleteFavoritePath); // DELETE /api/favorite-paths/:id
|
||||||
|
router.put('/:id/update-last-used', FavoritePathsController.updateLastUsedTimestamp); // PUT /api/favorite-paths/:id/update-last-used
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -55,6 +55,7 @@ import quickCommandTagRoutes from './quick-command-tags/quick-command-tag.routes
|
|||||||
import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes';
|
import sshSuspendRouter from './ssh-suspend/ssh-suspend.routes';
|
||||||
import { transfersRoutes } from './transfers/transfers.routes';
|
import { transfersRoutes } from './transfers/transfers.routes';
|
||||||
import pathHistoryRoutes from './path-history/path-history.routes';
|
import pathHistoryRoutes from './path-history/path-history.routes';
|
||||||
|
import favoritePathsRouter from './favorite-paths/favorite-paths.routes';
|
||||||
import { initializeWebSocket } from './websocket';
|
import { initializeWebSocket } from './websocket';
|
||||||
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
import { ipWhitelistMiddleware } from './auth/ipWhitelist.middleware';
|
||||||
|
|
||||||
@@ -261,6 +262,7 @@ const startServer = () => {
|
|||||||
app.use('/api/v1/ssh-suspend', sshSuspendRouter);
|
app.use('/api/v1/ssh-suspend', sshSuspendRouter);
|
||||||
app.use('/api/v1/transfers', transfersRoutes());
|
app.use('/api/v1/transfers', transfersRoutes());
|
||||||
app.use('/api/v1/path-history', pathHistoryRoutes);
|
app.use('/api/v1/path-history', pathHistoryRoutes);
|
||||||
|
app.use('/api/v1/favorite-paths', favoritePathsRouter);
|
||||||
|
|
||||||
// 状态检查接口
|
// 状态检查接口
|
||||||
app.get('/api/v1/status', (req: Request, res: Response) => {
|
app.get('/api/v1/status', (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { getDbInstance, runDb, getDb as getDbRow, allDb } from '../database/connection';
|
||||||
|
|
||||||
|
// 定义收藏路径接口
|
||||||
|
export interface FavoritePath {
|
||||||
|
id: number;
|
||||||
|
name: string | null; // 名称可选
|
||||||
|
path: string;
|
||||||
|
last_used_at?: number | null; // 上次使用时间,允许为空
|
||||||
|
created_at: number; // Unix 时间戳 (秒)
|
||||||
|
updated_at: number; // Unix 时间戳 (秒)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一条新的收藏路径
|
||||||
|
* @param name - 路径名称 (可选)
|
||||||
|
* @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'))`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const result = await runDb(db, sql, [name, path]);
|
||||||
|
if (typeof result.lastID !== 'number' || result.lastID <= 0) {
|
||||||
|
throw new Error('添加收藏路径后未能获取有效的 lastID');
|
||||||
|
}
|
||||||
|
return result.lastID;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('添加收藏路径时出错:', err.message);
|
||||||
|
throw new Error('无法添加收藏路径');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定的收藏路径
|
||||||
|
* @param id - 要更新的记录 ID
|
||||||
|
* @param name - 新的路径名称 (可选)
|
||||||
|
* @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 = ?`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const result = await runDb(db, sql, [name, path, id]);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('更新收藏路径时出错:', err.message);
|
||||||
|
throw new Error('无法更新收藏路径');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 删除指定的收藏路径
|
||||||
|
* @param id - 要删除的记录 ID
|
||||||
|
* @returns 返回是否成功删除 (true/false)
|
||||||
|
*/
|
||||||
|
export const deleteFavoritePath = async (id: number): Promise<boolean> => {
|
||||||
|
const sql = `DELETE FROM favorite_paths WHERE id = ?`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const result = await runDb(db, sql, [id]);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('删除收藏路径时出错:', err.message);
|
||||||
|
throw new Error('无法删除收藏路径');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有收藏路径
|
||||||
|
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
||||||
|
* @returns 返回包含所有收藏路径条目的数组
|
||||||
|
*/
|
||||||
|
export const getAllFavoritePaths = async (sortBy: 'name' | 'last_used_at' = 'name'): Promise<FavoritePath[]> => {
|
||||||
|
let orderByClause = 'ORDER BY name ASC'; // 默认按名称升序
|
||||||
|
if (sortBy === 'last_used_at') {
|
||||||
|
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}`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const rows = await allDb<FavoritePath>(db, sql);
|
||||||
|
return rows;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('获取收藏路径时出错:', err.message);
|
||||||
|
throw new Error('无法获取收藏路径');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新指定收藏路径的上次使用时间
|
||||||
|
* @param id - 要更新的记录 ID
|
||||||
|
* @returns 返回是否成功更新 (true/false)
|
||||||
|
*/
|
||||||
|
export const updateFavoritePathLastUsedAt = async (id: number): Promise<boolean> => {
|
||||||
|
const sql = `UPDATE favorite_paths SET last_used_at = strftime('%s', 'now'), updated_at = strftime('%s', 'now') WHERE id = ?`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const result = await runDb(db, sql, [id]);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('更新收藏路径上次使用时间时出错:', err.message);
|
||||||
|
throw new Error('无法更新收藏路径上次使用时间');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 查找收藏路径
|
||||||
|
* @param id - 要查找的记录 ID
|
||||||
|
* @returns 返回找到的收藏路径条目,如果未找到则返回 undefined
|
||||||
|
*/
|
||||||
|
export const findFavoritePathById = async (id: number): Promise<FavoritePath | undefined> => {
|
||||||
|
const sql = `SELECT id, name, path, last_used_at, created_at, updated_at FROM favorite_paths WHERE id = ?`;
|
||||||
|
try {
|
||||||
|
const db = await getDbInstance();
|
||||||
|
const row = await getDbRow<FavoritePath>(db, sql, [id]);
|
||||||
|
return row;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('查找收藏路径时出错:', err.message);
|
||||||
|
throw new Error('无法查找收藏路径');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import * as FavoritePathsRepository from '../repositories/favorite-paths.repository';
|
||||||
|
import { FavoritePath } from '../repositories/favorite-paths.repository';
|
||||||
|
|
||||||
|
// 定义排序类型
|
||||||
|
export type FavoritePathSortBy = 'name' | 'last_used_at';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加收藏路径
|
||||||
|
* @param name - 路径名称 (可选)
|
||||||
|
* @param path - 路径内容
|
||||||
|
* @returns 返回添加记录的 ID
|
||||||
|
*/
|
||||||
|
export const addFavoritePath = async (name: string | null, path: string): 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());
|
||||||
|
return favoritePathId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新收藏路径
|
||||||
|
* @param id - 要更新的记录 ID
|
||||||
|
* @param name - 新的路径名称 (可选)
|
||||||
|
* @param path - 新的路径内容
|
||||||
|
* @returns 返回是否成功更新 (更新行数 > 0)
|
||||||
|
*/
|
||||||
|
export const updateFavoritePath = async (id: number, name: string | null, path: string): 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());
|
||||||
|
return pathUpdated;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除收藏路径
|
||||||
|
* @param id - 要删除的记录 ID
|
||||||
|
* @returns 返回是否成功删除 (删除行数 > 0)
|
||||||
|
*/
|
||||||
|
export const deleteFavoritePath = async (id: number): Promise<boolean> => {
|
||||||
|
const changes = await FavoritePathsRepository.deleteFavoritePath(id);
|
||||||
|
return changes;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有收藏路径,并按指定方式排序
|
||||||
|
* @param sortBy - 排序字段 ('name' 或 'usage_count')
|
||||||
|
* @returns 返回排序后的收藏路径数组
|
||||||
|
*/
|
||||||
|
export const getAllFavoritePaths = async (sortBy: FavoritePathSortBy = 'name'): Promise<FavoritePath[]> => {
|
||||||
|
return FavoritePathsRepository.getAllFavoritePaths(sortBy);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新收藏路径的上次使用时间
|
||||||
|
* @param id - 收藏路径的ID
|
||||||
|
* @returns Promise<boolean> - 操作是否成功
|
||||||
|
*/
|
||||||
|
export const updateFavoritePathLastUsed = async (id: number): Promise<boolean> => {
|
||||||
|
// 未来可能在这里添加额外的业务逻辑或验证
|
||||||
|
return FavoritePathsRepository.updateFavoritePathLastUsedAt(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 获取单个收藏路径
|
||||||
|
* @param id - 记录 ID
|
||||||
|
* @returns 返回找到的收藏路径,或 undefined
|
||||||
|
*/
|
||||||
|
export const getFavoritePathById = async (id: number): Promise<FavoritePath | undefined> => {
|
||||||
|
return FavoritePathsRepository.findFavoritePathById(id);
|
||||||
|
};
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed, type PropType } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useFavoritePathsStore, type FavoritePathItem } from '../stores/favoritePaths.store';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
pathData: {
|
||||||
|
type: Object as PropType<FavoritePathItem | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'saveSuccess']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const favoritePathsStore = useFavoritePathsStore();
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
id: '',
|
||||||
|
path: '',
|
||||||
|
name: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEditMode = computed(() => !!props.pathData?.id);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const errorMessage = ref<string | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
watch(() => props.isVisible, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
errorMessage.value = null; // Reset error on open
|
||||||
|
if (props.pathData) {
|
||||||
|
form.value = {
|
||||||
|
id: props.pathData.id,
|
||||||
|
path: props.pathData.path,
|
||||||
|
name: props.pathData.name || ''
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
form.value = { id: '', path: '', name: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
if (!form.value.path.trim()) {
|
||||||
|
errorMessage.value = t('favoritePaths.addEditForm.validation.pathRequired', 'Path is required.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Add other validation rules if needed
|
||||||
|
errorMessage.value = null;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
errorMessage.value = null;
|
||||||
|
try {
|
||||||
|
if (isEditMode.value && form.value.id) {
|
||||||
|
await favoritePathsStore.updateFavoritePath(form.value.id, {
|
||||||
|
path: form.value.path,
|
||||||
|
name: form.value.name || undefined, // Send undefined if empty to allow backend to handle
|
||||||
|
}, t);
|
||||||
|
} else {
|
||||||
|
await favoritePathsStore.addFavoritePath({
|
||||||
|
path: form.value.path,
|
||||||
|
name: form.value.name || undefined,
|
||||||
|
}, t);
|
||||||
|
}
|
||||||
|
emit('saveSuccess');
|
||||||
|
closeModal();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error saving favorite path:', error);
|
||||||
|
errorMessage.value = error.message || t('favoritePaths.addEditForm.errors.genericSaveError', 'Failed to save favorite path.');
|
||||||
|
// Notification is usually handled by the store, but we can show a local error too.
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
if (!isLoading.value) { // Prevent closing while loading
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isVisible"
|
||||||
|
class="fixed inset-0 z-[60] flex items-center justify-center bg-[var(--overlay-bg-color)]"
|
||||||
|
@click.self="closeModal"
|
||||||
|
>
|
||||||
|
<div class="bg-background text-foreground shadow-xl rounded-lg w-full max-w-md flex flex-col overflow-hidden m-4 p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<h2 class="m-0 mb-6 text-center text-xl font-semibold">
|
||||||
|
{{ isEditMode ? t('favoritePaths.addEditForm.editTitle', 'Edit Favorite Path') : t('favoritePaths.addEditForm.addTitle', 'Add New Favorite Path') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Form Body -->
|
||||||
|
<form @submit.prevent="handleSubmit" class="space-y-4 flex-grow overflow-y-auto">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="favPath-name" class="block text-sm font-medium text-text-secondary mb-1">
|
||||||
|
{{ t('favoritePaths.addEditForm.nameLabel', 'Name (Optional)') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="favPath-name"
|
||||||
|
type="text"
|
||||||
|
v-model="form.name"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="w-full bg-input border border-border rounded-md px-3 py-2 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||||
|
:placeholder="t('favoritePaths.addEditForm.namePlaceholder', 'My Documents')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="favPath-path" class="block text-sm font-medium text-text-secondary mb-1">
|
||||||
|
{{ t('favoritePaths.addEditForm.pathLabel', 'Path') }}
|
||||||
|
<span class="text-danger ml-0.5">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="favPath-path"
|
||||||
|
type="text"
|
||||||
|
v-model="form.path"
|
||||||
|
required
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="w-full bg-input border border-border rounded-md px-3 py-2 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||||
|
:placeholder="t('favoritePaths.addEditForm.pathPlaceholder', '/example/folder/path')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errorMessage" class="text-danger text-sm p-2 bg-danger/10 rounded-md">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex justify-end mt-8 pt-4 border-t border-border/50">
|
||||||
|
<!-- Secondary/Cancel Button -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="closeModal"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="py-2 px-5 rounded-lg text-sm font-medium transition-colors duration-150 bg-background border border-border/50 text-text-secondary hover:bg-border hover:text-foreground mr-3">
|
||||||
|
{{ t('common.cancel', 'Cancel') }}
|
||||||
|
</button>
|
||||||
|
<!-- Primary/Submit Button -->
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:disabled="isLoading || !form.path.trim()"
|
||||||
|
class="py-2 px-5 rounded-lg text-sm font-semibold transition-colors duration-150 bg-primary text-white border-none shadow-md hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:bg-gray-400 disabled:opacity-70 disabled:cursor-not-allowed">
|
||||||
|
{{ isLoading ? t('common.saving', 'Saving...') : t('common.save', 'Save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Styles are primarily Tailwind based */
|
||||||
|
.bg-background-hover:hover {
|
||||||
|
background-color: var(--color-bg-hover); /* Ensure this CSS variable is defined globally or in Tailwind config */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, watch, onBeforeUnmount, nextTick, type PropType } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useFavoritePathsStore, type FavoritePathItem } from '../stores/favoritePaths.store';
|
||||||
|
import { useSessionStore } from '../stores/session.store';
|
||||||
|
import AddEditFavoritePathForm from './AddEditFavoritePathForm.vue';
|
||||||
|
|
||||||
|
const PADDING = 8; // px
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
triggerElement: {
|
||||||
|
type: Object as PropType<HTMLElement | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'navigateToPath']);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const favoritePathsStore = useFavoritePathsStore();
|
||||||
|
const sessionStore = useSessionStore();
|
||||||
|
|
||||||
|
const searchTerm = ref('');
|
||||||
|
const showAddEditModal = ref(false);
|
||||||
|
const editingPathItem = ref<FavoritePathItem | null>(null);
|
||||||
|
const modalContentRef = ref<HTMLElement | null>(null);
|
||||||
|
const modalStyle = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
|
||||||
|
const filteredPaths = computed(() => {
|
||||||
|
if (!searchTerm.value) {
|
||||||
|
return favoritePathsStore.favoritePaths;
|
||||||
|
}
|
||||||
|
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
||||||
|
return favoritePathsStore.favoritePaths.filter(
|
||||||
|
(p) =>
|
||||||
|
p.path.toLowerCase().includes(lowerSearchTerm) ||
|
||||||
|
(p.name && p.name.toLowerCase().includes(lowerSearchTerm))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed property for sort button icon and title
|
||||||
|
const currentSortBy = computed(() => favoritePathsStore.currentSortBy);
|
||||||
|
|
||||||
|
const sortButtonIcon = computed(() => {
|
||||||
|
return currentSortBy.value === 'name' ? 'fas fa-sort-alpha-down' : 'fas fa-clock';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const toggleSort = () => {
|
||||||
|
const newSortBy = currentSortBy.value === 'name' ? 'last_used_at' : 'name';
|
||||||
|
favoritePathsStore.setSortBy(newSortBy);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemClick = async (pathItem: FavoritePathItem) => {
|
||||||
|
try {
|
||||||
|
// Mark path as used before navigating
|
||||||
|
await favoritePathsStore.markPathAsUsed(pathItem.id, t);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to mark path as used:', error);
|
||||||
|
// Optionally, inform the user about the failure, though navigation will still proceed.
|
||||||
|
}
|
||||||
|
emit('navigateToPath', pathItem.path);
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddModal = () => {
|
||||||
|
editingPathItem.value = null;
|
||||||
|
showAddEditModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (pathItem: FavoritePathItem) => {
|
||||||
|
editingPathItem.value = { ...pathItem };
|
||||||
|
showAddEditModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (pathItem: FavoritePathItem) => {
|
||||||
|
if (confirm(t('favoritePaths.confirmDelete', { name: pathItem.name || pathItem.path }))) {
|
||||||
|
try {
|
||||||
|
await favoritePathsStore.deleteFavoritePath(pathItem.id, t);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete favorite path from modal:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
if (!props.isVisible || !props.triggerElement || !modalContentRef.value) {
|
||||||
|
// If not visible or refs not available, do nothing or hide.
|
||||||
|
// v-if handles DOM presence, so style isn't applied when not isVisible.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerRect = props.triggerElement.getBoundingClientRect();
|
||||||
|
const modalWidth = modalContentRef.value.offsetWidth;
|
||||||
|
const modalHeight = modalContentRef.value.offsetHeight;
|
||||||
|
|
||||||
|
// If dimensions are zero when modal is supposed to be visible,
|
||||||
|
// it might mean content affecting size isn't ready. Retry once.
|
||||||
|
if (modalWidth === 0 && modalHeight === 0 && props.isVisible) {
|
||||||
|
nextTick(updatePosition);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
let top = triggerRect.bottom + 2; // Default position below trigger, with a small 2px gap
|
||||||
|
let left = triggerRect.left;
|
||||||
|
|
||||||
|
// Check for bottom overflow
|
||||||
|
if (top + modalHeight + PADDING > viewportHeight) {
|
||||||
|
// Try to position above the trigger
|
||||||
|
top = triggerRect.top - modalHeight - 2; // Position above trigger, with a small 2px gap
|
||||||
|
}
|
||||||
|
|
||||||
|
// If positioning above also causes top overflow (e.g., trigger is near the top and modal is tall)
|
||||||
|
if (top < PADDING) {
|
||||||
|
top = PADDING; // Align to viewport top with padding
|
||||||
|
// Note: If modalHeight is still greater than viewportHeight - 2*PADDING,
|
||||||
|
// it will overflow downwards. The `max-h-80` class on the modal
|
||||||
|
// should generally prevent the modal itself from being excessively tall.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for right overflow
|
||||||
|
if (left + modalWidth + PADDING > viewportWidth) {
|
||||||
|
left = viewportWidth - modalWidth - PADDING; // Align to viewport right edge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for left overflow (less likely with initial left alignment to trigger, but good for robustness)
|
||||||
|
if (left < PADDING) {
|
||||||
|
left = PADDING; // Align to viewport left edge
|
||||||
|
}
|
||||||
|
|
||||||
|
modalStyle.value = {
|
||||||
|
position: 'fixed', // Position relative to the viewport
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${left}px`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Click Outside Logic ---
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (modalContentRef.value && !modalContentRef.value.contains(event.target as Node)) {
|
||||||
|
if (!showAddEditModal.value) { // Do not close if add/edit modal is open
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.isVisible, (newValue: boolean) => {
|
||||||
|
if (newValue) {
|
||||||
|
favoritePathsStore.fetchFavoritePaths(t);
|
||||||
|
searchTerm.value = '';
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
nextTick(() => { // Ensure DOM is ready for measurements
|
||||||
|
updatePosition(); // Calculate initial position
|
||||||
|
window.addEventListener('resize', updatePosition); // Adjust position on window resize
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('resize', updatePosition); // Clean up resize listener
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.isVisible) {
|
||||||
|
favoritePathsStore.fetchFavoritePaths(t);
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
window.removeEventListener('resize', updatePosition); // Ensure resize listener is cleaned up
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- New single root element -->
|
||||||
|
<div>
|
||||||
|
<!-- Favorite Paths Dropdown -->
|
||||||
|
<div
|
||||||
|
v-if="isVisible"
|
||||||
|
ref="modalContentRef"
|
||||||
|
:style="modalStyle"
|
||||||
|
class="z-50 w-72 md:w-80 rounded-md bg-background shadow-lg border border-border/50 max-h-80 flex flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<!-- Toolbar: Search and Add Button -->
|
||||||
|
<div class="p-2 flex-shrink-0 flex items-center gap-2">
|
||||||
|
<div class="relative flex-grow">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="searchTerm"
|
||||||
|
:placeholder="t('favoritePaths.searchPlaceholder', 'Search by name or path...')"
|
||||||
|
class="w-full bg-input border border-border rounded-md pl-2.5 pr-2 py-1.5 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="toggleSort"
|
||||||
|
class="flex items-center justify-center w-8 h-8 bg-background border border-border text-text-secondary rounded-lg text-sm cursor-pointer shadow-sm transition-colors duration-200 ease-in-out hover:bg-primary/10 hover:text-primary focus:outline-none flex-shrink-0"
|
||||||
|
>
|
||||||
|
<i :class="sortButtonIcon"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="openAddModal"
|
||||||
|
class="flex items-center justify-center w-8 h-8 bg-primary text-white border-none rounded-lg text-sm font-semibold cursor-pointer shadow-md transition-colors duration-200 ease-in-out hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary flex-shrink-0"
|
||||||
|
:title="t('favoritePaths.addNew', 'Add new favorite path')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus text-base"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Path List -->
|
||||||
|
<div class="overflow-y-auto flex-grow p-1 text-sm">
|
||||||
|
<ul v-if="!favoritePathsStore.isLoading && favoritePathsStore.filteredFavoritePaths.length" class="list-none m-0 p-0">
|
||||||
|
<li
|
||||||
|
v-for="favPath in favoritePathsStore.filteredFavoritePaths"
|
||||||
|
:key="favPath.id"
|
||||||
|
class="p-2 hover:bg-primary/10 cursor-pointer group flex items-center justify-between rounded-md transition-colors duration-150"
|
||||||
|
@click="handleItemClick(favPath)"
|
||||||
|
:title="favPath.path"
|
||||||
|
>
|
||||||
|
<div class="flex-grow overflow-hidden mr-2">
|
||||||
|
<p class="font-medium truncate text-foreground">
|
||||||
|
{{ favPath.name || favPath.path }}
|
||||||
|
</p>
|
||||||
|
<p v-if="favPath.name" class="text-xs text-text-secondary truncate">
|
||||||
|
{{ favPath.path }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 flex items-center gap-1 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
|
||||||
|
<button
|
||||||
|
@click.stop="openEditModal(favPath)"
|
||||||
|
class="p-1.5 rounded text-text-secondary hover:text-primary hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||||
|
:title="t('common.edit')">
|
||||||
|
<i class="fas fa-pencil-alt text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click.stop="handleDelete(favPath)"
|
||||||
|
class="p-1.5 rounded text-text-secondary hover:text-error hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
||||||
|
:title="t('common.delete')">
|
||||||
|
<i class="fas fa-trash-alt text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else-if="favoritePathsStore.isLoading" class="p-3 text-center text-text-secondary">
|
||||||
|
<i class="fas fa-spinner fa-spin mr-1"></i>
|
||||||
|
{{ t('favoritePaths.loading', 'Loading favorites...') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="p-3 text-center text-text-secondary">
|
||||||
|
<i class="fas fa-star-half-alt mr-1"></i>
|
||||||
|
{{ searchTerm ? t('favoritePaths.noResults', 'No matching favorites found.') : t('favoritePaths.noFavorites', 'No favorite paths yet. Add one!') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> <!-- End of Favorite Paths Dropdown div -->
|
||||||
|
|
||||||
|
<!-- Add/Edit Modal -->
|
||||||
|
<AddEditFavoritePathForm
|
||||||
|
v-if="showAddEditModal"
|
||||||
|
:is-visible="showAddEditModal"
|
||||||
|
:path-data="editingPathItem"
|
||||||
|
@close="showAddEditModal = false"
|
||||||
|
@save-success="() => { favoritePathsStore.fetchFavoritePaths(t); showAddEditModal = false; }"
|
||||||
|
/>
|
||||||
|
</div> <!-- End of new single root element -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Styles are mostly Tailwind. max-h-80 is applied to the main div */
|
||||||
|
/* Styling for the add/edit form if it's part of this component's template directly and needs specific scoping */
|
||||||
|
</style>
|
||||||
@@ -20,6 +20,7 @@ import type { FileListItem } from '../types/sftp.types';
|
|||||||
import type { WebSocketMessage } from '../types/websocket.types';
|
import type { WebSocketMessage } from '../types/websocket.types';
|
||||||
import PathHistoryDropdown from './PathHistoryDropdown.vue';
|
import PathHistoryDropdown from './PathHistoryDropdown.vue';
|
||||||
import { usePathHistoryStore } from '../stores/pathHistory.store';
|
import { usePathHistoryStore } from '../stores/pathHistory.store';
|
||||||
|
import FavoritePathsModal from './FavoritePathsModal.vue'; // +++ Import FavoritePathsModal +++
|
||||||
|
|
||||||
|
|
||||||
type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
|
type SftpManagerInstance = ReturnType<typeof createSftpActionsManager>;
|
||||||
@@ -130,6 +131,10 @@ const fileListContainerRef = ref<HTMLDivElement | null>(null); // 文件列表
|
|||||||
const dropOverlayRef = ref<HTMLDivElement | null>(null); // +++ 拖拽蒙版引用 +++
|
const dropOverlayRef = ref<HTMLDivElement | null>(null); // +++ 拖拽蒙版引用 +++
|
||||||
// const scrollIntervalId = ref<number | null>(null); // 已移至 useFileManagerDragAndDrop
|
// const scrollIntervalId = ref<number | null>(null); // 已移至 useFileManagerDragAndDrop
|
||||||
|
|
||||||
|
// +++ Favorite Paths Modal State +++
|
||||||
|
const showFavoritePathsModal = ref(false);
|
||||||
|
const favoritePathsButtonRef = ref<HTMLButtonElement | null>(null); // Ref for the trigger button
|
||||||
|
|
||||||
// +++ Path History Refs +++
|
// +++ Path History Refs +++
|
||||||
const showPathHistoryDropdown = ref(false);
|
const showPathHistoryDropdown = ref(false);
|
||||||
const pathInputWrapperRef = ref<HTMLDivElement | null>(null); // Wrapper for path input and dropdown
|
const pathInputWrapperRef = ref<HTMLDivElement | null>(null); // Wrapper for path input and dropdown
|
||||||
@@ -1548,8 +1553,23 @@ const handleOpenEditorClick = () => {
|
|||||||
// 暂时使用 triggerPopup,传递空字符串表示空编辑器
|
// 暂时使用 triggerPopup,传递空字符串表示空编辑器
|
||||||
// 后续可能需要 fileEditorStore.triggerEmptyPopup(props.sessionId);
|
// 后续可能需要 fileEditorStore.triggerEmptyPopup(props.sessionId);
|
||||||
fileEditorStore.triggerPopup('', props.sessionId); // 修复:传递空字符串而不是 null
|
fileEditorStore.triggerPopup('', props.sessionId); // 修复:传递空字符串而不是 null
|
||||||
};
|
};
|
||||||
</script>
|
|
||||||
|
// +++ Favorite Paths Modal Logic +++
|
||||||
|
const toggleFavoritePathsModal = () => {
|
||||||
|
showFavoritePathsModal.value = !showFavoritePathsModal.value;
|
||||||
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Toggled FavoritePathsModal. Visible: ${showFavoritePathsModal.value}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigateToPathFromFavorites = (path: string) => {
|
||||||
|
if (currentSftpManager.value) {
|
||||||
|
currentSftpManager.value.loadDirectory(path);
|
||||||
|
// Optionally, add to local path history if not already handled by the store/modal
|
||||||
|
// pathHistoryStore.addPath(path);
|
||||||
|
}
|
||||||
|
showFavoritePathsModal.value = false; // Close modal after navigation
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full overflow-hidden bg-background text-foreground text-sm font-sans">
|
<div class="flex flex-col h-full overflow-hidden bg-background text-foreground text-sm font-sans">
|
||||||
@@ -1615,13 +1635,31 @@ const handleOpenEditorClick = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- End Path Actions -->
|
</div> <!-- End Path Actions -->
|
||||||
|
<!-- Wrapper for Favorite Paths Button and Modal -->
|
||||||
|
<div class="relative flex-shrink-0">
|
||||||
|
<!-- Favorite Paths Button -->
|
||||||
|
<button
|
||||||
|
ref="favoritePathsButtonRef"
|
||||||
|
class="flex items-center justify-center w-7 h-7 text-text-secondary rounded transition-colors duration-200 hover:enabled:bg-black/10 hover:enabled:text-foreground"
|
||||||
|
@click="toggleFavoritePathsModal"
|
||||||
|
>
|
||||||
|
<i class="fas fa-star text-base"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Favorite Paths Modal -->
|
||||||
|
<FavoritePathsModal
|
||||||
|
:is-visible="showFavoritePathsModal"
|
||||||
|
:trigger-element="favoritePathsButtonRef"
|
||||||
|
@close="showFavoritePathsModal = false"
|
||||||
|
@navigate-to-path="handleNavigateToPathFromFavorites"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Path Bar with History Dropdown -->
|
<!-- Path Bar with History Dropdown -->
|
||||||
<div ref="pathInputWrapperRef" class="relative flex items-center bg-background border border-border rounded px-1.5 py-0.5 min-w-[100px] flex-shrink">
|
<div ref="pathInputWrapperRef" class="relative flex items-center bg-background border border-border rounded px-1.5 py-0.5 min-w-[100px] flex-shrink">
|
||||||
<span v-show="!isEditingPath && !showPathHistoryDropdown" @click="startPathEdit" class="text-text-secondary whitespace-nowrap overflow-x-auto pr-2 cursor-text">
|
<span v-show="!isEditingPath && !showPathHistoryDropdown" @click="startPathEdit" class="text-text-secondary whitespace-nowrap overflow-x-auto pr-2 cursor-text">
|
||||||
<span v-if="!props.isMobile">{{ t('fileManager.currentPath') }}:</span>
|
|
||||||
<strong
|
<strong
|
||||||
:title="t('fileManager.editPathTooltip')"
|
:title="t('fileManager.editPathTooltip')"
|
||||||
class="font-medium text-link ml-1 px-1 rounded transition-colors duration-200"
|
class="font-medium text-link px-1 rounded transition-colors duration-200"
|
||||||
:class="{
|
:class="{
|
||||||
'hover:bg-black/5': currentSftpManager && props.wsDeps.isConnected.value,
|
'hover:bg-black/5': currentSftpManager && props.wsDeps.isConnected.value,
|
||||||
'opacity-60 cursor-not-allowed': !currentSftpManager || !props.wsDeps.isConnected.value
|
'opacity-60 cursor-not-allowed': !currentSftpManager || !props.wsDeps.isConnected.value
|
||||||
@@ -1902,8 +1940,10 @@ const handleOpenEditorClick = () => {
|
|||||||
@confirm="handleModalConfirm"
|
@confirm="handleModalConfirm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Favorite Paths Modal is now positioned near its button -->
|
||||||
|
|
||||||
</div>
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1504,6 +1504,37 @@
|
|||||||
"transferInitiatedGeneric": "Transfer task created successfully.",
|
"transferInitiatedGeneric": "Transfer task created successfully.",
|
||||||
"transferFailedError": "Failed to initiate transfer. Please try again."
|
"transferFailedError": "Failed to initiate transfer. Please try again."
|
||||||
},
|
},
|
||||||
|
"favoritePaths": {
|
||||||
|
"addEditForm": {
|
||||||
|
"validation": {
|
||||||
|
"pathRequired": "Path is required."
|
||||||
|
},
|
||||||
|
"editTitle": "Edit Favorite Path",
|
||||||
|
"addTitle": "Add New Favorite Path",
|
||||||
|
"pathLabel": "Path",
|
||||||
|
"pathPlaceholder": "/example/folder/path",
|
||||||
|
"nameLabel": "Name (Optional)",
|
||||||
|
"namePlaceholder": "My Documents",
|
||||||
|
"errors": {
|
||||||
|
"genericSaveError": "Failed to save favorite path."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
|
||||||
|
"searchPlaceholder": "Search by name or path...",
|
||||||
|
"addNew": "Add new favorite path",
|
||||||
|
"loading": "Loading favorites...",
|
||||||
|
"noResults": "No matching favorites found.",
|
||||||
|
"noFavorites": "No favorite paths yet. Add one!",
|
||||||
|
"notifications": {
|
||||||
|
"fetchError": "Failed to load favorite paths.",
|
||||||
|
"addSuccess": "Favorite path added successfully.",
|
||||||
|
"addError": "Failed to add favorite path.",
|
||||||
|
"updateSuccess": "Favorite path updated successfully.",
|
||||||
|
"updateError": "Failed to update favorite path.",
|
||||||
|
"deleteSuccess": "Favorite path deleted successfully.",
|
||||||
|
"deleteError": "Failed to delete favorite path."
|
||||||
|
}
|
||||||
|
},
|
||||||
"pathHistory": {
|
"pathHistory": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"empty": "No path history",
|
"empty": "No path history",
|
||||||
|
|||||||
@@ -1466,6 +1466,37 @@
|
|||||||
"transferInitiatedGeneric": "転送タスクが正常に作成されました。",
|
"transferInitiatedGeneric": "転送タスクが正常に作成されました。",
|
||||||
"transferFailedError": "転送の開始に失敗しました。もう一度お試しください。"
|
"transferFailedError": "転送の開始に失敗しました。もう一度お試しください。"
|
||||||
},
|
},
|
||||||
|
"favoritePaths": {
|
||||||
|
"addEditForm": {
|
||||||
|
"validation": {
|
||||||
|
"pathRequired": "Path is required."
|
||||||
|
},
|
||||||
|
"editTitle": "Edit Favorite Path",
|
||||||
|
"addTitle": "Add New Favorite Path",
|
||||||
|
"pathLabel": "Path",
|
||||||
|
"pathPlaceholder": "/example/folder/path",
|
||||||
|
"nameLabel": "Name (Optional)",
|
||||||
|
"namePlaceholder": "My Documents",
|
||||||
|
"errors": {
|
||||||
|
"genericSaveError": "Failed to save favorite path."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
|
||||||
|
"searchPlaceholder": "Search by name or path...",
|
||||||
|
"addNew": "Add new favorite path",
|
||||||
|
"loading": "Loading favorites...",
|
||||||
|
"noResults": "No matching favorites found.",
|
||||||
|
"noFavorites": "No favorite paths yet. Add one!",
|
||||||
|
"notifications": {
|
||||||
|
"fetchError": "Failed to load favorite paths.",
|
||||||
|
"addSuccess": "Favorite path added successfully.",
|
||||||
|
"addError": "Failed to add favorite path.",
|
||||||
|
"updateSuccess": "Favorite path updated successfully.",
|
||||||
|
"updateError": "Failed to update favorite path.",
|
||||||
|
"deleteSuccess": "Favorite path deleted successfully.",
|
||||||
|
"deleteError": "Failed to delete favorite path."
|
||||||
|
}
|
||||||
|
},
|
||||||
"pathHistory": {
|
"pathHistory": {
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
"empty": "パス履歴がありません",
|
"empty": "パス履歴がありません",
|
||||||
|
|||||||
@@ -1509,6 +1509,37 @@
|
|||||||
"logExportError": "导出已挂起会话日志失败: {error}"
|
"logExportError": "导出已挂起会话日志失败: {error}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"favoritePaths": {
|
||||||
|
"addEditForm": {
|
||||||
|
"validation": {
|
||||||
|
"pathRequired": "路径不能为空。"
|
||||||
|
},
|
||||||
|
"editTitle": "编辑收藏路径",
|
||||||
|
"addTitle": "添加新收藏路径",
|
||||||
|
"pathLabel": "路径",
|
||||||
|
"pathPlaceholder": "/example/folder/path",
|
||||||
|
"nameLabel": "名称 (可选)",
|
||||||
|
"namePlaceholder": "我的文档",
|
||||||
|
"errors": {
|
||||||
|
"genericSaveError": "保存收藏路径失败。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"confirmDelete": "您确定要删除 \"{name}\" 吗?",
|
||||||
|
"searchPlaceholder": "按名称或路径搜索...",
|
||||||
|
"addNew": "添加新收藏路径",
|
||||||
|
"loading": "正在加载收藏...",
|
||||||
|
"noResults": "未找到匹配的收藏。",
|
||||||
|
"noFavorites": "还没有收藏路径,快添加一个吧!",
|
||||||
|
"notifications": {
|
||||||
|
"fetchError": "加载收藏路径失败。",
|
||||||
|
"addSuccess": "收藏路径添加成功。",
|
||||||
|
"addError": "添加收藏路径失败。",
|
||||||
|
"updateSuccess": "收藏路径更新成功。",
|
||||||
|
"updateError": "更新收藏路径失败。",
|
||||||
|
"deleteSuccess": "收藏路径删除成功。",
|
||||||
|
"deleteError": "删除收藏路径失败。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pathHistory": {
|
"pathHistory": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"empty": "没有路径历史记录",
|
"empty": "没有路径历史记录",
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import apiClient from '../utils/apiClient';
|
||||||
|
import { useUiNotificationsStore } from './uiNotifications.store';
|
||||||
|
|
||||||
|
// TODO: Define these types more precisely based on API response
|
||||||
|
export type FavoritePathSortType = 'name' | 'last_used_at';
|
||||||
|
|
||||||
|
export interface FavoritePathItem {
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
name?: string;
|
||||||
|
last_used_at?: number | null; // Added last_used_at
|
||||||
|
// Add other relevant fields from the API if any
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FavoritePathsState {
|
||||||
|
favoritePaths: FavoritePathItem[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
searchTerm: string;
|
||||||
|
currentSortBy: FavoritePathSortType; // Added currentSortBy
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFavoritePathsStore = defineStore('favoritePaths', {
|
||||||
|
state: (): FavoritePathsState => {
|
||||||
|
const savedSortBy = localStorage.getItem('favoritePathSortBy') as FavoritePathSortType | null;
|
||||||
|
return {
|
||||||
|
favoritePaths: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
searchTerm: '',
|
||||||
|
currentSortBy: savedSortBy || 'name', // Initialize from localStorage or default to 'name'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
// The filteredFavoritePaths getter will now operate on the already sorted list
|
||||||
|
filteredFavoritePaths(state): FavoritePathItem[] {
|
||||||
|
if (!state.searchTerm) {
|
||||||
|
return state.favoritePaths;
|
||||||
|
}
|
||||||
|
const lowerCaseSearchTerm = state.searchTerm.toLowerCase();
|
||||||
|
// Note: state.favoritePaths is now always sorted by this.currentSortBy
|
||||||
|
return state.favoritePaths.filter(fav =>
|
||||||
|
fav.path.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||||
|
(fav.name && fav.name.toLowerCase().includes(lowerCaseSearchTerm))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getFavoritePathById(state): (id: string) => FavoritePathItem | undefined {
|
||||||
|
return (id) => state.favoritePaths.find(fav => fav.id === id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
_sortFavoritePaths() {
|
||||||
|
this.favoritePaths.sort((a, b) => {
|
||||||
|
if (this.currentSortBy === 'name') {
|
||||||
|
const nameA = a.name?.toLowerCase() || a.path.toLowerCase();
|
||||||
|
const nameB = b.name?.toLowerCase() || b.path.toLowerCase();
|
||||||
|
if (nameA < nameB) return -1;
|
||||||
|
if (nameA > nameB) return 1;
|
||||||
|
return 0;
|
||||||
|
} else if (this.currentSortBy === 'last_used_at') {
|
||||||
|
// Sort by last_used_at descending, nulls/undefined last
|
||||||
|
const timeA = a.last_used_at ?? 0;
|
||||||
|
const timeB = b.last_used_at ?? 0;
|
||||||
|
return timeB - timeA; // Descending
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setSearchTerm(term: string) {
|
||||||
|
this.searchTerm = term;
|
||||||
|
},
|
||||||
|
async fetchFavoritePaths(t: (key: string, defaultMessage: string) => string) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.error = null;
|
||||||
|
const notificationsStore = useUiNotificationsStore();
|
||||||
|
try {
|
||||||
|
// Fetch all paths, sorting will be done locally
|
||||||
|
const response = await apiClient.get<FavoritePathItem[]>('/favorite-paths');
|
||||||
|
this.favoritePaths = response.data;
|
||||||
|
this._sortFavoritePaths(); // Sort locally after fetching
|
||||||
|
} catch (err: any) {
|
||||||
|
this.error = err.message || 'Failed to fetch favorite paths';
|
||||||
|
console.error('Error fetching favorite paths:', err);
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.fetchError', 'Failed to load favorite paths.'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSortBy(sortBy: FavoritePathSortType) {
|
||||||
|
this.currentSortBy = sortBy;
|
||||||
|
localStorage.setItem('favoritePathSortBy', sortBy);
|
||||||
|
this._sortFavoritePaths(); // Re-sort locally
|
||||||
|
},
|
||||||
|
async markPathAsUsed(pathId: string, t: (key: string, defaultMessage: string) => string) {
|
||||||
|
const notificationsStore = useUiNotificationsStore();
|
||||||
|
try {
|
||||||
|
const response = await apiClient.put<{ message: string, favoritePath: FavoritePathItem }>(`/favorite-paths/${pathId}/update-last-used`);
|
||||||
|
const updatedPath = response.data.favoritePath;
|
||||||
|
if (updatedPath) {
|
||||||
|
const index = this.favoritePaths.findIndex(p => p.id === pathId);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.favoritePaths[index] = updatedPath;
|
||||||
|
} else {
|
||||||
|
// Path not found locally, might happen if list is stale. Add it.
|
||||||
|
this.favoritePaths.push(updatedPath);
|
||||||
|
}
|
||||||
|
this._sortFavoritePaths(); // Re-sort after updating
|
||||||
|
} else {
|
||||||
|
// Fallback to re-fetch if updated item isn't returned as expected
|
||||||
|
console.warn('markPathAsUsed did not receive updated path, re-fetching list.');
|
||||||
|
await this.fetchFavoritePaths(t);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`Error marking path ${pathId} as used:`, err);
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.markAsUsedError', 'Failed to mark path as used.'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async addFavoritePath(newPathData: Omit<FavoritePathItem, 'id' | 'createdAt' | 'updatedAt' | 'last_used_at'>, t: (key: string, defaultMessage: string) => string) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.error = null;
|
||||||
|
const notificationsStore = useUiNotificationsStore();
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post<{ message: string, favoritePath: FavoritePathItem }>('/favorite-paths', newPathData);
|
||||||
|
this.favoritePaths.push(response.data.favoritePath);
|
||||||
|
this._sortFavoritePaths(); // Sort after adding
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.addSuccess', 'Favorite path added successfully.'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
this.error = err.message || 'Failed to add favorite path';
|
||||||
|
console.error('Error adding favorite path:', err);
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.addError', 'Failed to add favorite path.'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
throw err; // Re-throw to allow form to handle error
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateFavoritePath(id: string, updatedPathData: Partial<Omit<FavoritePathItem, 'id' | 'createdAt' | 'updatedAt' | 'last_used_at'>>, t: (key: string, defaultMessage: string) => string) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.error = null;
|
||||||
|
const notificationsStore = useUiNotificationsStore();
|
||||||
|
try {
|
||||||
|
const response = await apiClient.put<{ message: string, favoritePath: FavoritePathItem }>(`/favorite-paths/${id}`, updatedPathData);
|
||||||
|
const index = this.favoritePaths.findIndex(fav => fav.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.favoritePaths[index] = response.data.favoritePath;
|
||||||
|
this._sortFavoritePaths(); // Sort after updating
|
||||||
|
}
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.updateSuccess', 'Favorite path updated successfully.'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
this.error = err.message || 'Failed to update favorite path';
|
||||||
|
console.error('Error updating favorite path:', err);
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.updateError', 'Failed to update favorite path.'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
throw err; // Re-throw to allow form to handle error
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteFavoritePath(id: string, t: (key: string, defaultMessage: string) => string) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.error = null;
|
||||||
|
const notificationsStore = useUiNotificationsStore();
|
||||||
|
try {
|
||||||
|
await apiClient.delete(`/favorite-paths/${id}`);
|
||||||
|
this.favoritePaths = this.favoritePaths.filter(fav => fav.id !== id);
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.deleteSuccess', 'Favorite path deleted successfully.'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
this.error = err.message || 'Failed to delete favorite path';
|
||||||
|
console.error('Error deleting favorite path:', err);
|
||||||
|
notificationsStore.addNotification({
|
||||||
|
message: t('favoritePaths.notifications.deleteError', 'Failed to delete favorite path.'),
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user