优化,贴图
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "screenshot",
|
"identifier": "screenshot",
|
||||||
"description": "Capability for screenshot overlay and editor windows",
|
"description": "Capability for screenshot overlay, editor and pin windows",
|
||||||
"windows": ["screenshot-overlay*", "screenshot-editor-*"],
|
"windows": ["screenshot-overlay*", "screenshot-editor-*", "screenshot-pin"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-hide",
|
"core:window:allow-hide",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-position",
|
"core:window:allow-set-position",
|
||||||
"core:window:allow-set-size",
|
"core:window:allow-set-size",
|
||||||
|
"core:window:allow-set-resizable",
|
||||||
"core:window:allow-set-always-on-top",
|
"core:window:allow-set-always-on-top",
|
||||||
"core:window:allow-set-skip-taskbar",
|
"core:window:allow-set-skip-taskbar",
|
||||||
"core:window:allow-set-decorations",
|
"core:window:allow-set-decorations",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ pub mod windows {
|
|||||||
pub const OSD_OVERLAY: &str = "osd-overlay";
|
pub const OSD_OVERLAY: &str = "osd-overlay";
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const SCREENSHOT_OVERLAY: &str = "screenshot-overlay";
|
pub const SCREENSHOT_OVERLAY: &str = "screenshot-overlay";
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const SCREENSHOT_PIN: &str = "screenshot-pin";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tauri 事件名(与前端 constants::EVENTS 对应)
|
/// Tauri 事件名(与前端 constants::EVENTS 对应)
|
||||||
@@ -41,6 +43,7 @@ pub mod events {
|
|||||||
pub const OSD_END_DRAG: &str = "osd-end-drag";
|
pub const OSD_END_DRAG: &str = "osd-end-drag";
|
||||||
// 截图
|
// 截图
|
||||||
pub const SCREENSHOT_SHORTCUT: &str = "screenshot-shortcut";
|
pub const SCREENSHOT_SHORTCUT: &str = "screenshot-shortcut";
|
||||||
|
pub const SCREENSHOT_PIN_SHORTCUT: &str = "screenshot-pin-shortcut";
|
||||||
// 内核安装进度
|
// 内核安装进度
|
||||||
pub const KERNEL_INSTALL_PROGRESS: &str = "kernel-install-progress";
|
pub const KERNEL_INSTALL_PROGRESS: &str = "kernel-install-progress";
|
||||||
// 进程与下载
|
// 进程与下载
|
||||||
|
|||||||
+18
-12
@@ -34,10 +34,10 @@ use mihomo_manager::{
|
|||||||
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager,
|
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager,
|
||||||
};
|
};
|
||||||
use monitor_kernel::{
|
use monitor_kernel::{
|
||||||
monitor_elevate_self, monitor_get_elevate_on_launch, monitor_get_hardware_config,
|
monitor_elevate_self, monitor_get_auto_start, monitor_get_elevate_on_launch,
|
||||||
monitor_get_snapshot, monitor_get_status, monitor_kernel_info, monitor_set_elevate_on_launch,
|
monitor_get_hardware_config, monitor_get_snapshot, monitor_get_status, monitor_kernel_info,
|
||||||
monitor_set_hardware_config, monitor_start, monitor_start_elevated, monitor_status,
|
monitor_set_auto_start, monitor_set_elevate_on_launch, monitor_set_hardware_config,
|
||||||
monitor_stop, MonitorKernel,
|
monitor_start, monitor_start_elevated, monitor_status, monitor_stop, MonitorKernel,
|
||||||
};
|
};
|
||||||
use network_monitor::network_status;
|
use network_monitor::network_status;
|
||||||
use osd_window::{
|
use osd_window::{
|
||||||
@@ -54,8 +54,9 @@ use screenshot::commands::{
|
|||||||
screenshot_crop_stored, screenshot_cursor_pos, screenshot_delete_cache,
|
screenshot_crop_stored, screenshot_cursor_pos, screenshot_delete_cache,
|
||||||
screenshot_disable_transitions, screenshot_enum_windows, screenshot_fullscreen_png,
|
screenshot_disable_transitions, screenshot_enum_windows, screenshot_fullscreen_png,
|
||||||
screenshot_get_editor_image, screenshot_get_fullscreen_bmp, screenshot_load_cache,
|
screenshot_get_editor_image, screenshot_get_fullscreen_bmp, screenshot_load_cache,
|
||||||
screenshot_register_shortcut, screenshot_save_cache, screenshot_save_png,
|
screenshot_register_pin_shortcut, screenshot_register_shortcut, screenshot_save_cache,
|
||||||
screenshot_set_editor_image, screenshot_unregister_shortcut, screenshot_window_from_point,
|
screenshot_save_png, screenshot_set_editor_image, screenshot_unregister_pin_shortcut,
|
||||||
|
screenshot_unregister_shortcut, screenshot_window_from_point,
|
||||||
};
|
};
|
||||||
use clipboard::{
|
use clipboard::{
|
||||||
ClipboardManager,
|
ClipboardManager,
|
||||||
@@ -127,13 +128,14 @@ fn export_bindings() {
|
|||||||
downloader_get_tasks, downloader_check_url, downloader_add_task, downloader_pause_task,
|
downloader_get_tasks, downloader_check_url, downloader_add_task, downloader_pause_task,
|
||||||
downloader_resume_task, downloader_remove_task, downloader_get_settings,
|
downloader_resume_task, downloader_remove_task, downloader_get_settings,
|
||||||
downloader_save_settings, downloader_open_dir, downloader_open_url,
|
downloader_save_settings, downloader_open_dir, downloader_open_url,
|
||||||
// screenshot(19,豁免 2:get_fullscreen_bmp 返回 ipc::Response、compose_copy 接收 ipc::Request)
|
// screenshot(21,豁免 2:get_fullscreen_bmp 返回 ipc::Response、compose_copy 接收 ipc::Request)
|
||||||
screenshot_disable_transitions, screenshot_register_shortcut,
|
screenshot_disable_transitions, screenshot_register_shortcut,
|
||||||
screenshot_unregister_shortcut, screenshot_capture_fullscreen, screenshot_fullscreen_png,
|
screenshot_unregister_shortcut, screenshot_register_pin_shortcut,
|
||||||
screenshot_clear_fullscreen, screenshot_crop_stored, screenshot_crop_copy_stored,
|
screenshot_unregister_pin_shortcut, screenshot_capture_fullscreen,
|
||||||
screenshot_window_from_point, screenshot_cursor_pos, screenshot_enum_windows,
|
screenshot_fullscreen_png, screenshot_clear_fullscreen, screenshot_crop_stored,
|
||||||
screenshot_capture_window, screenshot_set_editor_image, screenshot_get_editor_image,
|
screenshot_crop_copy_stored, screenshot_window_from_point, screenshot_cursor_pos,
|
||||||
screenshot_copy_image, screenshot_save_png,
|
screenshot_enum_windows, screenshot_capture_window, screenshot_set_editor_image,
|
||||||
|
screenshot_get_editor_image, screenshot_copy_image, screenshot_save_png,
|
||||||
screenshot_save_cache, screenshot_load_cache, screenshot_delete_cache,
|
screenshot_save_cache, screenshot_load_cache, screenshot_delete_cache,
|
||||||
])
|
])
|
||||||
.export(Typescript::default(), "../src/lib/bindings.ts")
|
.export(Typescript::default(), "../src/lib/bindings.ts")
|
||||||
@@ -204,6 +206,8 @@ pub fn run() {
|
|||||||
monitor_get_snapshot,
|
monitor_get_snapshot,
|
||||||
monitor_get_elevate_on_launch,
|
monitor_get_elevate_on_launch,
|
||||||
monitor_set_elevate_on_launch,
|
monitor_set_elevate_on_launch,
|
||||||
|
monitor_get_auto_start,
|
||||||
|
monitor_set_auto_start,
|
||||||
monitor_get_hardware_config,
|
monitor_get_hardware_config,
|
||||||
monitor_set_hardware_config,
|
monitor_set_hardware_config,
|
||||||
network_status,
|
network_status,
|
||||||
@@ -291,6 +295,8 @@ pub fn run() {
|
|||||||
screenshot_delete_cache,
|
screenshot_delete_cache,
|
||||||
screenshot_register_shortcut,
|
screenshot_register_shortcut,
|
||||||
screenshot_unregister_shortcut,
|
screenshot_unregister_shortcut,
|
||||||
|
screenshot_register_pin_shortcut,
|
||||||
|
screenshot_unregister_pin_shortcut,
|
||||||
screenshot_disable_transitions,
|
screenshot_disable_transitions,
|
||||||
screenshot_compose_copy
|
screenshot_compose_copy
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ mod system_proxy;
|
|||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use pseudo::is_pseudo_node;
|
pub use pseudo::is_pseudo_node;
|
||||||
|
pub use system_proxy::get_system_proxy_windows;
|
||||||
pub use types::{InstallProgress, KernelInfo, KernelUpdateInfo, ProfileMeta, ProxySettings, ProxyStatus};
|
pub use types::{InstallProgress, KernelInfo, KernelUpdateInfo, ProfileMeta, ProxySettings, ProxyStatus};
|
||||||
pub use commands::{
|
pub use commands::{
|
||||||
proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection,
|
proxy_activate_profile, proxy_check_kernel_update, proxy_clear_system_proxy, proxy_close_connection,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pub(crate) fn clear_system_proxy_windows() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub(crate) fn get_system_proxy_windows() -> bool {
|
pub fn get_system_proxy_windows() -> bool {
|
||||||
use winreg::enums::*;
|
use winreg::enums::*;
|
||||||
use winreg::RegKey;
|
use winreg::RegKey;
|
||||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
@@ -67,7 +67,7 @@ pub(crate) fn clear_system_proxy_windows() -> Result<(), String> {
|
|||||||
Err("系统代理仅支持 Windows".into())
|
Err("系统代理仅支持 Windows".into())
|
||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
pub(crate) fn get_system_proxy_windows() -> bool {
|
pub fn get_system_proxy_windows() -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,41 @@ pub fn is_thing_elevated() -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== 监控设置持久化 =====================
|
||||||
|
|
||||||
|
/// 监控模块设置(存储于 {app_data_dir}/monitor/settings.json)
|
||||||
|
/// 参照代理模块 ProxySettings 的模式,仅包含需要持久化的运行时开关。
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MonitorSettings {
|
||||||
|
/// 应用启动时自动启动监控内核(默认 false)
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_start: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 监控设置文件路径: {app_data_dir}/monitor/settings.json
|
||||||
|
fn settings_path(app_data_dir: &std::path::Path) -> PathBuf {
|
||||||
|
app_data_dir.join("monitor").join("settings.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 读取监控设置(文件不存在或解析失败时返回默认值)
|
||||||
|
pub fn load_monitor_settings(app_data_dir: &std::path::Path) -> MonitorSettings {
|
||||||
|
fs::read_to_string(settings_path(app_data_dir))
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<MonitorSettings>(&s).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入监控设置
|
||||||
|
pub fn save_monitor_settings(app_data_dir: &std::path::Path, settings: &MonitorSettings) -> Result<(), String> {
|
||||||
|
let path = settings_path(app_data_dir);
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||||
|
}
|
||||||
|
let content = serde_json::to_string_pretty(settings).map_err(|e| format!("序列化设置失败: {}", e))?;
|
||||||
|
fs::write(path, content).map_err(|e| format!("写入设置失败: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== 永久提权标志持久化 =====================
|
// ===================== 永久提权标志持久化 =====================
|
||||||
|
|
||||||
/// 永久提权标志文件路径: {app_data_dir}/monitor/elevate.json
|
/// 永久提权标志文件路径: {app_data_dir}/monitor/elevate.json
|
||||||
@@ -295,6 +330,32 @@ impl MonitorKernel {
|
|||||||
write_elevate_flag(&self.root.join("elevate.json"), enabled)
|
write_elevate_flag(&self.root.join("elevate.json"), enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 读取监控设置(auto_start 等)
|
||||||
|
pub fn load_settings(&self) -> MonitorSettings {
|
||||||
|
load_monitor_settings(&self.root.parent().unwrap_or(&self.root).to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入监控设置
|
||||||
|
pub fn save_settings(&self, settings: &MonitorSettings) -> Result<(), String> {
|
||||||
|
save_monitor_settings(&self.root.parent().unwrap_or(&self.root).to_path_buf(), settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 应用启动时检查是否需要自动启动监控内核
|
||||||
|
pub fn auto_start_on_launch(&self, app: &AppHandle) {
|
||||||
|
let settings = self.load_settings();
|
||||||
|
if !settings.auto_start {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let monitor = self.clone();
|
||||||
|
let app_handle = app.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
match monitor.start_with_subscription(&app_handle).await {
|
||||||
|
Ok(info) => crate::logger::log_info("monitor", &format!("自动启动成功, pid={:?}", info.pid)),
|
||||||
|
Err(e) => crate::logger::log_warn("monitor", &format!("自动启动跳过: {}", e)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn cores_dir(&self) -> PathBuf {
|
fn cores_dir(&self) -> PathBuf {
|
||||||
self.root.join("cores")
|
self.root.join("cores")
|
||||||
}
|
}
|
||||||
@@ -950,6 +1011,25 @@ pub fn monitor_set_elevate_on_launch(
|
|||||||
state.set_elevate_on_launch(enabled)
|
state.set_elevate_on_launch(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 查询"应用启动时自动启动监控内核"是否已启用
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn monitor_get_auto_start(
|
||||||
|
state: tauri::State<'_, MonitorKernel>,
|
||||||
|
) -> bool {
|
||||||
|
state.load_settings().auto_start
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置/清除"应用启动时自动启动监控内核"开关
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn monitor_set_auto_start(
|
||||||
|
state: tauri::State<'_, MonitorKernel>,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut settings = state.load_settings();
|
||||||
|
settings.auto_start = enabled;
|
||||||
|
state.save_settings(&settings)
|
||||||
|
}
|
||||||
|
|
||||||
/// 查询硬件监控配置(透传 Kernel GET /config/hardware)。
|
/// 查询硬件监控配置(透传 Kernel GET /config/hardware)。
|
||||||
/// 返回当前配置 + 可用硬件/传感器类型清单,供前端 Dialog 渲染。
|
/// 返回当前配置 + 可用硬件/传感器类型清单,供前端 Dialog 渲染。
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ pub async fn quickpanel_build_file_index(app: AppHandle) -> Result<i64, String>
|
|||||||
} else {
|
} else {
|
||||||
settings.index_dirs
|
settings.index_dirs
|
||||||
};
|
};
|
||||||
|
// 懒加载:首次构建时自动初始化 DB 连接
|
||||||
|
file_index::ensure_initialized(&app);
|
||||||
// 阻塞操作放到 spawn_blocking
|
// 阻塞操作放到 spawn_blocking
|
||||||
tauri::async_runtime::spawn_blocking(move || file_index::build_index(&dirs))
|
tauri::async_runtime::spawn_blocking(move || file_index::build_index(&dirs))
|
||||||
.await
|
.await
|
||||||
@@ -126,7 +128,10 @@ pub async fn quickpanel_build_file_index(app: AppHandle) -> Result<i64, String>
|
|||||||
pub async fn quickpanel_search_files(
|
pub async fn quickpanel_search_files(
|
||||||
query: String,
|
query: String,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
|
app: AppHandle,
|
||||||
) -> Result<Vec<file_index::FileRecord>, String> {
|
) -> Result<Vec<file_index::FileRecord>, String> {
|
||||||
|
// 懒加载:首次搜索时自动初始化 DB 连接
|
||||||
|
file_index::ensure_initialized(&app);
|
||||||
tauri::async_runtime::spawn_blocking(move || file_index::search(&query, limit.unwrap_or(50)))
|
tauri::async_runtime::spawn_blocking(move || file_index::search(&query, limit.unwrap_or(50)))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("搜索任务失败: {}", e))
|
.map_err(|e| format!("搜索任务失败: {}", e))
|
||||||
@@ -135,7 +140,9 @@ pub async fn quickpanel_search_files(
|
|||||||
/// 获取索引状态
|
/// 获取索引状态
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn quickpanel_file_index_stats() -> file_index::IndexStats {
|
pub async fn quickpanel_file_index_stats(app: AppHandle) -> file_index::IndexStats {
|
||||||
|
// 懒加载:查询状态前确保 DB 已初始化(未初始化时 stats 返回全 0)
|
||||||
|
file_index::ensure_initialized(&app);
|
||||||
file_index::stats()
|
file_index::stats()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,17 @@ pub fn init(app: &AppHandle) {
|
|||||||
crate::logger::log_info("quickpanel", &format!("文件索引 DB 已就绪: {}", path.display()));
|
crate::logger::log_info("quickpanel", &format!("文件索引 DB 已就绪: {}", path.display()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 懒加载:首次访问文件索引时自动初始化(若尚未初始化)。
|
||||||
|
/// 避免应用启动时即打开 SQLite 连接,降低启动 IO 开销。
|
||||||
|
pub fn ensure_initialized(app: &AppHandle) {
|
||||||
|
let guard = index_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
if guard.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(guard);
|
||||||
|
init(app);
|
||||||
|
}
|
||||||
|
|
||||||
/// 判断索引是否已初始化
|
/// 判断索引是否已初始化
|
||||||
fn with_conn<F, R>(f: F) -> Option<R>
|
fn with_conn<F, R>(f: F) -> Option<R>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -67,6 +67,27 @@ pub async fn screenshot_unregister_shortcut(app: tauri::AppHandle) -> Result<(),
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 注册(或切换)贴图全局快捷键。传入空字符串则禁用快捷键。
|
||||||
|
/// 按下时 emit 'screenshot-pin-shortcut',由前端切换贴图窗口显示/隐藏。
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn screenshot_register_pin_shortcut(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
shortcut: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
crate::shortcut::register_shortcut(&app, "贴图", &shortcut, |a| {
|
||||||
|
let _ = a.emit(crate::constants::events::SCREENSHOT_PIN_SHORTCUT, ());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 注销贴图全局快捷键
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn screenshot_unregister_pin_shortcut(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
crate::shortcut::unregister_shortcut(&app, "贴图");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码
|
/// 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
@@ -349,17 +370,17 @@ pub async fn screenshot_save_png(png_base64: String, path: String) -> Result<(),
|
|||||||
.map_err(|e| format!("保存任务失败: {}", e))?
|
.map_err(|e| format!("保存任务失败: {}", e))?
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 截图历史缓存:完整 PNG 落盘缓存目录,内存只保留缩略图 =====
|
// ===== 截图历史缓存:完整 PNG 落盘到应用数据目录(持久化,随历史保留数量清理),内存只保留缩略图 =====
|
||||||
|
|
||||||
/// 历史缓存根目录(app_cache_dir/screenshot/history)
|
/// 历史根目录(app_data_dir/screenshot/history)
|
||||||
fn history_cache_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
|
fn history_cache_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
|
||||||
let dir = app
|
let dir = app
|
||||||
.path()
|
.path()
|
||||||
.app_cache_dir()
|
.app_data_dir()
|
||||||
.map_err(|e| format!("获取缓存目录失败: {}", e))?
|
.map_err(|e| format!("获取应用数据目录失败: {}", e))?
|
||||||
.join("screenshot")
|
.join("screenshot")
|
||||||
.join("history");
|
.join("history");
|
||||||
std::fs::create_dir_all(&dir).map_err(|e| format!("创建缓存目录失败: {}", e))?;
|
std::fs::create_dir_all(&dir).map_err(|e| format!("创建历史目录失败: {}", e))?;
|
||||||
Ok(dir)
|
Ok(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-12
@@ -90,8 +90,10 @@ pub fn init(app: &mut App<Wry>) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
app.manage(clipboard);
|
app.manage(clipboard);
|
||||||
|
|
||||||
// ===== 快速面板:快捷键 + 预创建弹窗 + 文件索引 =====
|
// ===== 快速面板:快捷键 + 预创建弹窗 =====
|
||||||
// defaultEnabled:true 假设启用;用户在设置页禁用模块时由前端 onDisable 钩子注销快捷键。
|
// defaultEnabled:true 假设启用;用户在设置页禁用模块时由前端 onDisable 钩子注销快捷键。
|
||||||
|
// 文件索引 DB 连接改为懒加载(首次搜索/构建时由 commands 中的 ensure_initialized 触发),
|
||||||
|
// 避免应用启动时即打开 SQLite 连接,降低启动 IO 开销。
|
||||||
let qp_settings = crate::quickpanel::load_settings(&app.handle());
|
let qp_settings = crate::quickpanel::load_settings(&app.handle());
|
||||||
if !qp_settings.shortcut.trim().is_empty() {
|
if !qp_settings.shortcut.trim().is_empty() {
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
@@ -106,8 +108,6 @@ pub fn init(app: &mut App<Wry>) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// 预创建弹窗窗口(隐藏),首次按快捷键时直接 show,避免首次创建时序问题
|
// 预创建弹窗窗口(隐藏),首次按快捷键时直接 show,避免首次创建时序问题
|
||||||
crate::quickpanel::ensure_window(&app_handle);
|
crate::quickpanel::ensure_window(&app_handle);
|
||||||
}
|
}
|
||||||
// 初始化文件索引数据库(不立即构建,由前端设置页或首次唤起时触发)
|
|
||||||
crate::quickpanel::file_index::init(&app.handle());
|
|
||||||
|
|
||||||
// ===== 托盘菜单 =====
|
// ===== 托盘菜单 =====
|
||||||
crate::tray_menu::create_tray_menu(app.handle())?;
|
crate::tray_menu::create_tray_menu(app.handle())?;
|
||||||
@@ -123,16 +123,9 @@ pub fn init(app: &mut App<Wry>) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitor Kernel:硬件监控默认启用,被动读取无副作用
|
// monitor Kernel:用户在设置中开启"自动启动"时随应用启动
|
||||||
if let Some(monitor) = app.try_state::<MonitorKernel>() {
|
if let Some(monitor) = app.try_state::<MonitorKernel>() {
|
||||||
let monitor = monitor.inner().clone();
|
monitor.auto_start_on_launch(app.handle());
|
||||||
let app_handle = app.handle().clone();
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
match monitor.start_with_subscription(&app_handle).await {
|
|
||||||
Ok(info) => crate::logger::log_info("monitor", &format!("自动启动成功, pid={:?}", info.pid)),
|
|
||||||
Err(e) => crate::logger::log_warn("monitor", &format!("自动启动跳过: {}", e)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 截图快捷键由前端 screenshotStore 启动时调用 screenshot_register_shortcut 注册
|
// 截图快捷键由前端 screenshotStore 启动时调用 screenshot_register_shortcut 注册
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ pub struct ProxyNodeInfo {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TrayMenuState {
|
pub struct TrayMenuState {
|
||||||
pub proxy_running: bool,
|
pub proxy_running: bool,
|
||||||
|
pub system_proxy: bool,
|
||||||
pub monitor_running: bool,
|
pub monitor_running: bool,
|
||||||
pub proxy_group: Option<String>,
|
pub proxy_group: Option<String>,
|
||||||
pub proxy_nodes: Vec<ProxyNodeInfo>,
|
pub proxy_nodes: Vec<ProxyNodeInfo>,
|
||||||
@@ -150,6 +151,8 @@ async fn fetch_proxy_nodes(app: &AppHandle) -> Option<(String, Vec<(String, Opti
|
|||||||
pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState {
|
pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState {
|
||||||
let proxy_running = is_proxy_running(app);
|
let proxy_running = is_proxy_running(app);
|
||||||
let monitor_running = is_monitor_running(app);
|
let monitor_running = is_monitor_running(app);
|
||||||
|
// 读取 Windows 注册表中的真实系统代理状态(不依赖 settings.json 缓存)
|
||||||
|
let system_proxy = crate::mihomo_manager::get_system_proxy_windows();
|
||||||
|
|
||||||
let (proxy_group, proxy_nodes, proxy_current) = if proxy_running {
|
let (proxy_group, proxy_nodes, proxy_current) = if proxy_running {
|
||||||
match fetch_proxy_nodes(app).await {
|
match fetch_proxy_nodes(app).await {
|
||||||
@@ -172,6 +175,7 @@ pub async fn get_tray_menu_state(app: &AppHandle) -> TrayMenuState {
|
|||||||
|
|
||||||
TrayMenuState {
|
TrayMenuState {
|
||||||
proxy_running,
|
proxy_running,
|
||||||
|
system_proxy,
|
||||||
monitor_running,
|
monitor_running,
|
||||||
proxy_group,
|
proxy_group,
|
||||||
proxy_nodes,
|
proxy_nodes,
|
||||||
@@ -338,6 +342,21 @@ pub async fn tray_menu_action(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"system_proxy_toggle" => {
|
||||||
|
// 根据注册表当前真实状态切换(不依赖 settings.json 缓存,避免与主界面不同步)
|
||||||
|
let mihomo = app.state::<MihomoManager>();
|
||||||
|
if crate::mihomo_manager::get_system_proxy_windows() {
|
||||||
|
if let Err(e) = mihomo.disable_system_proxy() {
|
||||||
|
crate::logger::log_error("tray", &format!("关闭系统代理失败: {}", e));
|
||||||
|
send_notification(&app, "系统代理关闭失败", &e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Err(e) = mihomo.enable_system_proxy() {
|
||||||
|
crate::logger::log_error("tray", &format!("开启系统代理失败: {}", e));
|
||||||
|
send_notification(&app, "系统代理开启失败", &e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"osd_toggle" => {
|
"osd_toggle" => {
|
||||||
let _ = app.emit(crate::constants::events::TRAY_TOGGLE_OSD, ());
|
let _ = app.emit(crate::constants::events::TRAY_TOGGLE_OSD, ());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": ["binaries/*", "resources/thing-extension/**/*"]
|
"resources": ["binaries/*", "resources/thing-extension/**/*"],
|
||||||
|
"windows": {
|
||||||
|
"nsis": {
|
||||||
|
"compression": "lzma"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useAppStore } from '@/stores/appStore'
|
|||||||
import { useScreenshotStore } from '@/stores/screenshotStore'
|
import { useScreenshotStore } from '@/stores/screenshotStore'
|
||||||
import { useQuickPanelStore } from '@/stores/quickpanelStore'
|
import { useQuickPanelStore } from '@/stores/quickpanelStore'
|
||||||
import { useMonitorStore } from '@/stores/monitorStore'
|
import { useMonitorStore } from '@/stores/monitorStore'
|
||||||
|
import { useProcessStore } from '@/stores/processStore'
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
import { moduleRegistry } from '@/modules/registry'
|
import { moduleRegistry } from '@/modules/registry'
|
||||||
import type { ModuleMeta } from '@/types/module'
|
import type { ModuleMeta } from '@/types/module'
|
||||||
@@ -20,6 +21,7 @@ const appStore = useAppStore()
|
|||||||
const screenshotStore = useScreenshotStore()
|
const screenshotStore = useScreenshotStore()
|
||||||
const quickpanelStore = useQuickPanelStore()
|
const quickpanelStore = useQuickPanelStore()
|
||||||
const monitorStore = useMonitorStore()
|
const monitorStore = useMonitorStore()
|
||||||
|
const processStore = useProcessStore()
|
||||||
|
|
||||||
/** 侧边栏 / 标题栏需要的模块信息(id + name + icon) */
|
/** 侧边栏 / 标题栏需要的模块信息(id + name + icon) */
|
||||||
interface NavModule {
|
interface NavModule {
|
||||||
@@ -193,6 +195,8 @@ onUnmounted(() => {
|
|||||||
screenshotStore.destroyExportListener()
|
screenshotStore.destroyExportListener()
|
||||||
// 释放 OSD 事件监听与配置 watcher
|
// 释放 OSD 事件监听与配置 watcher
|
||||||
monitorStore.disposeOsd()
|
monitorStore.disposeOsd()
|
||||||
|
// 释放进程状态事件监听
|
||||||
|
processStore.destroyListener()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ const displayModules = computed(() => {
|
|||||||
<Button
|
<Button
|
||||||
:variant="activeModule === module.id ? 'default' : 'ghost'"
|
:variant="activeModule === module.id ? 'default' : 'ghost'"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-10 w-10 rounded-lg transition-[background-color,color,box-shadow] duration-300"
|
class="size-10 rounded-lg transition-[background-color,color,box-shadow] duration-300"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-primary text-primary-foreground shadow-md': activeModule === module.id,
|
'bg-primary text-primary-foreground shadow-md': activeModule === module.id,
|
||||||
'hover:bg-secondary/50': activeModule !== module.id
|
'hover:bg-secondary/50': activeModule !== module.id
|
||||||
}"
|
}"
|
||||||
@click="emit('change', module.id)"
|
@click="emit('change', module.id)"
|
||||||
>
|
>
|
||||||
<component :is="getModuleIcon(module.icon)" class="h-5 w-5" />
|
<component :is="getModuleIcon(module.icon)" class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" class="w-fit">
|
<TooltipContent side="right" class="w-fit">
|
||||||
@@ -55,14 +55,14 @@ const displayModules = computed(() => {
|
|||||||
<Button
|
<Button
|
||||||
:variant="activeModule === 'settings' ? 'default' : 'ghost'"
|
:variant="activeModule === 'settings' ? 'default' : 'ghost'"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-10 w-10 rounded-lg transition-[background-color,color,box-shadow] duration-300"
|
class="size-10 rounded-lg transition-[background-color,color,box-shadow] duration-300"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-primary text-primary-foreground shadow-md': activeModule === 'settings',
|
'bg-primary text-primary-foreground shadow-md': activeModule === 'settings',
|
||||||
'hover:bg-secondary/50': activeModule !== 'settings'
|
'hover:bg-secondary/50': activeModule !== 'settings'
|
||||||
}"
|
}"
|
||||||
@click="emit('change', 'settings')"
|
@click="emit('change', 'settings')"
|
||||||
>
|
>
|
||||||
<Settings class="h-5 w-5" />
|
<Settings class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" class="w-fit">
|
<TooltipContent side="right" class="w-fit">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { Search, Settings, ChevronRight, ArrowUp, Check, Loader2 } from '@lucide/vue'
|
import { Search, Settings, ChevronRight, ArrowUp, Check, Loader2 } from '@lucide/vue'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { useSearchStore, type SearchItem } from '@/stores/searchStore'
|
import { useSearchStore, type SearchItem } from '@/stores/searchStore'
|
||||||
@@ -257,26 +258,29 @@ const handleBlur = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition name="floating-tabs">
|
<Transition name="floating-tabs">
|
||||||
|
<Tooltip v-if="showScrollTop">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
v-if="showScrollTop"
|
class="flex items-center justify-center size-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors"
|
||||||
class="flex items-center justify-center h-6 w-6 rounded-md text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors"
|
|
||||||
title="回到顶部"
|
|
||||||
@click="scrollToTop"
|
@click="scrollToTop"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
<ArrowUp class="h-3.5 w-3.5" />
|
<ArrowUp class="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>回到顶部</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
<!-- 保存设置按钮(当前模块在设置 tab 且注册了保存函数时显示,从右向左弹出) -->
|
<!-- 保存设置按钮(当前模块在设置 tab 且注册了保存函数时显示,从右向左弹出) -->
|
||||||
<Transition name="save-btn">
|
<Transition name="save-btn">
|
||||||
|
<Tooltip v-if="tabsStore.saveVisible">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
v-if="tabsStore.saveVisible"
|
|
||||||
class="flex items-center gap-1.5 h-7 px-3 mr-1 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-60 disabled:pointer-events-none transition-colors pointer-events-auto"
|
class="flex items-center gap-1.5 h-7 px-3 mr-1 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-60 disabled:pointer-events-none transition-colors pointer-events-auto"
|
||||||
:disabled="tabsStore.saving"
|
:disabled="tabsStore.saving"
|
||||||
title="保存当前模块设置"
|
|
||||||
@click="tabsStore.runSave"
|
@click="tabsStore.runSave"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
@@ -284,10 +288,13 @@ const handleBlur = () => {
|
|||||||
<Check v-else class="size-3.5" />
|
<Check v-else class="size-3.5" />
|
||||||
保存设置
|
保存设置
|
||||||
</button>
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>保存当前模块设置</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div class="relative max-w-xs mr-3 pointer-events-auto">
|
<div class="relative max-w-xs mr-3 pointer-events-auto">
|
||||||
<Search
|
<Search
|
||||||
class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground"
|
class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
@@ -325,14 +332,14 @@ const handleBlur = () => {
|
|||||||
class="w-full px-3 py-2 text-left text-sm hover:bg-accent transition-colors flex items-center gap-2"
|
class="w-full px-3 py-2 text-left text-sm hover:bg-accent transition-colors flex items-center gap-2"
|
||||||
@click="handleSettingSelect(item)"
|
@click="handleSettingSelect(item)"
|
||||||
>
|
>
|
||||||
<Settings class="h-4 w-4 text-muted-foreground" />
|
<Settings class="size-4 text-muted-foreground" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<span class="truncate">{{ item.title }}</span>
|
<span class="truncate">{{ item.title }}</span>
|
||||||
<span v-if="item.description" class="block text-xs text-muted-foreground truncate">
|
<span v-if="item.description" class="block text-xs text-muted-foreground truncate">
|
||||||
{{ item.description }}
|
{{ item.description }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight class="h-4 w-4 text-muted-foreground" />
|
<ChevronRight class="size-4 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -347,7 +354,7 @@ const handleBlur = () => {
|
|||||||
|
|
||||||
<div class="flex items-center pointer-events-auto" :class="{ 'hover-suppressed': hoverSuppressed }">
|
<div class="flex items-center pointer-events-auto" :class="{ 'hover-suppressed': hoverSuppressed }">
|
||||||
<button
|
<button
|
||||||
class="h-10 w-10 flex items-center justify-center hover:bg-foreground/5 transition-colors rounded-sm"
|
class="size-10 flex items-center justify-center hover:bg-foreground/5 transition-colors rounded-sm"
|
||||||
@click="minimize"
|
@click="minimize"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
@@ -358,7 +365,7 @@ const handleBlur = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="titlebar-maximize"
|
id="titlebar-maximize"
|
||||||
class="h-10 w-10 flex items-center justify-center transition-colors rounded-sm titlebar-maximize-btn"
|
class="size-10 flex items-center justify-center transition-colors rounded-sm titlebar-maximize-btn"
|
||||||
@click="maximize"
|
@click="maximize"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
@@ -375,7 +382,7 @@ const handleBlur = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="h-10 w-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-foreground transition-colors rounded-sm"
|
class="size-10 flex items-center justify-center hover:bg-red-500 hover:text-white text-foreground transition-colors rounded-sm"
|
||||||
@click="close"
|
@click="close"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
data-slot="switch"
|
data-slot="switch"
|
||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50',
|
'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer hover:data-[state=checked]:bg-primary/90 hover:data-[state=unchecked]:bg-input/70 active:scale-95',
|
||||||
props.class,
|
props.class,
|
||||||
)"
|
)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
<TooltipContent
|
<TooltipContent
|
||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
v-bind="{ ...forwarded, ...$attrs }"
|
v-bind="{ ...forwarded, ...$attrs }"
|
||||||
:class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)"
|
:class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit max-w-[calc(100vw-2rem)] rounded-md px-3 py-1.5 text-xs', props.class)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ export const commands = {
|
|||||||
screenshotRegisterShortcut: (shortcut: string) => __TAURI_INVOKE<null>("screenshot_register_shortcut", { shortcut }),
|
screenshotRegisterShortcut: (shortcut: string) => __TAURI_INVOKE<null>("screenshot_register_shortcut", { shortcut }),
|
||||||
/** 注销截图全局快捷键 */
|
/** 注销截图全局快捷键 */
|
||||||
screenshotUnregisterShortcut: () => __TAURI_INVOKE<null>("screenshot_unregister_shortcut"),
|
screenshotUnregisterShortcut: () => __TAURI_INVOKE<null>("screenshot_unregister_shortcut"),
|
||||||
|
/**
|
||||||
|
* 注册(或切换)贴图全局快捷键。传入空字符串则禁用快捷键。
|
||||||
|
* 按下时 emit 'screenshot-pin-shortcut',由前端切换贴图窗口显示/隐藏。
|
||||||
|
*/
|
||||||
|
screenshotRegisterPinShortcut: (shortcut: string) => __TAURI_INVOKE<null>("screenshot_register_pin_shortcut", { shortcut }),
|
||||||
|
/** 注销贴图全局快捷键 */
|
||||||
|
screenshotUnregisterPinShortcut: () => __TAURI_INVOKE<null>("screenshot_unregister_pin_shortcut"),
|
||||||
/** 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码 */
|
/** 捕获整个虚拟屏(多显示器拼接)存入静态,不做 PNG 编码 */
|
||||||
screenshotCaptureFullscreen: () => __TAURI_INVOKE<null>("screenshot_capture_fullscreen"),
|
screenshotCaptureFullscreen: () => __TAURI_INVOKE<null>("screenshot_capture_fullscreen"),
|
||||||
/** 全屏捕获编码为 PNG base64 并清除(全屏截图直接进编辑器) */
|
/** 全屏捕获编码为 PNG base64 并清除(全屏截图直接进编辑器) */
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const WINDOWS = {
|
|||||||
main: 'main',
|
main: 'main',
|
||||||
osdOverlay: 'osd-overlay',
|
osdOverlay: 'osd-overlay',
|
||||||
screenshotOverlay: 'screenshot-overlay',
|
screenshotOverlay: 'screenshot-overlay',
|
||||||
|
screenshotPin: 'screenshot-pin',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** Tauri 事件名(前端 emit / listen 与 Rust constants::events 对应) */
|
/** Tauri 事件名(前端 emit / listen 与 Rust constants::events 对应) */
|
||||||
@@ -31,6 +32,9 @@ export const EVENTS = {
|
|||||||
screenshotBegin: 'screenshot-begin',
|
screenshotBegin: 'screenshot-begin',
|
||||||
screenshotOverlayReady: 'screenshot-overlay-ready',
|
screenshotOverlayReady: 'screenshot-overlay-ready',
|
||||||
screenshotShortcut: 'screenshot-shortcut',
|
screenshotShortcut: 'screenshot-shortcut',
|
||||||
|
screenshotPinShortcut: 'screenshot-pin-shortcut',
|
||||||
|
screenshotPinReady: 'screenshot-pin-ready',
|
||||||
|
screenshotPinShow: 'screenshot-pin-show',
|
||||||
screenshotExported: 'screenshot-exported',
|
screenshotExported: 'screenshot-exported',
|
||||||
// 内核安装进度
|
// 内核安装进度
|
||||||
kernelInstallProgress: 'kernel-install-progress',
|
kernelInstallProgress: 'kernel-install-progress',
|
||||||
@@ -62,4 +66,6 @@ export const STORAGE_KEYS = {
|
|||||||
quickpanelHistoryItems: 'thing_quickpanel_history_items',
|
quickpanelHistoryItems: 'thing_quickpanel_history_items',
|
||||||
currencyRates: 'thing_quickpanel_currency_rates',
|
currencyRates: 'thing_quickpanel_currency_rates',
|
||||||
monitorOsdConfig: 'thing_monitor_osd_config',
|
monitorOsdConfig: 'thing_monitor_osd_config',
|
||||||
|
screenshotHistory: 'thing_screenshot_history',
|
||||||
|
screenshotPinIndex: 'thing_screenshot_pin_index',
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
+5
-1
@@ -26,6 +26,7 @@ const standaloneWindowApps: Array<[hash: string, label: string, loader: () => Pr
|
|||||||
['#tray-menu', '托盘菜单', () => import('./modules/tray/TrayMenu.vue')],
|
['#tray-menu', '托盘菜单', () => import('./modules/tray/TrayMenu.vue')],
|
||||||
['#screenshot-overlay', '截图覆盖层', () => import('./modules/screenshot/ScreenshotOverlay.vue')],
|
['#screenshot-overlay', '截图覆盖层', () => import('./modules/screenshot/ScreenshotOverlay.vue')],
|
||||||
['#screenshot-editor', '截图编辑器', () => import('./modules/screenshot/ScreenshotEditor.vue')],
|
['#screenshot-editor', '截图编辑器', () => import('./modules/screenshot/ScreenshotEditor.vue')],
|
||||||
|
['#screenshot-pin', '贴图窗口', () => import('./modules/screenshot/ScreenshotPin.vue')],
|
||||||
]
|
]
|
||||||
|
|
||||||
const winHash = window.location.hash
|
const winHash = window.location.hash
|
||||||
@@ -77,14 +78,17 @@ if (matched) {
|
|||||||
useProcessStore().initListener().catch(e => console.error('Process listener init error:', e))
|
useProcessStore().initListener().catch(e => console.error('Process listener init error:', e))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 截图模块:加载设置 + 预创建常驻覆盖层窗口 + 监听导出事件(历史/自动保存)+ 注册并监听全局快捷键
|
// 截图模块:加载设置 + 恢复持久化历史 + 预创建常驻覆盖层窗口 + 监听导出事件(历史/自动保存)+ 注册并监听全局快捷键(截图/贴图)
|
||||||
void import('./stores/screenshotStore').then(({ useScreenshotStore }) => {
|
void import('./stores/screenshotStore').then(({ useScreenshotStore }) => {
|
||||||
const screenshotStore = useScreenshotStore()
|
const screenshotStore = useScreenshotStore()
|
||||||
screenshotStore.loadSettings()
|
screenshotStore.loadSettings()
|
||||||
|
screenshotStore.loadHistory()
|
||||||
screenshotStore.initOverlay().catch(e => console.error('Screenshot overlay init error:', e))
|
screenshotStore.initOverlay().catch(e => console.error('Screenshot overlay init error:', e))
|
||||||
screenshotStore.initExportListener().catch(e => console.error('Screenshot export listener init error:', e))
|
screenshotStore.initExportListener().catch(e => console.error('Screenshot export listener init error:', e))
|
||||||
screenshotStore.initShortcutListener().catch(e => console.error('Screenshot shortcut listener init error:', e))
|
screenshotStore.initShortcutListener().catch(e => console.error('Screenshot shortcut listener init error:', e))
|
||||||
screenshotStore.initShortcutRegistration().catch(e => console.error('Screenshot shortcut registration init error:', e))
|
screenshotStore.initShortcutRegistration().catch(e => console.error('Screenshot shortcut registration init error:', e))
|
||||||
|
screenshotStore.initPinShortcutListener().catch(e => console.error('Screenshot pin shortcut listener init error:', e))
|
||||||
|
screenshotStore.initPinShortcutRegistration().catch(e => console.error('Screenshot pin shortcut registration init error:', e))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
@@ -308,7 +309,7 @@ onUnmounted(() => {
|
|||||||
<TabsContent value="history" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate">
|
<TabsContent value="history" class="flex-1 min-h-0 flex flex-col mt-4 tab-animate">
|
||||||
<div class="flex items-center gap-2 mb-3 shrink-0">
|
<div class="flex items-center gap-2 mb-3 shrink-0">
|
||||||
<div class="relative flex-1 max-w-sm">
|
<div class="relative flex-1 max-w-sm">
|
||||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
<Input v-model="searchQuery" placeholder="搜索历史..." class="pl-8" />
|
<Input v-model="searchQuery" placeholder="搜索历史..." class="pl-8" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@@ -327,7 +328,7 @@ onUnmounted(() => {
|
|||||||
{{ store.historyTotal }} 条
|
{{ store.historyTotal }} 条
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button variant="outline" size="sm" @click="clearOpen = true" :disabled="!historyList.length">
|
<Button variant="outline" size="sm" @click="clearOpen = true" :disabled="!historyList.length">
|
||||||
<Eraser class="h-4 w-4 mr-1" />清空
|
<Eraser class="size-4 mr-1" />清空
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -372,7 +373,7 @@ onUnmounted(() => {
|
|||||||
v-if="!historyList.length"
|
v-if="!historyList.length"
|
||||||
class="flex flex-col items-center justify-center text-muted-foreground py-12"
|
class="flex flex-col items-center justify-center text-muted-foreground py-12"
|
||||||
>
|
>
|
||||||
<ClipboardList class="h-12 w-12 mb-3 opacity-40" />
|
<ClipboardList class="size-12 mb-3 opacity-40" />
|
||||||
<p class="text-sm">暂无历史记录,复制内容后将自动收录</p>
|
<p class="text-sm">暂无历史记录,复制内容后将自动收录</p>
|
||||||
</div>
|
</div>
|
||||||
<Card
|
<Card
|
||||||
@@ -381,9 +382,9 @@ onUnmounted(() => {
|
|||||||
class="group hover:shadow-md transition-shadow py-0"
|
class="group hover:shadow-md transition-shadow py-0"
|
||||||
>
|
>
|
||||||
<CardContent class="flex items-center gap-3 px-3 py-2">
|
<CardContent class="flex items-center gap-3 px-3 py-2">
|
||||||
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-muted-foreground shrink-0" />
|
<component :is="kindIcon(item.kind)" class="size-4 text-muted-foreground shrink-0" />
|
||||||
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
|
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
|
||||||
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
|
<p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
|
||||||
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
||||||
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
|
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
|
||||||
<span>{{ formatTime(item.createdAt) }}</span>
|
<span>{{ formatTime(item.createdAt) }}</span>
|
||||||
@@ -391,15 +392,30 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)">
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-7" @click.stop="handleCopy(item)">
|
||||||
<Copy class="h-3.5 w-3.5" />
|
<Copy class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" :title="item.pinned ? '取消固定' : '固定'" @click.stop="handlePin(item)">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>复制</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-7" @click.stop="handlePin(item)">
|
||||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ item.pinned ? '取消固定' : '固定' }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" @click.stop="handleDelete(item)">
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<Trash2 class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>删除</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -414,15 +430,15 @@ onUnmounted(() => {
|
|||||||
v-if="!pinnedList.length"
|
v-if="!pinnedList.length"
|
||||||
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
class="flex flex-col items-center justify-center h-full text-muted-foreground"
|
||||||
>
|
>
|
||||||
<Pin class="h-12 w-12 mb-3 opacity-40" />
|
<Pin class="size-12 mb-3 opacity-40" />
|
||||||
<p class="text-sm">暂无固定条目</p>
|
<p class="text-sm">暂无固定条目</p>
|
||||||
<p class="text-xs mt-1">鼠标悬停历史条目,点击图钉按钮即可固定</p>
|
<p class="text-xs mt-1">鼠标悬停历史条目,点击图钉按钮即可固定</p>
|
||||||
</div>
|
</div>
|
||||||
<Card v-for="item in pinnedList" :key="item.id" class="group hover:shadow-md transition-shadow py-0">
|
<Card v-for="item in pinnedList" :key="item.id" class="group hover:shadow-md transition-shadow py-0">
|
||||||
<CardContent class="flex items-center gap-3 px-3 py-2">
|
<CardContent class="flex items-center gap-3 px-3 py-2">
|
||||||
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-primary shrink-0" />
|
<component :is="kindIcon(item.kind)" class="size-4 text-primary shrink-0" />
|
||||||
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
|
<div class="flex-1 min-w-0 cursor-pointer" @click="openDetail(item)">
|
||||||
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
|
<p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
|
||||||
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
||||||
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
|
<Badge variant="outline" :class="['px-1.5 py-0 text-[10px] border', kindBadgeClass(item.kind)]">{{ kindLabel(item.kind) }}</Badge>
|
||||||
<span>{{ formatTime(item.createdAt) }}</span>
|
<span>{{ formatTime(item.createdAt) }}</span>
|
||||||
@@ -430,15 +446,30 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
<div class="flex items-center gap-0.5 shrink-0 opacity-0 group-hover:opacity-100 transition">
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" title="复制" @click.stop="handleCopy(item)">
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-7" @click.stop="handleCopy(item)">
|
||||||
<Copy class="h-3.5 w-3.5" />
|
<Copy class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" title="取消固定" @click.stop="handlePin(item)">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>复制</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-7" @click.stop="handlePin(item)">
|
||||||
<PinOff class="h-3.5 w-3.5" />
|
<PinOff class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7 hover:text-destructive" title="删除" @click.stop="handleDelete(item)">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>取消固定</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="size-7 hover:text-destructive" @click.stop="handleDelete(item)">
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<Trash2 class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>删除</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -450,7 +481,7 @@ onUnmounted(() => {
|
|||||||
<div class="max-w-xl space-y-4">
|
<div class="max-w-xl space-y-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h3 class="text-base font-medium flex items-center gap-2">
|
<h3 class="text-base font-medium flex items-center gap-2">
|
||||||
<SettingsIcon class="h-4 w-4" />基本设置
|
<SettingsIcon class="size-4" />基本设置
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -519,7 +550,7 @@ onUnmounted(() => {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Label class="flex items-center gap-2">
|
<Label class="flex items-center gap-2">
|
||||||
<Keyboard class="h-4 w-4" />快捷弹窗快捷键
|
<Keyboard class="size-4" />快捷弹窗快捷键
|
||||||
</Label>
|
</Label>
|
||||||
<p class="text-xs text-muted-foreground mt-1">全局快捷键触发鼠标位置历史弹窗,留空禁用</p>
|
<p class="text-xs text-muted-foreground mt-1">全局快捷键触发鼠标位置历史弹窗,留空禁用</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -563,7 +594,7 @@ onUnmounted(() => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div class="flex-1 min-h-0 overflow-auto">
|
<div class="flex-1 min-h-0 overflow-auto">
|
||||||
<div v-if="detailLoading" class="flex items-center justify-center py-12">
|
<div v-if="detailLoading" class="flex items-center justify-center py-12">
|
||||||
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
|
<Loader2 class="size-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import {
|
import {
|
||||||
Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
|
Pagination, PaginationContent, PaginationItem, PaginationEllipsis,
|
||||||
} from '@/components/ui/pagination'
|
} from '@/components/ui/pagination'
|
||||||
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
// ===== 与 Rust 端对应的数据结构(bindings 提供,camelCase) =====
|
// ===== 与 Rust 端对应的数据结构(bindings 提供,camelCase) =====
|
||||||
// kind 为 bindings 生成的 string,前端按字符串比较即可
|
// kind 为 bindings 生成的 string,前端按字符串比较即可
|
||||||
@@ -363,10 +364,11 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="popup-root flex flex-col h-screen w-screen" @keydown="onKeydown">
|
<div class="popup-root flex flex-col h-screen w-screen" @keydown="onKeydown">
|
||||||
|
<TooltipProvider>
|
||||||
<!-- 搜索栏(与剪切板主页统一样式) -->
|
<!-- 搜索栏(与剪切板主页统一样式) -->
|
||||||
<div class="flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
<div class="flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
||||||
<div class="relative flex-1 max-w-sm">
|
<div class="relative flex-1 max-w-sm">
|
||||||
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search class="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
@@ -389,10 +391,10 @@ onUnmounted(() => {
|
|||||||
<ScrollArea class="popup-list flex-1 min-h-0">
|
<ScrollArea class="popup-list flex-1 min-h-0">
|
||||||
<div class="space-y-1.5 p-2">
|
<div class="space-y-1.5 p-2">
|
||||||
<div v-if="loading && !hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
<div v-if="loading && !hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
<Loader2 class="h-6 w-6 animate-spin" />
|
<Loader2 class="size-6 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
<div v-else-if="!hasItems" class="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
<ClipboardList class="h-10 w-10 mb-2 opacity-40" />
|
<ClipboardList class="size-10 mb-2 opacity-40" />
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
{{ searchQuery ? '无匹配结果' : '暂无历史记录' }}
|
{{ searchQuery ? '无匹配结果' : '暂无历史记录' }}
|
||||||
</p>
|
</p>
|
||||||
@@ -406,21 +408,31 @@ onUnmounted(() => {
|
|||||||
@mouseenter="onItemHover(idx, item)"
|
@mouseenter="onItemHover(idx, item)"
|
||||||
@mouseleave="onItemLeave"
|
@mouseleave="onItemLeave"
|
||||||
>
|
>
|
||||||
<component :is="kindIcon(item.kind)" class="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
<component :is="kindIcon(item.kind)" class="size-4 text-muted-foreground shrink-0 mt-0.5" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm break-all line-clamp-1" :title="item.preview">{{ item.preview }}</p>
|
<p class="text-sm break-all line-clamp-1">{{ item.preview }}</p>
|
||||||
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
<div class="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground flex-wrap">
|
||||||
<span class="popup-item-kind px-1.5 py-0 text-[10px] border rounded-sm" :class="kindBadgeClass(item.kind)">{{ kindLabel(item.kind) }}</span>
|
<span class="popup-item-kind px-1.5 py-0 text-[10px] border rounded-sm" :class="kindBadgeClass(item.kind)">{{ kindLabel(item.kind) }}</span>
|
||||||
<span>{{ formatTime(item.createdAt) }}</span>
|
<span>{{ formatTime(item.createdAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="popup-item-actions shrink-0">
|
<div class="popup-item-actions shrink-0">
|
||||||
<button class="popup-action-btn h-7 w-7" title="固定" @click="togglePin(item, $event)">
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<button class="popup-action-btn size-7" @click="togglePin(item, $event)">
|
||||||
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
<component :is="item.pinned ? PinOff : Pin" class="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button class="popup-action-btn h-7 w-7 hover:text-destructive" title="删除" @click="deleteItem(item, $event)">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>固定</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<button class="popup-action-btn size-7 hover:text-destructive" @click="deleteItem(item, $event)">
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<Trash2 class="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>删除</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -460,6 +472,7 @@ onUnmounted(() => {
|
|||||||
<span><kbd>Enter</kbd> 粘贴</span>
|
<span><kbd>Enter</kbd> 粘贴</span>
|
||||||
<span><kbd>Esc</kbd> 关闭</span>
|
<span><kbd>Esc</kbd> 关闭</span>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -616,15 +616,15 @@ const toggleSortOrder = () => {
|
|||||||
<div ref="tabsListRef">
|
<div ref="tabsListRef">
|
||||||
<TabsList class="grid w-full grid-cols-3 max-w-md !bg-transparent !p-0 !shadow-none">
|
<TabsList class="grid w-full grid-cols-3 max-w-md !bg-transparent !p-0 !shadow-none">
|
||||||
<TabsTrigger value="tasks" class="gap-1.5">
|
<TabsTrigger value="tasks" class="gap-1.5">
|
||||||
<ListChecks class="h-4 w-4" />
|
<ListChecks class="size-4" />
|
||||||
下载任务
|
下载任务
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="settings" class="gap-1.5">
|
<TabsTrigger value="settings" class="gap-1.5">
|
||||||
<SettingsIcon class="h-4 w-4" />
|
<SettingsIcon class="size-4" />
|
||||||
设置
|
设置
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="extension" class="gap-1.5">
|
<TabsTrigger value="extension" class="gap-1.5">
|
||||||
<Puzzle class="h-4 w-4" />
|
<Puzzle class="size-4" />
|
||||||
浏览器扩展
|
浏览器扩展
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -650,19 +650,19 @@ const toggleSortOrder = () => {
|
|||||||
<Separator orientation="vertical" class="h-5" />
|
<Separator orientation="vertical" class="h-5" />
|
||||||
<div v-if="running" class="flex items-center gap-2 text-xs">
|
<div v-if="running" class="flex items-center gap-2 text-xs">
|
||||||
<Badge variant="secondary" class="gap-1">
|
<Badge variant="secondary" class="gap-1">
|
||||||
<Download class="h-3 w-3" />
|
<Download class="size-3" />
|
||||||
下载中 {{ statusCounts.active }}
|
下载中 {{ statusCounts.active }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="secondary" class="gap-1">
|
<Badge variant="secondary" class="gap-1">
|
||||||
<Clock class="h-3 w-3" />
|
<Clock class="size-3" />
|
||||||
等待 {{ statusCounts.queued }}
|
等待 {{ statusCounts.queued }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-if="statusCounts.paused > 0" variant="secondary" class="gap-1">
|
<Badge v-if="statusCounts.paused > 0" variant="secondary" class="gap-1">
|
||||||
<Pause class="h-3 w-3" />
|
<Pause class="size-3" />
|
||||||
已暂停 {{ statusCounts.paused }}
|
已暂停 {{ statusCounts.paused }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="secondary" class="gap-1">
|
<Badge variant="secondary" class="gap-1">
|
||||||
<Check class="h-3 w-3" />
|
<Check class="size-3" />
|
||||||
已完成 {{ statusCounts.complete }}
|
已完成 {{ statusCounts.complete }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -673,7 +673,7 @@ const toggleSortOrder = () => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="store.refreshTasks()"
|
@click="store.refreshTasks()"
|
||||||
>
|
>
|
||||||
<RefreshCw class="h-4 w-4" />
|
<RefreshCw class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -683,20 +683,15 @@ const toggleSortOrder = () => {
|
|||||||
<!-- 工具栏:筛选 / 排序 / 搜索 -->
|
<!-- 工具栏:筛选 / 排序 / 搜索 -->
|
||||||
<div class="shrink-0 flex items-center gap-2 flex-wrap">
|
<div class="shrink-0 flex items-center gap-2 flex-wrap">
|
||||||
<!-- 新建下载按钮 -->
|
<!-- 新建下载按钮 -->
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-8 gap-1.5"
|
class="h-8 gap-1.5"
|
||||||
:disabled="addingTask"
|
:disabled="addingTask"
|
||||||
@click="addDialogOpen = true"
|
@click="addDialogOpen = true"
|
||||||
>
|
>
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="size-4" />
|
||||||
新建下载
|
新建下载
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>添加下载链接</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<!-- 状态筛选 -->
|
<!-- 状态筛选 -->
|
||||||
<Select v-model="statusFilter">
|
<Select v-model="statusFilter">
|
||||||
@@ -736,11 +731,11 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-8 w-8 p-0 bg-transparent"
|
class="size-8 p-0 bg-transparent"
|
||||||
@click="toggleSortOrder"
|
@click="toggleSortOrder"
|
||||||
>
|
>
|
||||||
<ArrowUp v-if="sortOrder === 'asc'" class="h-4 w-4" />
|
<ArrowUp v-if="sortOrder === 'asc'" class="size-4" />
|
||||||
<ArrowDown v-else class="h-4 w-4" />
|
<ArrowDown v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{{ sortOrder === 'asc' ? '升序(点击切换降序)' : '降序(点击切换升序)' }}</TooltipContent>
|
<TooltipContent>{{ sortOrder === 'asc' ? '升序(点击切换降序)' : '降序(点击切换升序)' }}</TooltipContent>
|
||||||
@@ -748,7 +743,7 @@ const toggleSortOrder = () => {
|
|||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div class="relative flex-1 min-w-[180px] max-w-xs">
|
<div class="relative flex-1 min-w-[180px] max-w-xs">
|
||||||
<Search class="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search class="absolute left-2 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
placeholder="搜索文件名..."
|
placeholder="搜索文件名..."
|
||||||
@@ -756,20 +751,15 @@ const toggleSortOrder = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-8 gap-1.5 bg-transparent"
|
class="h-8 gap-1.5 bg-transparent"
|
||||||
@click="downloadSettingsOpen = true"
|
@click="downloadSettingsOpen = true"
|
||||||
>
|
>
|
||||||
<SettingsIcon class="h-4 w-4" />
|
<SettingsIcon class="size-4" />
|
||||||
下载设置
|
下载设置
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>下载设置</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<span class="text-xs text-muted-foreground ml-auto">
|
<span class="text-xs text-muted-foreground ml-auto">
|
||||||
共 {{ sortedTasks.length }} 个任务
|
共 {{ sortedTasks.length }} 个任务
|
||||||
@@ -779,10 +769,10 @@ const toggleSortOrder = () => {
|
|||||||
<!-- 任务列表 -->
|
<!-- 任务列表 -->
|
||||||
<ScrollArea class="flex-1 min-h-0">
|
<ScrollArea class="flex-1 min-h-0">
|
||||||
<div v-if="pagedTasks.length === 0" key="empty" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10">
|
<div v-if="pagedTasks.length === 0" key="empty" class="h-full flex flex-col items-center justify-center text-muted-foreground gap-2 pt-10">
|
||||||
<CheckCircle2 class="h-16 w-16 opacity-30" />
|
<CheckCircle2 class="size-16 opacity-30" />
|
||||||
<p>暂无符合条件的任务</p>
|
<p>暂无符合条件的任务</p>
|
||||||
<Button size="sm" class="mt-2" @click="addDialogOpen = true">
|
<Button size="sm" class="mt-2" @click="addDialogOpen = true">
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="size-4" />
|
||||||
新建下载
|
新建下载
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -792,19 +782,14 @@ const toggleSortOrder = () => {
|
|||||||
<div class="flex items-start justify-between gap-3 mb-3">
|
<div class="flex items-start justify-between gap-3 mb-3">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger as-child>
|
|
||||||
<span class="font-medium truncate cursor-default">
|
<span class="font-medium truncate cursor-default">
|
||||||
{{ getFileName(task) }}
|
{{ getFileName(task) }}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>{{ getFileName(task) }}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Badge
|
<Badge
|
||||||
:variant="getTaskStatusBadge(task).variant"
|
:variant="getTaskStatusBadge(task).variant"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
>
|
>
|
||||||
<component :is="getTaskStatusBadge(task).icon" class="h-3 w-3" />
|
<component :is="getTaskStatusBadge(task).icon" class="size-3" />
|
||||||
{{ getTaskStatusBadge(task).text }}
|
{{ getTaskStatusBadge(task).text }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -813,7 +798,7 @@ const toggleSortOrder = () => {
|
|||||||
{{ formatSize(task.completedSize) }} / {{ formatSize(task.totalSize) }}
|
{{ formatSize(task.completedSize) }} / {{ formatSize(task.totalSize) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="task.status === 'active'" class="flex items-center gap-1 text-green-600 dark:text-green-400">
|
<span v-if="task.status === 'active'" class="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||||
<Download class="h-3 w-3" />
|
<Download class="size-3" />
|
||||||
{{ formatSpeed(task.speed) }}
|
{{ formatSpeed(task.speed) }}
|
||||||
</span>
|
</span>
|
||||||
<Badge v-if="task.status === 'active' && task.segments.length > 1" variant="outline" class="gap-1 py-0 px-1.5 text-[10px]">
|
<Badge v-if="task.status === 'active' && task.segments.length > 1" variant="outline" class="gap-1 py-0 px-1.5 text-[10px]">
|
||||||
@@ -826,7 +811,7 @@ const toggleSortOrder = () => {
|
|||||||
{{ truncatePath(task.dir) }}
|
{{ truncatePath(task.dir) }}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent class="max-w-[400px] break-all">{{ task.dir }}</TooltipContent>
|
<TooltipContent class="max-w-[480px] break-words">{{ task.dir }}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span v-if="task.error" class="text-destructive">
|
<span v-if="task.error" class="text-destructive">
|
||||||
{{ task.error }}
|
{{ task.error }}
|
||||||
@@ -839,10 +824,10 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
@click="handlePause(task.id)"
|
@click="handlePause(task.id)"
|
||||||
>
|
>
|
||||||
<Pause class="h-4 w-4" />
|
<Pause class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{{ task.status === 'queued' ? '取消排队' : '暂停' }}</TooltipContent>
|
<TooltipContent>{{ task.status === 'queued' ? '取消排队' : '暂停' }}</TooltipContent>
|
||||||
@@ -852,10 +837,10 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
@click="handleResume(task.id)"
|
@click="handleResume(task.id)"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4" />
|
<Play class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>继续</TooltipContent>
|
<TooltipContent>继续</TooltipContent>
|
||||||
@@ -865,10 +850,10 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
@click="handleShowDetail(task)"
|
@click="handleShowDetail(task)"
|
||||||
>
|
>
|
||||||
<Info class="h-4 w-4" />
|
<Info class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>详细信息</TooltipContent>
|
<TooltipContent>详细信息</TooltipContent>
|
||||||
@@ -878,10 +863,10 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
@click="handleOpenDir(task)"
|
@click="handleOpenDir(task)"
|
||||||
>
|
>
|
||||||
<FolderOpen class="h-4 w-4" />
|
<FolderOpen class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>打开目录</TooltipContent>
|
<TooltipContent>打开目录</TooltipContent>
|
||||||
@@ -891,10 +876,10 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8 text-destructive hover:text-destructive"
|
class="size-8 text-destructive hover:text-destructive"
|
||||||
@click="handleRemove(task)"
|
@click="handleRemove(task)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-4 w-4" />
|
<Trash2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>删除</TooltipContent>
|
<TooltipContent>删除</TooltipContent>
|
||||||
@@ -919,12 +904,12 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-8 w-8 p-0"
|
class="size-8 p-0"
|
||||||
:disabled="currentPage <= 1"
|
:disabled="currentPage <= 1"
|
||||||
@click="currentPage = 1"
|
@click="currentPage = 1"
|
||||||
>
|
>
|
||||||
<ChevronLeft class="h-4 w-4" />
|
<ChevronLeft class="size-4" />
|
||||||
<ChevronLeft class="h-4 w-4 -ml-2" />
|
<ChevronLeft class="size-4 -ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>第一页</TooltipContent>
|
<TooltipContent>第一页</TooltipContent>
|
||||||
@@ -934,11 +919,11 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-8 w-8 p-0"
|
class="size-8 p-0"
|
||||||
:disabled="currentPage <= 1"
|
:disabled="currentPage <= 1"
|
||||||
@click="currentPage--"
|
@click="currentPage--"
|
||||||
>
|
>
|
||||||
<ChevronLeft class="h-4 w-4" />
|
<ChevronLeft class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>上一页</TooltipContent>
|
<TooltipContent>上一页</TooltipContent>
|
||||||
@@ -951,11 +936,11 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-8 w-8 p-0"
|
class="size-8 p-0"
|
||||||
:disabled="currentPage >= totalPages"
|
:disabled="currentPage >= totalPages"
|
||||||
@click="currentPage++"
|
@click="currentPage++"
|
||||||
>
|
>
|
||||||
<ChevronRight class="h-4 w-4" />
|
<ChevronRight class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>下一页</TooltipContent>
|
<TooltipContent>下一页</TooltipContent>
|
||||||
@@ -965,12 +950,12 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-8 w-8 p-0"
|
class="size-8 p-0"
|
||||||
:disabled="currentPage >= totalPages"
|
:disabled="currentPage >= totalPages"
|
||||||
@click="currentPage = totalPages"
|
@click="currentPage = totalPages"
|
||||||
>
|
>
|
||||||
<ChevronRight class="h-4 w-4" />
|
<ChevronRight class="size-4" />
|
||||||
<ChevronRight class="h-4 w-4 -ml-2" />
|
<ChevronRight class="size-4 -ml-2" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>最后一页</TooltipContent>
|
<TooltipContent>最后一页</TooltipContent>
|
||||||
@@ -988,7 +973,7 @@ const toggleSortOrder = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
<FolderOpen class="h-4 w-4 text-primary" />
|
<FolderOpen class="size-4 text-primary" />
|
||||||
下载设置
|
下载设置
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -1004,7 +989,7 @@ const toggleSortOrder = () => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button variant="outline" size="icon" @click="handleSelectSettingsDir">
|
<Button variant="outline" size="icon" @click="handleSelectSettingsDir">
|
||||||
<FolderOpen class="h-4 w-4" />
|
<FolderOpen class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>选择目录</TooltipContent>
|
<TooltipContent>选择目录</TooltipContent>
|
||||||
@@ -1079,7 +1064,7 @@ const toggleSortOrder = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
<Zap class="h-4 w-4 text-primary" />
|
<Zap class="size-4 text-primary" />
|
||||||
扩展 API
|
扩展 API
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -1121,7 +1106,7 @@ const toggleSortOrder = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
<CardTitle class="flex items-center gap-2 text-base">
|
||||||
<Puzzle class="h-4 w-4 text-primary" />
|
<Puzzle class="size-4 text-primary" />
|
||||||
Thing Extension
|
Thing Extension
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -1142,7 +1127,7 @@ const toggleSortOrder = () => {
|
|||||||
{{ store.extensionInfo.url }}
|
{{ store.extensionInfo.url }}
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent class="max-w-[400px] break-all">{{ store.extensionInfo.url }}</TooltipContent>
|
<TooltipContent class="max-w-[480px] break-words">{{ store.extensionInfo.url }}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -1150,10 +1135,10 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8 shrink-0"
|
class="size-8 shrink-0"
|
||||||
@click="handleCopy(store.extensionInfo.url, 'API 地址')"
|
@click="handleCopy(store.extensionInfo.url, 'API 地址')"
|
||||||
>
|
>
|
||||||
<Copy class="h-4 w-4" />
|
<Copy class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>复制 API 地址</TooltipContent>
|
<TooltipContent>复制 API 地址</TooltipContent>
|
||||||
@@ -1173,11 +1158,11 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
@click="showSecret = !showSecret"
|
@click="showSecret = !showSecret"
|
||||||
>
|
>
|
||||||
<Eye v-if="!showSecret" class="h-4 w-4" />
|
<Eye v-if="!showSecret" class="size-4" />
|
||||||
<EyeOff v-else class="h-4 w-4" />
|
<EyeOff v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{{ showSecret ? '隐藏' : '显示' }}</TooltipContent>
|
<TooltipContent>{{ showSecret ? '隐藏' : '显示' }}</TooltipContent>
|
||||||
@@ -1187,10 +1172,10 @@ const toggleSortOrder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
@click="handleCopy(store.extensionInfo.secret, '密钥')"
|
@click="handleCopy(store.extensionInfo.secret, '密钥')"
|
||||||
>
|
>
|
||||||
<Copy class="h-4 w-4" />
|
<Copy class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>复制密钥</TooltipContent>
|
<TooltipContent>复制密钥</TooltipContent>
|
||||||
@@ -1205,7 +1190,7 @@ const toggleSortOrder = () => {
|
|||||||
<Label class="text-xs text-muted-foreground">安装扩展</Label>
|
<Label class="text-xs text-muted-foreground">安装扩展</Label>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Button variant="outline" @click="handleInstallExtensionOnline">
|
<Button variant="outline" @click="handleInstallExtensionOnline">
|
||||||
<ExternalLink class="h-4 w-4" />
|
<ExternalLink class="size-4" />
|
||||||
在线安装
|
在线安装
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1235,7 +1220,7 @@ const toggleSortOrder = () => {
|
|||||||
<DialogContent class="max-w-lg">
|
<DialogContent class="max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle class="flex items-center gap-2">
|
<DialogTitle class="flex items-center gap-2">
|
||||||
<SettingsIcon class="h-4 w-4 text-primary" />
|
<SettingsIcon class="size-4 text-primary" />
|
||||||
下载设置
|
下载设置
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription class="text-xs">
|
<DialogDescription class="text-xs">
|
||||||
@@ -1256,7 +1241,7 @@ const toggleSortOrder = () => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button variant="outline" size="icon" @click="handleSelectSettingsDir">
|
<Button variant="outline" size="icon" @click="handleSelectSettingsDir">
|
||||||
<FolderOpen class="h-4 w-4" />
|
<FolderOpen class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>选择目录</TooltipContent>
|
<TooltipContent>选择目录</TooltipContent>
|
||||||
@@ -1328,7 +1313,7 @@ const toggleSortOrder = () => {
|
|||||||
<Button variant="outline">取消</Button>
|
<Button variant="outline">取消</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button @click="handleDialogSave">
|
<Button @click="handleDialogSave">
|
||||||
<Check class="h-4 w-4" />
|
<Check class="size-4" />
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -1340,7 +1325,7 @@ const toggleSortOrder = () => {
|
|||||||
<DialogContent class="max-w-lg">
|
<DialogContent class="max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle class="flex items-center gap-2">
|
<DialogTitle class="flex items-center gap-2">
|
||||||
<Link2 class="h-4 w-4 text-primary" />
|
<Link2 class="size-4 text-primary" />
|
||||||
新建下载
|
新建下载
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription class="text-xs">
|
<DialogDescription class="text-xs">
|
||||||
@@ -1371,7 +1356,7 @@ const toggleSortOrder = () => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button variant="outline" size="icon" @click="handleSelectDir">
|
<Button variant="outline" size="icon" @click="handleSelectDir">
|
||||||
<FolderOpen class="h-4 w-4" />
|
<FolderOpen class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>选择目录</TooltipContent>
|
<TooltipContent>选择目录</TooltipContent>
|
||||||
@@ -1389,8 +1374,8 @@ const toggleSortOrder = () => {
|
|||||||
<Button variant="outline">取消</Button>
|
<Button variant="outline">取消</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button :disabled="addingTask || !addUriText.trim()" @click="handleAddDownload">
|
<Button :disabled="addingTask || !addUriText.trim()" @click="handleAddDownload">
|
||||||
<Loader2 v-if="addingTask" class="h-4 w-4 animate-spin" />
|
<Loader2 v-if="addingTask" class="size-4 animate-spin" />
|
||||||
<Plus v-else class="h-4 w-4" />
|
<Plus v-else class="size-4" />
|
||||||
添加下载
|
添加下载
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -1402,7 +1387,7 @@ const toggleSortOrder = () => {
|
|||||||
<AlertDialogContent class="max-w-md">
|
<AlertDialogContent class="max-w-md">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle class="flex items-center gap-2">
|
<AlertDialogTitle class="flex items-center gap-2">
|
||||||
<AlertCircle class="h-4 w-4 text-amber-500" />
|
<AlertCircle class="size-4 text-amber-500" />
|
||||||
下载重复提醒
|
下载重复提醒
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="text-xs">
|
<AlertDialogDescription class="text-xs">
|
||||||
@@ -1423,7 +1408,7 @@ const toggleSortOrder = () => {
|
|||||||
<AlertDialogCancel @click="onDuplicateCancel">取消全部</AlertDialogCancel>
|
<AlertDialogCancel @click="onDuplicateCancel">取消全部</AlertDialogCancel>
|
||||||
<Button variant="outline" @click="onDuplicateSkip">跳过此链接</Button>
|
<Button variant="outline" @click="onDuplicateSkip">跳过此链接</Button>
|
||||||
<AlertDialogAction @click="onDuplicateConfirm">
|
<AlertDialogAction @click="onDuplicateConfirm">
|
||||||
<Download class="h-4 w-4" />
|
<Download class="size-4" />
|
||||||
仍然下载
|
仍然下载
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@@ -1469,7 +1454,7 @@ const toggleSortOrder = () => {
|
|||||||
<DialogContent class="max-w-xl max-h-[80vh]">
|
<DialogContent class="max-w-xl max-h-[80vh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle class="flex items-center gap-2">
|
<DialogTitle class="flex items-center gap-2">
|
||||||
<Info class="h-4 w-4 text-primary" />
|
<Info class="size-4 text-primary" />
|
||||||
任务详情
|
任务详情
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription class="text-xs">
|
<DialogDescription class="text-xs">
|
||||||
@@ -1487,7 +1472,7 @@ const toggleSortOrder = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.filename, '文件名')">
|
<Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.filename, '文件名')">
|
||||||
<Copy class="h-3.5 w-3.5" />
|
<Copy class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -1501,7 +1486,7 @@ const toggleSortOrder = () => {
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-xs text-muted-foreground">状态</span>
|
<span class="text-xs text-muted-foreground">状态</span>
|
||||||
<Badge :variant="getTaskStatusBadge(detailTask).variant" class="gap-1">
|
<Badge :variant="getTaskStatusBadge(detailTask).variant" class="gap-1">
|
||||||
<component :is="getTaskStatusBadge(detailTask).icon" class="h-3 w-3" />
|
<component :is="getTaskStatusBadge(detailTask).icon" class="size-3" />
|
||||||
{{ getTaskStatusBadge(detailTask).text }}
|
{{ getTaskStatusBadge(detailTask).text }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
@@ -1514,7 +1499,7 @@ const toggleSortOrder = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.url, '下载链接')">
|
<Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.url, '下载链接')">
|
||||||
<Copy class="h-3.5 w-3.5" />
|
<Copy class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -1532,13 +1517,13 @@ const toggleSortOrder = () => {
|
|||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span class="font-mono text-xs break-all cursor-default">{{ detailTask.dir }}</span>
|
<span class="font-mono text-xs break-all cursor-default">{{ detailTask.dir }}</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent class="max-w-[400px] break-all">{{ detailTask.dir }}</TooltipContent>
|
<TooltipContent class="max-w-[480px] break-words">{{ detailTask.dir }}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span class="font-mono text-xs text-muted-foreground break-all">{{ detailTask.filename }}</span>
|
<span class="font-mono text-xs text-muted-foreground break-all">{{ detailTask.filename }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.dir + '\\' + detailTask.filename, '完整路径')">
|
<Button size="icon" variant="ghost" class="size-7 shrink-0" @click="handleCopyText(detailTask.dir + '\\' + detailTask.filename, '完整路径')">
|
||||||
<Copy class="h-3.5 w-3.5" />
|
<Copy class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -1643,7 +1628,7 @@ const toggleSortOrder = () => {
|
|||||||
<Button variant="outline">关闭</Button>
|
<Button variant="outline">关闭</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button v-if="detailTask?.dir" variant="outline" @click="handleOpenDir(detailTask)">
|
<Button v-if="detailTask?.dir" variant="outline" @click="handleOpenDir(detailTask)">
|
||||||
<FolderOpen class="h-4 w-4" />
|
<FolderOpen class="size-4" />
|
||||||
打开目录
|
打开目录
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -1145,7 +1145,12 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||||
<CardTitle class="flex items-center justify-between text-sm">
|
<CardTitle class="flex items-center justify-between text-sm">
|
||||||
<span class="flex items-center gap-1.5"><Cpu class="size-4 text-primary" />CPU</span>
|
<span class="flex items-center gap-1.5"><Cpu class="size-4 text-primary" />CPU</span>
|
||||||
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="cpuModel ?? ''">{{ cpuModel ?? '--' }}</span>
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<span class="text-xs text-muted-foreground font-normal truncate ml-2">{{ cpuModel ?? '--' }}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ cpuModel ?? '' }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
||||||
@@ -1178,7 +1183,12 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||||
<CardTitle class="flex items-center justify-between text-sm">
|
<CardTitle class="flex items-center justify-between text-sm">
|
||||||
<span class="flex items-center gap-1.5"><Gauge class="size-4 text-primary" />GPU</span>
|
<span class="flex items-center gap-1.5"><Gauge class="size-4 text-primary" />GPU</span>
|
||||||
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="gpuModel ?? ''">{{ gpuModel ?? '--' }}</span>
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<span class="text-xs text-muted-foreground font-normal truncate ml-2">{{ gpuModel ?? '--' }}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ gpuModel ?? '' }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
||||||
@@ -1211,9 +1221,14 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
<CardHeader class="pb-1.5 px-3.5 pt-2.5">
|
||||||
<CardTitle class="flex items-center justify-between text-sm">
|
<CardTitle class="flex items-center justify-between text-sm">
|
||||||
<span class="flex items-center gap-1.5"><MemoryStick class="size-4 text-primary" />内存</span>
|
<span class="flex items-center gap-1.5"><MemoryStick class="size-4 text-primary" />内存</span>
|
||||||
<span class="text-xs text-muted-foreground font-normal truncate ml-2" :title="memModuleModels.join(', ')">
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<span class="text-xs text-muted-foreground font-normal truncate ml-2">
|
||||||
{{ memTotalGB != null ? fmt(memTotalGB, 0) + ' GB' : '--' }}
|
{{ memTotalGB != null ? fmt(memTotalGB, 0) + ' GB' : '--' }}
|
||||||
</span>
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ memModuleModels.join(', ') }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
<CardContent class="px-3.5 pb-2.5 space-y-1.5">
|
||||||
@@ -1275,7 +1290,12 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
<div v-if="storageDrives.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
<div v-if="storageDrives.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||||
<div v-for="drive in storageDrives" :key="drive.name" class="border rounded-md p-2 space-y-1">
|
<div v-for="drive in storageDrives" :key="drive.name" class="border rounded-md p-2 space-y-1">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-xs font-medium truncate" :title="drive.name">{{ drive.name }}</span>
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<span class="text-xs font-medium truncate">{{ drive.name }}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ drive.name }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<span :class="['text-xs font-mono tabular-nums shrink-0', tempColor(drive.temp)]">{{ fmt(drive.temp, 0) }}°C</span>
|
<span :class="['text-xs font-mono tabular-nums shrink-0', tempColor(drive.temp)]">{{ fmt(drive.temp, 0) }}°C</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -1330,14 +1350,24 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
<Play class="size-3" />启动
|
<Play class="size-3" />启动
|
||||||
</Button>
|
</Button>
|
||||||
<!-- 提权按钮(标志未启用时显示:设置标志 + 以管理员权限重启 Thing) -->
|
<!-- 提权按钮(标志未启用时显示:设置标志 + 以管理员权限重启 Thing) -->
|
||||||
<Button v-if="!store.elevateOnLaunch" size="xs" variant="outline" class="gap-1 text-emerald-600 dark:text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10" :disabled="store.starting" title="以管理员权限重启 Thing(弹 UAC,ThingHK 子进程继承权限,后续启动自动提权,崩溃自动重启)" @click="handleElevateSelf">
|
<Tooltip v-if="!store.elevateOnLaunch">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button size="xs" variant="outline" class="gap-1 text-emerald-600 dark:text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10" :disabled="store.starting" @click="handleElevateSelf">
|
||||||
<Loader2 v-if="store.starting" class="size-3 animate-spin" />
|
<Loader2 v-if="store.starting" class="size-3 animate-spin" />
|
||||||
<ShieldCheck v-else class="size-3" />提权
|
<ShieldCheck v-else class="size-3" />提权
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent class="max-w-[480px] break-words">以管理员权限重启 Thing(弹 UAC,ThingHK 子进程继承权限,后续启动自动提权,崩溃自动重启)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<!-- 取消提权按钮(标志已启用时显示:清除标志,下次启动不触发 UAC) -->
|
<!-- 取消提权按钮(标志已启用时显示:清除标志,下次启动不触发 UAC) -->
|
||||||
<Button v-else size="xs" variant="outline" class="gap-1 text-muted-foreground hover:text-foreground" title="取消提权,下次启动将以普通权限运行(不影响当前会话)" @click="handleCancelElevation">
|
<Tooltip v-else>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button size="xs" variant="outline" class="gap-1 text-muted-foreground hover:text-foreground" @click="handleCancelElevation">
|
||||||
<ShieldOff class="size-3" />取消提权
|
<ShieldOff class="size-3" />取消提权
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent class="max-w-[480px] break-words">取消提权,下次启动将以普通权限运行(不影响当前会话)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleRefresh">
|
<Button size="xs" variant="outline" :disabled="store.starting || store.stopping || store.connState === 'idle'" @click="handleRefresh">
|
||||||
<RefreshCw class="size-3" />刷新
|
<RefreshCw class="size-3" />刷新
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1420,7 +1450,12 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
<div class="text-[11px] uppercase tracking-wide text-muted-foreground/70 mb-1">{{ typeLabel(typeGroup.type) }}</div>
|
<div class="text-[11px] uppercase tracking-wide text-muted-foreground/70 mb-1">{{ typeLabel(typeGroup.type) }}</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-0.5 text-xs">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-0.5 text-xs">
|
||||||
<div v-for="s in typeGroup.items" :key="s.id" class="flex justify-between items-center py-0.5">
|
<div v-for="s in typeGroup.items" :key="s.id" class="flex justify-between items-center py-0.5">
|
||||||
<span class="text-muted-foreground truncate pr-2" :title="s.name">{{ s.name }}</span>
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<span class="text-muted-foreground truncate pr-2">{{ s.name }}</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ s.name }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<span class="font-mono tabular-nums shrink-0" :class="{ 'text-muted-foreground/50': s.value == null }">
|
<span class="font-mono tabular-nums shrink-0" :class="{ 'text-muted-foreground/50': s.value == null }">
|
||||||
{{ s.value == null ? 'N/A' : s.value.toFixed(s.type === 'voltage' || s.type === 'power' ? 2 : 1) }}
|
{{ s.value == null ? 'N/A' : s.value.toFixed(s.type === 'voltage' || s.type === 'power' ? 2 : 1) }}
|
||||||
<span class="text-muted-foreground ml-0.5">{{ s.unit }}</span>
|
<span class="text-muted-foreground ml-0.5">{{ s.unit }}</span>
|
||||||
@@ -2074,7 +2109,18 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Kernel 由 ProcessManager 统一管理,应用启动时自动拉起,崩溃自动重启。
|
Kernel 由 ProcessManager 统一管理,崩溃自动重启。
|
||||||
|
</div>
|
||||||
|
<!-- 应用启动时自动启动 toggle(参照代理模块) -->
|
||||||
|
<div class="flex items-center justify-between rounded-md border p-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm">应用启动时自动启动监控内核</p>
|
||||||
|
<p class="text-xs text-muted-foreground">软件启动时自动运行 ThingHK 内核</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
:model-value="store.autoStart"
|
||||||
|
@update:model-value="(v: boolean) => store.setAutoStart(v)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -2108,7 +2154,7 @@ watch(() => store.status?.ready, (ready, prev) => {
|
|||||||
>{{ pathDisplay || pathShort }}</span>
|
>{{ pathDisplay || pathShort }}</span>
|
||||||
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
|
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent class="max-w-[400px] break-all">{{ store.kernelInfo.path ?? '' }}</TooltipContent>
|
<TooltipContent class="max-w-[480px] break-words">{{ store.kernelInfo.path ?? '' }}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<template v-if="store.kernelInfo.exists">
|
<template v-if="store.kernelInfo.exists">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -30,9 +30,12 @@ export const moduleConfig: ModuleConfig = {
|
|||||||
},
|
},
|
||||||
lifecycle: {
|
lifecycle: {
|
||||||
onEnable: async () => {
|
onEnable: async () => {
|
||||||
// 启用模块时拉起 Kernel 并开始 SSE 订阅
|
// 若用户在监控设置中开启了"自动启动",则随模块启用而运行 Kernel
|
||||||
try {
|
try {
|
||||||
|
const autoStart = await invoke<boolean>('monitor_get_auto_start')
|
||||||
|
if (autoStart) {
|
||||||
await invoke('monitor_start')
|
await invoke('monitor_start')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* 忽略:可能 Kernel 未安装 */
|
/* 忽略:可能 Kernel 未安装 */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,14 +345,26 @@ onMounted(() => {
|
|||||||
// 窗口/标签页不可见时暂停状态轮询,恢复可见后下个 tick 自动继续
|
// 窗口/标签页不可见时暂停状态轮询,恢复可见后下个 tick 自动继续
|
||||||
if (document.hidden) return
|
if (document.hidden) return
|
||||||
await store.refreshStatus()
|
await store.refreshStatus()
|
||||||
|
// 同步系统代理真实状态(注册表可能被外部改动,3s 周期足够感知)
|
||||||
|
await store.refreshSystemProxy()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
// 页面重新可见时立即刷新一次系统代理状态(切回标签页/从托盘返回主窗口)
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (statusTimer) clearInterval(statusTimer)
|
if (statusTimer) clearInterval(statusTimer)
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||||
stopAutoSwitch()
|
stopAutoSwitch()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 页面可见性变化时刷新系统代理状态(低成本感知外部修改) */
|
||||||
|
async function onVisibilityChange() {
|
||||||
|
if (!document.hidden) {
|
||||||
|
await store.refreshSystemProxy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(running, async (val, old) => {
|
watch(running, async (val, old) => {
|
||||||
if (val && !old) {
|
if (val && !old) {
|
||||||
await store.waitForApi()
|
await store.waitForApi()
|
||||||
@@ -978,13 +990,9 @@ tabsStore.registerSave(saveSettingsForm)
|
|||||||
<div class="flex items-center gap-1.5 min-w-0">
|
<div class="flex items-center gap-1.5 min-w-0">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span
|
<span class="font-mono text-xs text-right truncate cursor-default">{{ pathDisplay || pathShort }}</span>
|
||||||
v-if="store.kernel?.exists"
|
|
||||||
class="font-mono text-xs text-right truncate cursor-default"
|
|
||||||
>{{ pathDisplay || pathShort }}</span>
|
|
||||||
<span v-else class="font-mono text-xs text-right cursor-default">{{ pathShort }}</span>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent class="max-w-[400px] break-all">{{ store.kernel?.path ?? '' }}</TooltipContent>
|
<TooltipContent class="max-w-[480px] break-words">{{ store.kernel?.path ?? '' }}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<template v-if="store.kernel?.exists">
|
<template v-if="store.kernel?.exists">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export const moduleConfig: ModuleConfig = {
|
|||||||
const s = await commands.proxyGetSettings()
|
const s = await commands.proxyGetSettings()
|
||||||
if (s.autoStart) {
|
if (s.autoStart) {
|
||||||
await commands.proxyStart()
|
await commands.proxyStart()
|
||||||
|
// 若同时开启了"启动时自动开启系统代理",则一并开启系统代理
|
||||||
|
if (s.autoSystemProxy) {
|
||||||
|
await commands.proxySetSystemProxy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* 忽略:可能内核未安装 */
|
/* 忽略:可能内核未安装 */
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getCurrentWindow, Effect, EffectState } from '@tauri-apps/api/window'
|
|||||||
import { commands } from '@/lib/bindings'
|
import { commands } from '@/lib/bindings'
|
||||||
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, Terminal, History, FolderOpen, Ruler, Trash2 } from '@lucide/vue'
|
import { Search, Command, Loader2, CornerDownLeft, Calculator, Globe, Lock, ChevronRight, Terminal, History, FolderOpen, Ruler, Trash2 } from '@lucide/vue'
|
||||||
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
|
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion'
|
||||||
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||||
import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getMoreHistoryItems, getMoreHistoryCount, setFileIndexReady, type QPItem, type QPSubAction } from './providers'
|
import { aggregateSearch, loadAppIconsForResults, recordHistoryItem, getMoreHistoryItems, getMoreHistoryCount, setFileIndexReady, type QPItem, type QPSubAction } from './providers'
|
||||||
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
|
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
|
||||||
|
|
||||||
@@ -429,9 +430,10 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="qp-root flex flex-col h-screen w-screen" @keydown="onKeydown">
|
<div class="qp-root flex flex-col h-screen w-screen" @keydown="onKeydown">
|
||||||
|
<TooltipProvider>
|
||||||
<!-- 搜索输入 -->
|
<!-- 搜索输入 -->
|
||||||
<div class="qp-input-wrap">
|
<div class="qp-input-wrap">
|
||||||
<Search class="h-4 w-4 text-muted-foreground shrink-0" />
|
<Search class="size-4 text-muted-foreground shrink-0" />
|
||||||
<input
|
<input
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
v-model="query"
|
v-model="query"
|
||||||
@@ -446,16 +448,16 @@ onUnmounted(() => {
|
|||||||
<!-- 结果区 -->
|
<!-- 结果区 -->
|
||||||
<div class="qp-results">
|
<div class="qp-results">
|
||||||
<div v-if="loading && !hasResults()" class="qp-empty">
|
<div v-if="loading && !hasResults()" class="qp-empty">
|
||||||
<Loader2 class="h-6 w-6 animate-spin mb-2" />
|
<Loader2 class="size-6 animate-spin mb-2" />
|
||||||
<p class="text-sm">搜索中…</p>
|
<p class="text-sm">搜索中…</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!hasResults() && query.trim()" class="qp-empty">
|
<div v-else-if="!hasResults() && query.trim()" class="qp-empty">
|
||||||
<Search class="h-10 w-10 mb-3 opacity-40" />
|
<Search class="size-10 mb-3 opacity-40" />
|
||||||
<p class="text-sm">无匹配结果</p>
|
<p class="text-sm">无匹配结果</p>
|
||||||
<p class="text-xs mt-1 opacity-60">按 Enter 在搜索引擎中查找</p>
|
<p class="text-xs mt-1 opacity-60">按 Enter 在搜索引擎中查找</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!hasResults()" class="qp-empty">
|
<div v-else-if="!hasResults()" class="qp-empty">
|
||||||
<Command class="h-10 w-10 mb-3 opacity-40" />
|
<Command class="size-10 mb-3 opacity-40" />
|
||||||
<p class="text-sm">输入关键词开始搜索</p>
|
<p class="text-sm">输入关键词开始搜索</p>
|
||||||
<p class="text-xs mt-1 opacity-60">命令 · 计算 · 系统 · 网页</p>
|
<p class="text-xs mt-1 opacity-60">命令 · 计算 · 系统 · 网页</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,7 +479,7 @@ onUnmounted(() => {
|
|||||||
<component
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="groupIcon(item.group)"
|
:is="groupIcon(item.group)"
|
||||||
class="h-4 w-4 text-muted-foreground shrink-0"
|
class="size-4 text-muted-foreground shrink-0"
|
||||||
:class="isAppLike(item) ? '' : 'mt-0.5'"
|
:class="isAppLike(item) ? '' : 'mt-0.5'"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
|
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
|
||||||
@@ -522,7 +524,7 @@ onUnmounted(() => {
|
|||||||
<component
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="groupIcon(item.group)"
|
:is="groupIcon(item.group)"
|
||||||
class="h-4 w-4 text-muted-foreground shrink-0"
|
class="size-4 text-muted-foreground shrink-0"
|
||||||
:class="isAppLike(item) ? '' : 'mt-0.5'"
|
:class="isAppLike(item) ? '' : 'mt-0.5'"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
|
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
|
||||||
@@ -552,7 +554,7 @@ onUnmounted(() => {
|
|||||||
<component
|
<component
|
||||||
v-else
|
v-else
|
||||||
:is="groupIcon(item.group)"
|
:is="groupIcon(item.group)"
|
||||||
class="h-4 w-4 text-muted-foreground shrink-0"
|
class="size-4 text-muted-foreground shrink-0"
|
||||||
:class="isAppLike(item) ? '' : 'mt-0.5'"
|
:class="isAppLike(item) ? '' : 'mt-0.5'"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
|
<div class="flex-1 min-w-0 flex" :class="isAppLike(item) ? 'items-center' : 'flex-col'">
|
||||||
@@ -605,10 +607,15 @@ onUnmounted(() => {
|
|||||||
<div v-if="pendingDelete" class="qp-confirm-mask" @click.self="cancelDelete">
|
<div v-if="pendingDelete" class="qp-confirm-mask" @click.self="cancelDelete">
|
||||||
<div class="qp-confirm-box" role="alertdialog" aria-modal="true">
|
<div class="qp-confirm-box" role="alertdialog" aria-modal="true">
|
||||||
<div class="qp-confirm-head">
|
<div class="qp-confirm-head">
|
||||||
<Trash2 class="h-4 w-4 shrink-0 text-destructive" />
|
<Trash2 class="size-4 shrink-0 text-destructive" />
|
||||||
<p class="qp-confirm-title">确定删除?</p>
|
<p class="qp-confirm-title">确定删除?</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="qp-confirm-name truncate" :title="pendingDelete.name">{{ pendingDelete.name }}</p>
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<p class="qp-confirm-name truncate">{{ pendingDelete.name }}</p>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ pendingDelete.name }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<p class="qp-confirm-tip">将移动到回收站{{ pendingDelete.isDir ? '(含所有子内容)' : '' }},删除后可在回收站中还原。</p>
|
<p class="qp-confirm-tip">将移动到回收站{{ pendingDelete.isDir ? '(含所有子内容)' : '' }},删除后可在回收站中还原。</p>
|
||||||
<div class="flex items-center justify-end gap-2 mt-4">
|
<div class="flex items-center justify-end gap-2 mt-4">
|
||||||
<button class="qp-confirm-btn" @click="cancelDelete">取消</button>
|
<button class="qp-confirm-btn" @click="cancelDelete">取消</button>
|
||||||
@@ -616,6 +623,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -276,11 +276,11 @@ async function changeEngine(v: string) {
|
|||||||
<!-- 标题与测试 -->
|
<!-- 标题与测试 -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||||
<Command class="h-5 w-5 text-primary" />
|
<Command class="size-5 text-primary" />
|
||||||
快速面板
|
快速面板
|
||||||
</h2>
|
</h2>
|
||||||
<Button size="sm" @click="testPopup">
|
<Button size="sm" @click="testPopup">
|
||||||
<Zap class="h-4 w-4 mr-1" />
|
<Zap class="size-4 mr-1" />
|
||||||
测试唤起
|
测试唤起
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,7 +289,7 @@ async function changeEngine(v: string) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-sm flex items-center gap-2">
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
<Keyboard class="h-4 w-4" />
|
<Keyboard class="size-4" />
|
||||||
唤起快捷键
|
唤起快捷键
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -350,7 +350,7 @@ async function changeEngine(v: string) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-sm flex items-center gap-2">
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
<Globe class="h-4 w-4" />
|
<Globe class="size-4" />
|
||||||
默认搜索引擎
|
默认搜索引擎
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -376,7 +376,7 @@ async function changeEngine(v: string) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-sm flex items-center gap-2">
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
<FolderTree class="h-4 w-4" />
|
<FolderTree class="size-4" />
|
||||||
文件索引
|
文件索引
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -406,14 +406,14 @@ async function changeEngine(v: string) {
|
|||||||
class="flex items-center gap-2 p-2 rounded-md bg-muted/40"
|
class="flex items-center gap-2 p-2 rounded-md bg-muted/40"
|
||||||
>
|
>
|
||||||
<FolderTree class="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
<FolderTree class="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||||
<span class="text-xs font-mono truncate flex-1" :title="dir">{{ dir }}</span>
|
<span class="text-xs font-mono truncate flex-1">{{ dir }}</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-6 w-6 shrink-0 hover:text-destructive"
|
class="size-6 shrink-0 hover:text-destructive"
|
||||||
@click="removeDir(idx)"
|
@click="removeDir(idx)"
|
||||||
>
|
>
|
||||||
<X class="h-3 w-3" />
|
<X class="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!form.indexDirs.length" class="text-xs text-muted-foreground py-2">
|
<div v-if="!form.indexDirs.length" class="text-xs text-muted-foreground py-2">
|
||||||
@@ -435,7 +435,7 @@ async function changeEngine(v: string) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-sm flex items-center gap-2">
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
<Terminal class="h-4 w-4" />
|
<Terminal class="size-4" />
|
||||||
自定义命令
|
自定义命令
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -452,11 +452,11 @@ async function changeEngine(v: string) {
|
|||||||
<p class="text-xs font-medium truncate">{{ cmd.title }}</p>
|
<p class="text-xs font-medium truncate">{{ cmd.title }}</p>
|
||||||
<p class="text-xs text-muted-foreground font-mono truncate">{{ cmd.command }} {{ cmd.args.join(' ') }}</p>
|
<p class="text-xs text-muted-foreground font-mono truncate">{{ cmd.command }} {{ cmd.args.join(' ') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" @click="editCustomCommand(idx)">
|
<Button variant="ghost" size="icon" class="size-6 shrink-0" @click="editCustomCommand(idx)">
|
||||||
<Pencil class="h-3 w-3" />
|
<Pencil class="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0 hover:text-destructive" @click="removeCustomCommand(idx)">
|
<Button variant="ghost" size="icon" class="size-6 shrink-0 hover:text-destructive" @click="removeCustomCommand(idx)">
|
||||||
<X class="h-3 w-3" />
|
<X class="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!form.customCommands.length" class="text-xs text-muted-foreground py-2">
|
<div v-if="!form.customCommands.length" class="text-xs text-muted-foreground py-2">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
Eraser, Undo2, Redo2, Trash2, Copy, Save, X, Image as ImageIcon,
|
Eraser, Undo2, Redo2, Trash2, Copy, Save, X, Image as ImageIcon,
|
||||||
} from '@lucide/vue'
|
} from '@lucide/vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
// ===== 标注数据结构 =====
|
// ===== 标注数据结构 =====
|
||||||
type ToolType = 'rect' | 'arrow' | 'pen' | 'text' | 'mosaic' | 'highlight'
|
type ToolType = 'rect' | 'arrow' | 'pen' | 'text' | 'mosaic' | 'highlight'
|
||||||
@@ -451,41 +452,48 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen w-screen flex flex-col bg-zinc-950 text-zinc-100 overflow-hidden">
|
<div class="h-screen w-screen flex flex-col bg-zinc-950 text-zinc-100 overflow-hidden">
|
||||||
|
<TooltipProvider>
|
||||||
<!-- 标题栏 -->
|
<!-- 标题栏 -->
|
||||||
<div
|
<div
|
||||||
class="h-9 flex items-center justify-between px-3 bg-zinc-900 border-b border-zinc-800 shrink-0 select-none"
|
class="h-9 flex items-center justify-between px-3 bg-zinc-900 border-b border-zinc-800 shrink-0 select-none"
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
<span class="text-sm font-medium">截图编辑器</span>
|
<span class="text-sm font-medium">截图编辑器</span>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
class="text-zinc-400 hover:text-white hover:bg-zinc-800"
|
class="text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||||
title="关闭"
|
|
||||||
@click="closeWindow"
|
@click="closeWindow"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
>
|
>
|
||||||
<X class="h-4 w-4" />
|
<X class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>关闭</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="flex items-center gap-3 px-3 py-2 bg-zinc-900 border-b border-zinc-800 shrink-0 flex-wrap">
|
<div class="flex items-center gap-3 px-3 py-2 bg-zinc-900 border-b border-zinc-800 shrink-0 flex-wrap">
|
||||||
<!-- 工具组 -->
|
<!-- 工具组 -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
<Tooltip v-for="tool in TOOLS" :key="tool.value">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
v-for="tool in TOOLS"
|
|
||||||
:key="tool.value"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
:class="currentTool === tool.value
|
:class="currentTool === tool.value
|
||||||
? 'bg-zinc-700 text-white hover:bg-zinc-700 hover:text-white'
|
? 'bg-zinc-700 text-white hover:bg-zinc-700 hover:text-white'
|
||||||
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white'"
|
: 'text-zinc-400 hover:bg-zinc-800 hover:text-white'"
|
||||||
:title="tool.label"
|
|
||||||
@click="currentTool = tool.value"
|
@click="currentTool = tool.value"
|
||||||
>
|
>
|
||||||
<component :is="tool.icon" class="h-4 w-4" />
|
<component :is="tool.icon" class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ tool.label }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-6 w-px bg-zinc-700" />
|
<div class="h-6 w-px bg-zinc-700" />
|
||||||
@@ -493,15 +501,17 @@ onUnmounted(() => {
|
|||||||
<!-- 颜色 -->
|
<!-- 颜色 -->
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<span class="text-xs text-zinc-500">颜色</span>
|
<span class="text-xs text-zinc-500">颜色</span>
|
||||||
|
<Tooltip v-for="c in COLORS" :key="c">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
v-for="c in COLORS"
|
|
||||||
:key="c"
|
|
||||||
class="size-6 rounded-full border border-zinc-500 transition-transform hover:scale-110"
|
class="size-6 rounded-full border border-zinc-500 transition-transform hover:scale-110"
|
||||||
:class="currentColor === c ? 'ring-2 ring-blue-400 ring-offset-1 ring-offset-zinc-900 scale-110' : ''"
|
:class="currentColor === c ? 'ring-2 ring-blue-400 ring-offset-1 ring-offset-zinc-900 scale-110' : ''"
|
||||||
:style="{ backgroundColor: c }"
|
:style="{ backgroundColor: c }"
|
||||||
:title="c"
|
|
||||||
@click="currentColor = c"
|
@click="currentColor = c"
|
||||||
/>
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ c }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-6 w-px bg-zinc-700" />
|
<div class="h-6 w-px bg-zinc-700" />
|
||||||
@@ -558,46 +568,62 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 历史 / 清空 -->
|
<!-- 历史 / 清空 -->
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
||||||
:disabled="!canUndo"
|
:disabled="!canUndo"
|
||||||
title="橡皮(撤销最近标注)"
|
|
||||||
@click="undo"
|
@click="undo"
|
||||||
>
|
>
|
||||||
<Eraser class="h-4 w-4" />
|
<Eraser class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>橡皮(撤销最近标注)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
||||||
:disabled="!canUndo"
|
:disabled="!canUndo"
|
||||||
title="撤销"
|
|
||||||
@click="undo"
|
@click="undo"
|
||||||
>
|
>
|
||||||
<Undo2 class="h-4 w-4" />
|
<Undo2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>撤销</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
class="text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
||||||
:disabled="!canRedo"
|
:disabled="!canRedo"
|
||||||
title="重做"
|
|
||||||
@click="redo"
|
@click="redo"
|
||||||
>
|
>
|
||||||
<Redo2 class="h-4 w-4" />
|
<Redo2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>重做</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
class="text-zinc-400 hover:bg-red-900 hover:text-white"
|
class="text-zinc-400 hover:bg-red-900 hover:text-white"
|
||||||
:disabled="!canUndo"
|
:disabled="!canUndo"
|
||||||
title="清空"
|
|
||||||
@click="clearAll"
|
@click="clearAll"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-4 w-4" />
|
<Trash2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>清空</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -608,7 +634,7 @@ onUnmounted(() => {
|
|||||||
v-if="loadError"
|
v-if="loadError"
|
||||||
class="flex flex-col items-center justify-center h-full gap-3 text-zinc-400"
|
class="flex flex-col items-center justify-center h-full gap-3 text-zinc-400"
|
||||||
>
|
>
|
||||||
<ImageIcon class="h-12 w-12 opacity-40" />
|
<ImageIcon class="size-12 opacity-40" />
|
||||||
<p>未找到待编辑的截图</p>
|
<p>未找到待编辑的截图</p>
|
||||||
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="closeWindow">
|
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="closeWindow">
|
||||||
关闭
|
关闭
|
||||||
@@ -655,13 +681,14 @@ onUnmounted(() => {
|
|||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="saveToFile">
|
<Button variant="ghost" class="text-zinc-200 hover:bg-zinc-800 hover:text-white" @click="saveToFile">
|
||||||
<Save class="h-4 w-4" />
|
<Save class="size-4" />
|
||||||
保存到文件
|
保存到文件
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" class="bg-blue-600 text-white hover:bg-blue-500" @click="copyToClipboard">
|
<Button variant="ghost" class="bg-blue-600 text-white hover:bg-blue-500" @click="copyToClipboard">
|
||||||
<Copy class="h-4 w-4" />
|
<Copy class="size-4" />
|
||||||
复制到剪贴板
|
复制到剪贴板
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import {
|
import {
|
||||||
Keyboard, Settings, FolderOpen, Camera, Copy, Save, Trash2, Timer,
|
Keyboard, Settings, FolderOpen, Camera, Copy, Save, Trash2, Timer, StickyNote,
|
||||||
Image as ImageIcon, Loader2,
|
Image as ImageIcon, Loader2,
|
||||||
} from '@lucide/vue'
|
} from '@lucide/vue'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@@ -14,6 +14,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
const store = useScreenshotStore()
|
const store = useScreenshotStore()
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ const tabsListRef = useModuleTabs(activeTab, [
|
|||||||
{ value: 'history', label: '历史' },
|
{ value: 'history', label: '历史' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const HISTORY_LIMITS = [6, 12, 24, 48]
|
const HISTORY_LIMITS = [24, 48, 96]
|
||||||
const DELAY_OPTIONS = [0, 1, 2, 3, 5]
|
const DELAY_OPTIONS = [0, 1, 2, 3, 5]
|
||||||
|
|
||||||
function formatTime(t: number): string {
|
function formatTime(t: number): string {
|
||||||
@@ -69,14 +70,45 @@ async function handleSave(item: RecentCapture) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handlePin(item: RecentCapture) {
|
||||||
|
const idx = store.recent.findIndex((r) => r.id === item.id)
|
||||||
|
await store.togglePin(idx >= 0 ? idx : 0)
|
||||||
|
}
|
||||||
|
|
||||||
function handleDelete(item: RecentCapture) {
|
function handleDelete(item: RecentCapture) {
|
||||||
void store.removeRecent(item)
|
void store.removeRecent(item)
|
||||||
toast.success('已从历史移除')
|
toast.success('已从历史移除')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 快捷键录入器 =====
|
// ===== 历史保留数量:预设 + 自定义输入 =====
|
||||||
const recording = ref(false)
|
const customLimit = ref('')
|
||||||
const recorderRef = ref<HTMLDivElement | null>(null)
|
|
||||||
|
function onLimitPreset(n: number) {
|
||||||
|
store.setSettings({ historyLimit: n })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCustomLimit() {
|
||||||
|
const v = parseInt(customLimit.value, 10)
|
||||||
|
if (!Number.isFinite(v) || v < 1) {
|
||||||
|
toast.warning('请输入大于 0 的数字')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.setSettings({ historyLimit: Math.min(999, Math.max(1, Math.floor(v))) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前值不在预设中时,把自定义输入框预填为当前值
|
||||||
|
watch(
|
||||||
|
() => store.settings.historyLimit,
|
||||||
|
(v) => {
|
||||||
|
if (!HISTORY_LIMITS.includes(v)) customLimit.value = String(v)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// ===== 快捷键录入器(截图 / 贴图共用) =====
|
||||||
|
const recordingField = ref<'capture' | 'pin' | null>(null)
|
||||||
|
const captureRecorderRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const pinRecorderRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
/** 把存储的快捷键字符串格式化为展示形式:ctrl+alt+a → Ctrl + Alt + A */
|
/** 把存储的快捷键字符串格式化为展示形式:ctrl+alt+a → Ctrl + Alt + A */
|
||||||
function displayShortcut(s: string): string {
|
function displayShortcut(s: string): string {
|
||||||
@@ -119,11 +151,11 @@ function eventToShortcut(e: KeyboardEvent): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onRecordKey(e: KeyboardEvent) {
|
function onRecordKey(e: KeyboardEvent) {
|
||||||
if (!recording.value) return
|
if (!recordingField.value) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
recording.value = false
|
recordingField.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 仅修饰键按下时不结束(等待主键)
|
// 仅修饰键按下时不结束(等待主键)
|
||||||
@@ -133,24 +165,27 @@ function onRecordKey(e: KeyboardEvent) {
|
|||||||
toast.warning('不支持的按键组合,请使用字母/数字/功能键 + 修饰键')
|
toast.warning('不支持的按键组合,请使用字母/数字/功能键 + 修饰键')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
recording.value = false
|
const field = recordingField.value
|
||||||
void commitShortcut(combo)
|
recordingField.value = null
|
||||||
|
void commitShortcut(field, combo)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function commitShortcut(combo: string) {
|
async function commitShortcut(field: 'capture' | 'pin', combo: string) {
|
||||||
const ok = await store.setShortcut(combo)
|
const ok = field === 'capture'
|
||||||
|
? await store.setShortcut(combo)
|
||||||
|
: await store.setPinShortcut(combo)
|
||||||
if (ok) toast.success(`快捷键已更新为 ${displayShortcut(combo)}`)
|
if (ok) toast.success(`快捷键已更新为 ${displayShortcut(combo)}`)
|
||||||
else toast.error('快捷键注册失败,可能被其他程序占用')
|
else toast.error('快捷键注册失败,可能被其他程序占用')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startRecord() {
|
async function startRecord(field: 'capture' | 'pin') {
|
||||||
recording.value = true
|
recordingField.value = field
|
||||||
await nextTick()
|
await nextTick()
|
||||||
recorderRef.value?.focus()
|
;(field === 'capture' ? captureRecorderRef : pinRecorderRef).value?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(recording, (on) => {
|
watch(recordingField, (field) => {
|
||||||
if (on) window.addEventListener('keydown', onRecordKey, true)
|
if (field) window.addEventListener('keydown', onRecordKey, true)
|
||||||
else window.removeEventListener('keydown', onRecordKey, true)
|
else window.removeEventListener('keydown', onRecordKey, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,6 +194,11 @@ async function clearShortcut() {
|
|||||||
toast.success('已禁用截图快捷键')
|
toast.success('已禁用截图快捷键')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clearPinShortcut() {
|
||||||
|
await store.setPinShortcut('')
|
||||||
|
toast.success('已禁用贴图快捷键')
|
||||||
|
}
|
||||||
|
|
||||||
// 导出监听(screenshot-exported)由应用级注册(main.ts/App.vue),随应用生命周期管理;
|
// 导出监听(screenshot-exported)由应用级注册(main.ts/App.vue),随应用生命周期管理;
|
||||||
// 模块卸载不得销毁该单例监听,否则离开截图模块后全局快捷键截图将不记录历史/不自动保存。
|
// 模块卸载不得销毁该单例监听,否则离开截图模块后全局快捷键截图将不记录历史/不自动保存。
|
||||||
|
|
||||||
@@ -188,7 +228,7 @@ onUnmounted(() => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-sm flex items-center gap-2">
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
<Keyboard class="h-4 w-4 text-primary" />
|
<Keyboard class="size-4 text-primary" />
|
||||||
截图快捷键
|
截图快捷键
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -198,13 +238,13 @@ onUnmounted(() => {
|
|||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<!-- 快捷键录入器 -->
|
<!-- 快捷键录入器 -->
|
||||||
<div
|
<div
|
||||||
ref="recorderRef"
|
ref="captureRecorderRef"
|
||||||
class="hotkey-recorder"
|
class="hotkey-recorder"
|
||||||
:class="{ recording }"
|
:class="{ recording: recordingField === 'capture' }"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="startRecord"
|
@click="startRecord('capture')"
|
||||||
>
|
>
|
||||||
<template v-if="recording">按下快捷键…(Esc 取消)</template>
|
<template v-if="recordingField === 'capture'">按下快捷键…(Esc 取消)</template>
|
||||||
<template v-else-if="store.settings.shortcut">
|
<template v-else-if="store.settings.shortcut">
|
||||||
{{ displayShortcut(store.settings.shortcut) }}
|
{{ displayShortcut(store.settings.shortcut) }}
|
||||||
</template>
|
</template>
|
||||||
@@ -219,7 +259,7 @@ onUnmounted(() => {
|
|||||||
>清除</Button>
|
>清除</Button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
点击方框录入自定义快捷键,留空则禁用。默认 Ctrl + Alt + A
|
点击方框录入自定义快捷键,留空则禁用。默认 Ctrl + 1
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -227,8 +267,8 @@ onUnmounted(() => {
|
|||||||
:disabled="store.capturing"
|
:disabled="store.capturing"
|
||||||
@click="handleCapture"
|
@click="handleCapture"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="store.capturing" class="h-4 w-4 animate-spin" />
|
<Loader2 v-if="store.capturing" class="size-4 animate-spin" />
|
||||||
<Camera v-else class="h-4 w-4" />
|
<Camera v-else class="size-4" />
|
||||||
立即截图
|
立即截图
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,11 +286,52 @@ onUnmounted(() => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- 贴图快捷键 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
|
<StickyNote class="size-4 text-primary" />
|
||||||
|
贴图快捷键
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="flex items-center justify-between gap-4 py-1">
|
||||||
|
<div class="space-y-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<div
|
||||||
|
ref="pinRecorderRef"
|
||||||
|
class="hotkey-recorder"
|
||||||
|
:class="{ recording: recordingField === 'pin' }"
|
||||||
|
tabindex="0"
|
||||||
|
@click="startRecord('pin')"
|
||||||
|
>
|
||||||
|
<template v-if="recordingField === 'pin'">按下快捷键…(Esc 取消)</template>
|
||||||
|
<template v-else-if="store.settings.pinShortcut">
|
||||||
|
{{ displayShortcut(store.settings.pinShortcut) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>未设置(点击录入)</template>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="store.settings.pinShortcut"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 text-muted-foreground"
|
||||||
|
@click="clearPinShortcut"
|
||||||
|
>清除</Button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
把最近截图以原分辨率贴到屏幕(显示在原框选位置),再次按下可关闭贴图。默认 Alt + 1
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- 截图选项 -->
|
<!-- 截图选项 -->
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-sm flex items-center gap-2">
|
<CardTitle class="text-sm flex items-center gap-2">
|
||||||
<Settings class="h-4 w-4 text-primary" />
|
<Settings class="size-4 text-primary" />
|
||||||
截图选项
|
截图选项
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -259,7 +340,7 @@ onUnmounted(() => {
|
|||||||
<div class="flex items-center justify-between gap-3 py-2">
|
<div class="flex items-center justify-between gap-3 py-2">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label class="flex items-center gap-2 text-base font-medium">
|
<Label class="flex items-center gap-2 text-base font-medium">
|
||||||
<Timer class="h-4 w-4" />
|
<Timer class="size-4" />
|
||||||
延时截图
|
延时截图
|
||||||
</Label>
|
</Label>
|
||||||
<p class="text-sm text-muted-foreground">按下快捷键后倒计时再截图,便于切到目标画面</p>
|
<p class="text-sm text-muted-foreground">按下快捷键后倒计时再截图,便于切到目标画面</p>
|
||||||
@@ -302,7 +383,7 @@ onUnmounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" class="shrink-0" @click="chooseSaveDir">
|
<Button variant="outline" size="sm" class="shrink-0" @click="chooseSaveDir">
|
||||||
<FolderOpen class="h-4 w-4" />
|
<FolderOpen class="size-4" />
|
||||||
选择目录
|
选择目录
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -311,9 +392,9 @@ onUnmounted(() => {
|
|||||||
<div class="flex items-center justify-between gap-3 py-2 border-t">
|
<div class="flex items-center justify-between gap-3 py-2 border-t">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label class="text-base font-medium">历史保留数量</Label>
|
<Label class="text-base font-medium">历史保留数量</Label>
|
||||||
<p class="text-sm text-muted-foreground">超过上限的旧截图将自动移除</p>
|
<p class="text-sm text-muted-foreground">截图完成后自动持久化保存到应用目录,超过上限的旧截图将自动移除</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 shrink-0">
|
<div class="flex items-center gap-1.5 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
v-for="n in HISTORY_LIMITS"
|
v-for="n in HISTORY_LIMITS"
|
||||||
:key="n"
|
:key="n"
|
||||||
@@ -323,8 +404,19 @@ onUnmounted(() => {
|
|||||||
:class="store.settings.historyLimit === n
|
:class="store.settings.historyLimit === n
|
||||||
? 'bg-primary/10 text-primary hover:bg-primary/10'
|
? 'bg-primary/10 text-primary hover:bg-primary/10'
|
||||||
: 'text-muted-foreground'"
|
: 'text-muted-foreground'"
|
||||||
@click="store.setSettings({ historyLimit: n })"
|
@click="onLimitPreset(n)"
|
||||||
>{{ n }}</Button>
|
>{{ n }}</Button>
|
||||||
|
<input
|
||||||
|
v-model="customLimit"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="999"
|
||||||
|
class="h-8 w-16 rounded-md border border-input bg-transparent px-2 text-sm text-center outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||||
|
placeholder="自定义"
|
||||||
|
title="输入自定义保留数量"
|
||||||
|
@keydown.enter="onCustomLimit"
|
||||||
|
@blur="onCustomLimit"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -342,9 +434,9 @@ onUnmounted(() => {
|
|||||||
v-if="store.recent.length === 0"
|
v-if="store.recent.length === 0"
|
||||||
class="flex flex-col items-center justify-center h-64 text-muted-foreground gap-3"
|
class="flex flex-col items-center justify-center h-64 text-muted-foreground gap-3"
|
||||||
>
|
>
|
||||||
<ImageIcon class="h-12 w-12 opacity-40" />
|
<ImageIcon class="size-12 opacity-40" />
|
||||||
<p>暂无截图历史</p>
|
<p>暂无截图历史</p>
|
||||||
<p class="text-xs">按 Ctrl + Alt + A 截图并导出后会显示在这里</p>
|
<p class="text-xs">按 Ctrl + 1 截图并导出后会显示在这里</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 历史网格 -->
|
<!-- 历史网格 -->
|
||||||
@@ -373,31 +465,55 @@ onUnmounted(() => {
|
|||||||
<div
|
<div
|
||||||
class="absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity"
|
class="absolute inset-0 flex items-center justify-center gap-2 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
>
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon-sm"
|
||||||
|
@click.stop="handlePin(item)"
|
||||||
|
>
|
||||||
|
<StickyNote class="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>贴到屏幕</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
title="复制到剪贴板"
|
|
||||||
@click.stop="handleCopy(item)"
|
@click.stop="handleCopy(item)"
|
||||||
>
|
>
|
||||||
<Copy class="h-4 w-4" />
|
<Copy class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>复制到剪贴板</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
title="保存到文件"
|
|
||||||
@click.stop="handleSave(item)"
|
@click.stop="handleSave(item)"
|
||||||
>
|
>
|
||||||
<Save class="h-4 w-4" />
|
<Save class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>保存到文件</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
class="text-destructive hover:text-destructive"
|
class="text-destructive hover:text-destructive"
|
||||||
title="从历史移除"
|
|
||||||
@click.stop="handleDelete(item)"
|
@click.stop="handleDelete(item)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-4 w-4" />
|
<Trash2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>从历史移除</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@lucide/vue'
|
} from '@lucide/vue'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { Slider } from '@/components/ui/slider'
|
import { Slider } from '@/components/ui/slider'
|
||||||
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||||
import {
|
import {
|
||||||
TOOLS, COLORS, BLOCK_SIZES, ALPHAS, HANDLES, HANDLE_HIT, DRAG_THRESHOLD,
|
TOOLS, COLORS, BLOCK_SIZES, ALPHAS, HANDLES, HANDLE_HIT, DRAG_THRESHOLD,
|
||||||
type Phase, type ToolType, type Annotation, type DrawableAnnotation,
|
type Phase, type ToolType, type Annotation, type DrawableAnnotation,
|
||||||
@@ -1779,7 +1780,16 @@ async function finish() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!out) return
|
if (!out) return
|
||||||
await emit(EVENTS.screenshotExported, { pngBase64: out.b64, width: out.w, height: out.h })
|
// 附带框选区域的屏幕逻辑坐标(DIP):窗口逻辑位置 + 选区图像坐标。
|
||||||
|
// 截图图像按虚拟屏 DIP 捕获,坐标按 DIP 传递,贴图窗口按 DIP 布局即可 1:1 还原
|
||||||
|
const spPos = selPhys.value
|
||||||
|
await emit(EVENTS.screenshotExported, {
|
||||||
|
pngBase64: out.b64,
|
||||||
|
width: out.w,
|
||||||
|
height: out.h,
|
||||||
|
posX: Math.round(winOuterX / dpr + spPos.x),
|
||||||
|
posY: Math.round(winOuterY / dpr + spPos.y),
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[screenshot] 完成失败', e)
|
console.error('[screenshot] 完成失败', e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1801,7 +1811,14 @@ async function doSave() {
|
|||||||
})
|
})
|
||||||
if (!path) return
|
if (!path) return
|
||||||
await commands.screenshotSavePng(out.b64, path)
|
await commands.screenshotSavePng(out.b64, path)
|
||||||
await emit(EVENTS.screenshotExported, { pngBase64: out.b64, width: out.w, height: out.h })
|
const spPos = selPhys.value
|
||||||
|
await emit(EVENTS.screenshotExported, {
|
||||||
|
pngBase64: out.b64,
|
||||||
|
width: out.w,
|
||||||
|
height: out.h,
|
||||||
|
posX: Math.round(winOuterX / dpr + spPos.x),
|
||||||
|
posY: Math.round(winOuterY / dpr + spPos.y),
|
||||||
|
})
|
||||||
await win.hide().catch(() => {})
|
await win.hide().catch(() => {})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[screenshot] 保存失败', e)
|
console.error('[screenshot] 保存失败', e)
|
||||||
@@ -1985,6 +2002,7 @@ onUnmounted(() => {
|
|||||||
@mousemove="onMouseMove"
|
@mousemove="onMouseMove"
|
||||||
@mouseup="onMouseUp"
|
@mouseup="onMouseUp"
|
||||||
>
|
>
|
||||||
|
<TooltipProvider>
|
||||||
<!-- 冻结的屏幕底图 -->
|
<!-- 冻结的屏幕底图 -->
|
||||||
<img
|
<img
|
||||||
v-if="imgSrc"
|
v-if="imgSrc"
|
||||||
@@ -2094,31 +2112,37 @@ onUnmounted(() => {
|
|||||||
<div v-if="showToolbar" ref="toolbarRef" class="toolbar" :style="toolbarPos" @mousedown.stop>
|
<div v-if="showToolbar" ref="toolbarRef" class="toolbar" :style="toolbarPos" @mousedown.stop>
|
||||||
<!-- 标注工具 -->
|
<!-- 标注工具 -->
|
||||||
<div class="tb-group">
|
<div class="tb-group">
|
||||||
|
<Tooltip v-for="t in TOOLS" :key="t.value">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
v-for="t in TOOLS"
|
|
||||||
:key="t.value"
|
|
||||||
class="tb-icon"
|
class="tb-icon"
|
||||||
:class="{ active: phase === 'editing' && currentTool === t.value }"
|
:class="{ active: phase === 'editing' && currentTool === t.value }"
|
||||||
:title="t.label"
|
|
||||||
@click="onSelectTool(t.value)"
|
@click="onSelectTool(t.value)"
|
||||||
>
|
>
|
||||||
<component :is="t.icon" class="tb-svg" />
|
<component :is="t.icon" class="tb-svg" />
|
||||||
</button>
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ t.label }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tb-sep" />
|
<div class="tb-sep" />
|
||||||
|
|
||||||
<!-- 颜色色卡:显示当前颜色,点击打开选色板 -->
|
<!-- 颜色色卡:显示当前颜色,点击打开选色板 -->
|
||||||
<Popover v-model:open="colorPickerOpen">
|
<Popover v-model:open="colorPickerOpen">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<button
|
<button
|
||||||
class="color-badge"
|
class="color-badge"
|
||||||
:class="{ disabled: !colorEnabled }"
|
:class="{ disabled: !colorEnabled }"
|
||||||
:style="{ '--swatch-color': effectiveColor }"
|
:style="{ '--swatch-color': effectiveColor }"
|
||||||
:disabled="!colorEnabled"
|
:disabled="!colorEnabled"
|
||||||
title="颜色"
|
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>颜色</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<PopoverContent class="w-auto p-3" align="start">
|
<PopoverContent class="w-auto p-3" align="start">
|
||||||
<div class="color-popover">
|
<div class="color-popover">
|
||||||
<input
|
<input
|
||||||
@@ -2128,15 +2152,17 @@ onUnmounted(() => {
|
|||||||
@input="onCustomColorPick(($event.target as HTMLInputElement).value)"
|
@input="onCustomColorPick(($event.target as HTMLInputElement).value)"
|
||||||
/>
|
/>
|
||||||
<div class="color-swatches">
|
<div class="color-swatches">
|
||||||
|
<Tooltip v-for="c in COLORS" :key="c">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
v-for="c in COLORS"
|
|
||||||
:key="c"
|
|
||||||
class="color-swatch-mini"
|
class="color-swatch-mini"
|
||||||
:class="{ active: effectiveColor === c }"
|
:class="{ active: effectiveColor === c }"
|
||||||
:style="{ backgroundColor: c }"
|
:style="{ backgroundColor: c }"
|
||||||
:title="c"
|
|
||||||
@click="effectiveColor = c"
|
@click="effectiveColor = c"
|
||||||
/>
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{{ c }}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@@ -2144,14 +2170,18 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 粗细 badge:点击打开 Slider -->
|
<!-- 粗细 badge:点击打开 Slider -->
|
||||||
<Popover v-model:open="lineWidthPickerOpen">
|
<Popover v-model:open="lineWidthPickerOpen">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<button
|
<button
|
||||||
class="width-badge"
|
class="width-badge"
|
||||||
:class="{ disabled: !lineWidthEnabled }"
|
:class="{ disabled: !lineWidthEnabled }"
|
||||||
:disabled="!lineWidthEnabled"
|
:disabled="!lineWidthEnabled"
|
||||||
title="粗细"
|
|
||||||
>{{ effectiveLineWidth }}</button>
|
>{{ effectiveLineWidth }}</button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>粗细</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<PopoverContent class="w-auto p-3" align="start">
|
<PopoverContent class="w-auto p-3" align="start">
|
||||||
<div class="width-popover">
|
<div class="width-popover">
|
||||||
<div class="width-popover-header">
|
<div class="width-popover-header">
|
||||||
@@ -2174,43 +2204,62 @@ onUnmounted(() => {
|
|||||||
<template v-if="currentTool === 'mosaic'">
|
<template v-if="currentTool === 'mosaic'">
|
||||||
<div class="tb-sep" />
|
<div class="tb-sep" />
|
||||||
<div class="tb-group">
|
<div class="tb-group">
|
||||||
|
<Tooltip v-for="b in BLOCK_SIZES" :key="b">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
v-for="b in BLOCK_SIZES"
|
|
||||||
:key="b"
|
|
||||||
class="tb-num"
|
class="tb-num"
|
||||||
:class="{ active: blockSize === b }"
|
:class="{ active: blockSize === b }"
|
||||||
title="马赛克块大小"
|
|
||||||
@click="blockSize = b"
|
@click="blockSize = b"
|
||||||
>{{ b }}</button>
|
>{{ b }}</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>马赛克块大小</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="currentTool === 'highlight'">
|
<template v-if="currentTool === 'highlight'">
|
||||||
<div class="tb-sep" />
|
<div class="tb-sep" />
|
||||||
<div class="tb-group">
|
<div class="tb-group">
|
||||||
|
<Tooltip v-for="a in ALPHAS" :key="a">
|
||||||
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
v-for="a in ALPHAS"
|
|
||||||
:key="a"
|
|
||||||
class="tb-num"
|
class="tb-num"
|
||||||
:class="{ active: highlightAlpha === a }"
|
:class="{ active: highlightAlpha === a }"
|
||||||
title="高亮透明度"
|
|
||||||
@click="highlightAlpha = a"
|
@click="highlightAlpha = a"
|
||||||
>{{ Math.round(a * 100) }}%</button>
|
>{{ Math.round(a * 100) }}%</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>高亮透明度</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="tb-sep" />
|
<div class="tb-sep" />
|
||||||
|
|
||||||
<div class="tb-group">
|
<div class="tb-group">
|
||||||
<button class="tb-icon" :disabled="!canUndo" title="撤销" @click="undo">
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<button class="tb-icon" :disabled="!canUndo" @click="undo">
|
||||||
<Undo2 class="tb-svg" />
|
<Undo2 class="tb-svg" />
|
||||||
</button>
|
</button>
|
||||||
<button class="tb-icon" :disabled="!canRedo" title="重做" @click="redo">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>撤销</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<button class="tb-icon" :disabled="!canRedo" @click="redo">
|
||||||
<Redo2 class="tb-svg" />
|
<Redo2 class="tb-svg" />
|
||||||
</button>
|
</button>
|
||||||
<button class="tb-icon" :disabled="!canUndo" title="清空标注" @click="clearAnnotations">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>重做</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<button class="tb-icon" :disabled="!canUndo" @click="clearAnnotations">
|
||||||
<Eraser class="tb-svg" />
|
<Eraser class="tb-svg" />
|
||||||
</button>
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>清空标注</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tb-sep" />
|
<div class="tb-sep" />
|
||||||
@@ -2221,12 +2270,22 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 复制 / 保存(置末) -->
|
<!-- 复制 / 保存(置末) -->
|
||||||
<div class="tb-group">
|
<div class="tb-group">
|
||||||
<button class="tb-icon" title="保存到文件" @click="doSave">
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<button class="tb-icon" @click="doSave">
|
||||||
<Save class="tb-svg" />
|
<Save class="tb-svg" />
|
||||||
</button>
|
</button>
|
||||||
<button class="tb-icon primary" title="复制到剪贴板 (Enter)" @click="finish">
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>保存到文件</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<button class="tb-icon primary" @click="finish">
|
||||||
<Copy class="tb-svg" />
|
<Copy class="tb-svg" />
|
||||||
</button>
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>复制到剪贴板 (Enter)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2245,6 +2304,7 @@ onUnmounted(() => {
|
|||||||
<div class="mag-hint">{{ hint.split(' · ').join('\n') }}</div>
|
<div class="mag-hint">{{ hint.split(' · ').join('\n') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// 贴图窗口:无边框、透明、置顶。
|
||||||
|
// 图片以原分辨率显示,窗口定位到截图时的框选位置(原框选位置)。
|
||||||
|
// 右下角展开图标,hover 展开 上一张 / 下一张 / 关闭。
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { getCurrentWindow, currentMonitor } from '@tauri-apps/api/window'
|
||||||
|
import { LogicalPosition, LogicalSize } from '@tauri-apps/api/dpi'
|
||||||
|
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||||
|
import { ChevronLeft, ChevronRight, X, Maximize2, Loader2 } from '@lucide/vue'
|
||||||
|
import { EVENTS, STORAGE_KEYS } from '@/lib/constants'
|
||||||
|
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts)
|
||||||
|
import { commands } from '@/lib/bindings'
|
||||||
|
|
||||||
|
/** 图片四周留白(CSS 像素 = 逻辑像素 DIP,与截图图像分辨率单位一致),用于呈现蓝色光晕 */
|
||||||
|
const GLOW_PAD = 18
|
||||||
|
|
||||||
|
const win = getCurrentWindow()
|
||||||
|
const imgSrc = ref('')
|
||||||
|
const imgCssW = ref(0)
|
||||||
|
const imgCssH = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMsg = ref('')
|
||||||
|
/** 历史列表(localStorage 与主窗口共享,最新在前) */
|
||||||
|
const items = ref<{ filePath: string; width: number; height: number; posX?: number; posY?: number }[]>([])
|
||||||
|
const cur = ref(0)
|
||||||
|
|
||||||
|
let showUnlisten: UnlistenFn | null = null
|
||||||
|
|
||||||
|
/** 同步主界面主题(独立窗口无 pinia,从 localStorage 读取应用设置) */
|
||||||
|
function applyTheme() {
|
||||||
|
const root = document.documentElement
|
||||||
|
let theme = 'system'
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEYS.appSettings)
|
||||||
|
if (raw) {
|
||||||
|
const s = JSON.parse(raw)
|
||||||
|
theme = s.theme ?? 'system'
|
||||||
|
}
|
||||||
|
} catch { /* 忽略 */ }
|
||||||
|
let isDark: boolean
|
||||||
|
if (theme === 'system') {
|
||||||
|
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
} else {
|
||||||
|
isDark = theme === 'dark'
|
||||||
|
}
|
||||||
|
root.classList.toggle('dark', isDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 localStorage 读取主窗口持久化的截图历史(图片文件在应用数据目录) */
|
||||||
|
function readHistory() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEYS.screenshotHistory)
|
||||||
|
if (!raw) return []
|
||||||
|
const parsed = JSON.parse(raw) as {
|
||||||
|
version?: number
|
||||||
|
items?: { filePath: string; width: number; height: number; posX?: number; posY?: number }[]
|
||||||
|
}
|
||||||
|
if (!Array.isArray(parsed.items)) return []
|
||||||
|
return parsed.items
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 窗口排版到目标位置(全部使用逻辑坐标/DIP):
|
||||||
|
* 截图图像的分辨率是逻辑像素(BMP 按虚拟屏 DIP 捕获),CSS 像素与 DIP 一致,
|
||||||
|
* 故图片以 item.width/height 的 CSS 尺寸显示即 1:1 还原;窗口尺寸 = 图片 + 2×留白,
|
||||||
|
* 窗口位置 = 原框选位置 - 留白,使图片精确覆盖在截图时的区域上(跨 DPI 显示器也无需换算)。
|
||||||
|
*/
|
||||||
|
async function layoutTo(item: { width: number; height: number; posX?: number; posY?: number }) {
|
||||||
|
const pad = GLOW_PAD
|
||||||
|
let x = item.posX !== undefined ? item.posX - pad : 0
|
||||||
|
let y = item.posY !== undefined ? item.posY - pad : 0
|
||||||
|
// 无定位信息(如编辑器导出)时放到当前鼠标位置附近(鼠标坐标为物理像素,需换算为 DIP)
|
||||||
|
if (item.posX === undefined || item.posY === undefined) {
|
||||||
|
try {
|
||||||
|
const [mx, my] = await commands.screenshotCursorPos()
|
||||||
|
const mon = await currentMonitor()
|
||||||
|
const s = mon?.scaleFactor || 1
|
||||||
|
x = mx / s - pad
|
||||||
|
y = my / s - pad
|
||||||
|
} catch { /* 忽略,沿用 0,0 */ }
|
||||||
|
}
|
||||||
|
// 粗略限制在鼠标所在显示器内(避免贴图完全出现在屏幕外,至少保留 40px 可见)
|
||||||
|
try {
|
||||||
|
const mon = await currentMonitor()
|
||||||
|
if (mon) {
|
||||||
|
const s = mon.scaleFactor || 1
|
||||||
|
const mlx = mon.position.x / s
|
||||||
|
const mly = mon.position.y / s
|
||||||
|
const mlw = mon.size.width / s
|
||||||
|
const mlh = mon.size.height / s
|
||||||
|
x = Math.min(Math.max(x, mlx), mlx + mlw - 40)
|
||||||
|
y = Math.min(Math.max(y, mly), mly + mlh - 40)
|
||||||
|
}
|
||||||
|
} catch { /* 忽略 */ }
|
||||||
|
// Windows 上非 resizable 窗口 setSize 会失效,须先临时开启再恢复:
|
||||||
|
// 布局结束后恢复为不可缩放,禁用边缘拖动修改大小(仅保留左键自由拖动)
|
||||||
|
await win.setPosition(new LogicalPosition(Math.round(x), Math.round(y)))
|
||||||
|
await win.setResizable(true)
|
||||||
|
await win.setSize(new LogicalSize(item.width + pad * 2, item.height + pad * 2))
|
||||||
|
await win.setResizable(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载并显示第 index 张截图(index 缺省/越界时按最近一张) */
|
||||||
|
async function loadImage(index: number) {
|
||||||
|
const list = items.value
|
||||||
|
if (!list.length) {
|
||||||
|
errorMsg.value = '暂无历史截图'
|
||||||
|
loading.value = false
|
||||||
|
await win.show().catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const idx = Math.max(0, Math.min(index, list.length - 1))
|
||||||
|
const item = list[idx]
|
||||||
|
cur.value = idx
|
||||||
|
loading.value = true
|
||||||
|
errorMsg.value = ''
|
||||||
|
try {
|
||||||
|
const b64 = await commands.screenshotLoadCache(item.filePath)
|
||||||
|
const dataUrl = `data:image/png;base64,${b64}`
|
||||||
|
// 预解码:等待图片就绪后再排版显示,避免闪烁
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => resolve()
|
||||||
|
img.onerror = () => reject(new Error('图片解码失败'))
|
||||||
|
img.src = dataUrl
|
||||||
|
})
|
||||||
|
// CSS 尺寸 = 原分辨率(逻辑像素/DIP),与 CSS 像素一致,1:1 还原截图时覆盖的区域
|
||||||
|
await layoutTo(item)
|
||||||
|
imgCssW.value = item.width
|
||||||
|
imgCssH.value = item.height
|
||||||
|
imgSrc.value = dataUrl
|
||||||
|
await win.show().catch(() => {})
|
||||||
|
loading.value = false
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[screenshot] 贴图加载失败', e)
|
||||||
|
errorMsg.value = '加载图片失败:' + (e as Error).message
|
||||||
|
loading.value = false
|
||||||
|
await win.show().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev() {
|
||||||
|
if (cur.value >= items.value.length - 1) return
|
||||||
|
void loadImage(cur.value + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (cur.value <= 0) return
|
||||||
|
void loadImage(cur.value - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePin() {
|
||||||
|
void win.hide().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 左键按下时启动原生窗口拖动(与 OSD 窗口同款方案:
|
||||||
|
* 透明 + focus:false 窗口上 data-tauri-drag-region 不生效,需显式 startDragging) */
|
||||||
|
function onImageMouseDown(e: MouseEvent) {
|
||||||
|
if (e.button !== 0) return
|
||||||
|
e.preventDefault()
|
||||||
|
void win.startDragging().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 响应主窗口 'screenshot-pin-show':读取要贴的历史索引并加载显示 */
|
||||||
|
function openPin() {
|
||||||
|
items.value = readHistory()
|
||||||
|
let index = 0
|
||||||
|
try {
|
||||||
|
index = Number(localStorage.getItem(STORAGE_KEYS.screenshotPinIndex) ?? 0) || 0
|
||||||
|
} catch { /* 忽略 */ }
|
||||||
|
void loadImage(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
applyTheme()
|
||||||
|
// 先注册 show 监听再通知 store 就绪,避免首轮事件丢失(与覆盖层同模式)
|
||||||
|
showUnlisten = await listen(EVENTS.screenshotPinShow, () => {
|
||||||
|
openPin()
|
||||||
|
})
|
||||||
|
await emit(EVENTS.screenshotPinReady)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
showUnlisten?.()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pin-root">
|
||||||
|
<!-- 图片区:左键按住自由拖动(显式 startDragging),蓝色光晕阴影 -->
|
||||||
|
<div
|
||||||
|
class="pin-image"
|
||||||
|
:style="{ width: imgCssW + 'px', height: imgCssH + 'px' }"
|
||||||
|
@mousedown="onImageMouseDown"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="imgSrc"
|
||||||
|
:src="imgSrc"
|
||||||
|
:style="{ width: imgCssW + 'px', height: imgCssH + 'px' }"
|
||||||
|
draggable="false"
|
||||||
|
alt="贴图"
|
||||||
|
>
|
||||||
|
<div v-else-if="errorMsg" class="pin-tip">{{ errorMsg }}</div>
|
||||||
|
<div v-else-if="loading" class="pin-tip"><Loader2 class="size-5 animate-spin" /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右下角控制:展开图标,hover 展开 上一张 / 下一张 / 关闭 -->
|
||||||
|
<div class="pin-controls">
|
||||||
|
<div class="pin-actions">
|
||||||
|
<button
|
||||||
|
class="pin-btn"
|
||||||
|
title="上一张"
|
||||||
|
:disabled="cur >= items.length - 1"
|
||||||
|
@click="prev"
|
||||||
|
>
|
||||||
|
<ChevronLeft class="size-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="pin-btn"
|
||||||
|
title="下一张"
|
||||||
|
:disabled="cur <= 0"
|
||||||
|
@click="next"
|
||||||
|
>
|
||||||
|
<ChevronRight class="size-4" />
|
||||||
|
</button>
|
||||||
|
<button class="pin-btn pin-btn-danger" title="关闭" @click="closePin">
|
||||||
|
<X class="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="pin-expand" title="更多操作">
|
||||||
|
<Maximize2 class="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pin-root {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 18px; /* GLOW_PAD,与脚本常量一致;留白大于光晕扩散范围,避免光晕照出窗口外边框 */
|
||||||
|
background: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-image {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0; /* 直角 */
|
||||||
|
cursor: grab;
|
||||||
|
/* 散发蓝色光晕的阴影:无实线边框,且扩散范围控制在留白内(光晕小、柔和) */
|
||||||
|
box-shadow:
|
||||||
|
0 0 6px rgba(59, 130, 246, 0.55),
|
||||||
|
0 0 14px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-image:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-image img {
|
||||||
|
display: block;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(15, 23, 42, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右下角控制:独立于图片拖动区(兄弟节点),点击不触发拖动 */
|
||||||
|
.pin-controls {
|
||||||
|
position: absolute;
|
||||||
|
right: 26px;
|
||||||
|
bottom: 26px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-expand,
|
||||||
|
.pin-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
background: rgba(15, 23, 42, 0.65);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn:hover {
|
||||||
|
background: rgba(30, 58, 138, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn-danger:hover {
|
||||||
|
background: rgba(185, 28, 28, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateX(8px);
|
||||||
|
transition:
|
||||||
|
opacity 0.15s ease,
|
||||||
|
transform 0.15s ease,
|
||||||
|
visibility 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 展开三个图标按钮 */
|
||||||
|
.pin-controls:hover .pin-actions,
|
||||||
|
.pin-controls:focus-within .pin-actions {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -29,6 +29,8 @@ onMounted(() => {
|
|||||||
searchStore.registerAction('settings', 4, () => appStore.setEffect('mica'))
|
searchStore.registerAction('settings', 4, () => appStore.setEffect('mica'))
|
||||||
searchStore.registerAction('settings', 5, () => appStore.setEffect('acrylic'))
|
searchStore.registerAction('settings', 5, () => appStore.setEffect('acrylic'))
|
||||||
searchStore.registerAction('settings', 6, () => appStore.toggleAutoStart())
|
searchStore.registerAction('settings', 6, () => appStore.toggleAutoStart())
|
||||||
|
// 主动刷新所有进程状态,确保内核 badge 显示当前真实状态(而非过期缓存)
|
||||||
|
processStore.refreshAll().catch(() => { /* 忽略:后端可能未就绪 */ })
|
||||||
})
|
})
|
||||||
|
|
||||||
const themes: Array<{ id: Theme; name: string; color: string; icon: typeof Sun }> = [
|
const themes: Array<{ id: Theme; name: string; color: string; icon: typeof Sun }> = [
|
||||||
@@ -132,7 +134,7 @@ const onDragEnd = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<Sparkles class="h-5 w-5 text-primary" />
|
<Sparkles class="size-5 text-primary" />
|
||||||
常规设置
|
常规设置
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -153,7 +155,7 @@ const onDragEnd = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<Layers class="h-5 w-5 text-primary" />
|
<Layers class="size-5 text-primary" />
|
||||||
主题切换
|
主题切换
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -175,7 +177,7 @@ const onDragEnd = () => {
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="theme.icon"
|
:is="theme.icon"
|
||||||
class="h-8 w-8 transition-colors duration-300"
|
class="size-8 transition-colors duration-300"
|
||||||
:class="theme.id === 'dark' || (theme.id === 'system' && systemDark) ? 'text-white' : 'text-gray-800'"
|
:class="theme.id === 'dark' || (theme.id === 'system' && systemDark) ? 'text-white' : 'text-gray-800'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,7 +200,7 @@ const onDragEnd = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<Sparkles class="h-5 w-5 text-primary" />
|
<Sparkles class="size-5 text-primary" />
|
||||||
效果切换
|
效果切换
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -244,7 +246,7 @@ const onDragEnd = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<Package class="h-5 w-5 text-primary" />
|
<Package class="size-5 text-primary" />
|
||||||
模块管理
|
模块管理
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -271,7 +273,7 @@ const onDragEnd = () => {
|
|||||||
<div
|
<div
|
||||||
class="drag-handle cursor-grab active:cursor-grabbing text-muted-foreground/40 hover:text-muted-foreground transition-colors"
|
class="drag-handle cursor-grab active:cursor-grabbing text-muted-foreground/40 hover:text-muted-foreground transition-colors"
|
||||||
>
|
>
|
||||||
<GripVertical class="h-4 w-4 no-native-drag" />
|
<GripVertical class="size-4 no-native-drag" />
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>拖拽排序</TooltipContent>
|
<TooltipContent>拖拽排序</TooltipContent>
|
||||||
@@ -280,7 +282,7 @@ const onDragEnd = () => {
|
|||||||
class="w-9 h-9 rounded-lg flex items-center justify-center"
|
class="w-9 h-9 rounded-lg flex items-center justify-center"
|
||||||
:class="module.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'"
|
:class="module.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'"
|
||||||
>
|
>
|
||||||
<component :is="getModuleIcon(module.icon)" class="h-5 w-5" />
|
<component :is="getModuleIcon(module.icon)" class="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-sm flex items-center gap-2">
|
<div class="font-medium text-sm flex items-center gap-2">
|
||||||
@@ -314,7 +316,7 @@ const onDragEnd = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="flex items-center gap-2">
|
<CardTitle class="flex items-center gap-2">
|
||||||
<LogOut class="h-5 w-5 text-destructive" />
|
<LogOut class="size-5 text-destructive" />
|
||||||
退出程序
|
退出程序
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -324,7 +326,7 @@ const onDragEnd = () => {
|
|||||||
class="text-destructive border-destructive/20 hover:bg-destructive/10"
|
class="text-destructive border-destructive/20 hover:bg-destructive/10"
|
||||||
@click="quitApp"
|
@click="quitApp"
|
||||||
>
|
>
|
||||||
<LogOut class="h-4 w-4 mr-2" />
|
<LogOut class="size-4 mr-2" />
|
||||||
彻底退出
|
彻底退出
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ onUnmounted(() => {
|
|||||||
<div class="tray-menu-scroll flex-1 min-h-0 overflow-y-auto">
|
<div class="tray-menu-scroll flex-1 min-h-0 overflow-y-auto">
|
||||||
<div class="tray-section">
|
<div class="tray-section">
|
||||||
<div class="tray-section-header">
|
<div class="tray-section-header">
|
||||||
<Globe class="h-4 w-4 text-primary" />
|
<Globe class="size-4 text-primary" />
|
||||||
<span class="tray-section-title">代理</span>
|
<span class="tray-section-title">代理</span>
|
||||||
<span v-if="state.proxyRunning" class="tray-status-dot running"></span>
|
<span v-if="state.proxyRunning" class="tray-status-dot running"></span>
|
||||||
<span v-else class="tray-status-dot stopped"></span>
|
<span v-else class="tray-status-dot stopped"></span>
|
||||||
@@ -441,12 +441,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<div class="tray-section-body">
|
<div class="tray-section-body">
|
||||||
<button class="tray-item" :disabled="proxyLoading" @click="handleProxyToggle">
|
<button class="tray-item" :disabled="proxyLoading" @click="handleProxyToggle">
|
||||||
<component :is="proxyButtonIcon" class="h-4 w-4" />
|
<component :is="proxyButtonIcon" class="size-4" />
|
||||||
<span>{{ proxyButtonLabel }}</span>
|
<span>{{ proxyButtonLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="state.proxyRunning" class="tray-item" :disabled="proxyLoading" @click="handleSystemProxyToggle">
|
<button v-if="state.proxyRunning" class="tray-item" :disabled="proxyLoading" @click="handleSystemProxyToggle">
|
||||||
<Power class="h-4 w-4" />
|
<Power class="size-4" />
|
||||||
<span>系统代理</span>
|
<span>系统代理</span>
|
||||||
<span class="tray-toggle-indicator" :class="{ active: state.systemProxy }"></span>
|
<span class="tray-toggle-indicator" :class="{ active: state.systemProxy }"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -457,7 +457,7 @@ onUnmounted(() => {
|
|||||||
:disabled="proxyLoading"
|
:disabled="proxyLoading"
|
||||||
@click="handleAction('proxy_refresh')"
|
@click="handleAction('proxy_refresh')"
|
||||||
>
|
>
|
||||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': refreshing }" />
|
<RefreshCw class="size-4" :class="{ 'animate-spin': refreshing }" />
|
||||||
<span>刷新节点列表</span>
|
<span>刷新节点列表</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -482,7 +482,7 @@ onUnmounted(() => {
|
|||||||
:value="node.name"
|
:value="node.name"
|
||||||
class="tray-select-option"
|
class="tray-select-option"
|
||||||
>
|
>
|
||||||
<Check v-if="node.isCurrent" class="h-3 w-3 text-primary flex-shrink-0" />
|
<Check v-if="node.isCurrent" class="size-3 text-primary flex-shrink-0" />
|
||||||
<span class="tray-select-badge-option">{{ abbreviateNodeName(node.name) }}</span>
|
<span class="tray-select-badge-option">{{ abbreviateNodeName(node.name) }}</span>
|
||||||
<span class="tray-select-delay" :class="delayClass(node.delay)">
|
<span class="tray-select-delay" :class="delayClass(node.delay)">
|
||||||
{{ delayText(node.delay) }}
|
{{ delayText(node.delay) }}
|
||||||
@@ -498,7 +498,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<div class="tray-section">
|
<div class="tray-section">
|
||||||
<div class="tray-section-header">
|
<div class="tray-section-header">
|
||||||
<Monitor class="h-4 w-4 text-primary" />
|
<Monitor class="size-4 text-primary" />
|
||||||
<span class="tray-section-title">监控</span>
|
<span class="tray-section-title">监控</span>
|
||||||
<span v-if="state.monitorRunning" class="tray-status-dot running"></span>
|
<span v-if="state.monitorRunning" class="tray-status-dot running"></span>
|
||||||
<span v-else class="tray-status-dot stopped"></span>
|
<span v-else class="tray-status-dot stopped"></span>
|
||||||
@@ -506,7 +506,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<div class="tray-section-body">
|
<div class="tray-section-body">
|
||||||
<button class="tray-item" @click="handleAction('osd_toggle')">
|
<button class="tray-item" @click="handleAction('osd_toggle')">
|
||||||
<Monitor class="h-4 w-4" />
|
<Monitor class="size-4" />
|
||||||
<span>OSD 显示</span>
|
<span>OSD 显示</span>
|
||||||
<span class="tray-toggle-indicator" :class="{ active: osdVisible }"></span>
|
<span class="tray-toggle-indicator" :class="{ active: osdVisible }"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -515,7 +515,7 @@ onUnmounted(() => {
|
|||||||
:disabled="!state.monitorRunning"
|
:disabled="!state.monitorRunning"
|
||||||
@click="handleAction('kernel_restart')"
|
@click="handleAction('kernel_restart')"
|
||||||
>
|
>
|
||||||
<RefreshCw class="h-4 w-4" />
|
<RefreshCw class="size-4" />
|
||||||
<span>重启 Kernel</span>
|
<span>重启 Kernel</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -524,18 +524,18 @@ onUnmounted(() => {
|
|||||||
<div class="tray-separator"></div>
|
<div class="tray-separator"></div>
|
||||||
|
|
||||||
<button class="tray-item" @click="handleAction('download_new')">
|
<button class="tray-item" @click="handleAction('download_new')">
|
||||||
<Download class="h-4 w-4" />
|
<Download class="size-4" />
|
||||||
<span>新建下载</span>
|
<span>新建下载</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="tray-separator"></div>
|
<div class="tray-separator"></div>
|
||||||
|
|
||||||
<button class="tray-item" @click="handleAction('settings')">
|
<button class="tray-item" @click="handleAction('settings')">
|
||||||
<Settings class="h-4 w-4" />
|
<Settings class="size-4" />
|
||||||
<span>常规设置</span>
|
<span>常规设置</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tray-item tray-quit" @click="handleAction('quit')">
|
<button class="tray-item tray-quit" @click="handleAction('quit')">
|
||||||
<LogOut class="h-4 w-4" />
|
<LogOut class="size-4" />
|
||||||
<span>退出</span>
|
<span>退出</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -325,6 +325,9 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
/** 提权标志是否已启用(后续启动自动触发 UAC) */
|
/** 提权标志是否已启用(后续启动自动触发 UAC) */
|
||||||
const elevateOnLaunch = ref(false)
|
const elevateOnLaunch = ref(false)
|
||||||
|
|
||||||
|
/** "应用启动时自动启动监控内核"开关(持久化于后端 settings.json) */
|
||||||
|
const autoStart = ref(false)
|
||||||
|
|
||||||
/** 硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
|
/** 硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
|
||||||
const hardwareConfig = ref<HardwareConfigResponse | null>(null)
|
const hardwareConfig = ref<HardwareConfigResponse | null>(null)
|
||||||
|
|
||||||
@@ -540,6 +543,27 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 刷新"自动启动监控内核"开关状态 */
|
||||||
|
async function refreshAutoStart() {
|
||||||
|
try {
|
||||||
|
autoStart.value = await invoke<boolean>('monitor_get_auto_start')
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('获取自动启动开关失败: ' + e)
|
||||||
|
}
|
||||||
|
return autoStart.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置"自动启动监控内核"开关 */
|
||||||
|
async function setAutoStart(enabled: boolean) {
|
||||||
|
try {
|
||||||
|
await invoke('monitor_set_auto_start', { enabled })
|
||||||
|
autoStart.value = enabled
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = String(e)
|
||||||
|
logger.error('设置自动启动开关失败: ' + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 拉取硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
|
/** 拉取硬件监控配置(可用硬件 + 传感器类型清单 + 当前启用状态) */
|
||||||
async function fetchHardwareConfig() {
|
async function fetchHardwareConfig() {
|
||||||
try {
|
try {
|
||||||
@@ -641,7 +665,7 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
try {
|
try {
|
||||||
// 启动心跳,驱动 connState 的断线判定(SSE 断开后依赖不再变化,须有心跳触发重算)
|
// 启动心跳,驱动 connState 的断线判定(SSE 断开后依赖不再变化,须有心跳触发重算)
|
||||||
startHeartbeat()
|
startHeartbeat()
|
||||||
await Promise.all([refreshStatus(), refreshKernelInfo(), refreshElevateOnLaunch()])
|
await Promise.all([refreshStatus(), refreshKernelInfo(), refreshElevateOnLaunch(), refreshAutoStart()])
|
||||||
await subscribe()
|
await subscribe()
|
||||||
// 若 Kernel 已就绪,立即拉一次快照避免 UI 空白
|
// 若 Kernel 已就绪,立即拉一次快照避免 UI 空白
|
||||||
if (status.value?.ready) {
|
if (status.value?.ready) {
|
||||||
@@ -978,6 +1002,11 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
// OSD 开关变化时创建/隐藏悬浮窗
|
// OSD 开关变化时创建/隐藏悬浮窗
|
||||||
osdWatchStops.push(watch(() => osdConfig.value.overlayEnabled, (enabled) => {
|
osdWatchStops.push(watch(() => osdConfig.value.overlayEnabled, (enabled) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
// OSD 显示开启时自动开启"应用启动时自动启动监控内核",
|
||||||
|
// 使 OSD 持续显示不因重启而中断
|
||||||
|
if (!autoStart.value) {
|
||||||
|
setAutoStart(true).catch(e => logger.error('[OSD] 自动开启 autoStart 失败: ' + e))
|
||||||
|
}
|
||||||
// 开启时若显示项为空则不创建窗口
|
// 开启时若显示项为空则不创建窗口
|
||||||
if (osdConfig.value.overlayItems.length === 0) return
|
if (osdConfig.value.overlayItems.length === 0) return
|
||||||
ensureOverlayWindow().catch(e => logger.error('[OSD] 创建悬浮窗失败: ' + e))
|
ensureOverlayWindow().catch(e => logger.error('[OSD] 创建悬浮窗失败: ' + e))
|
||||||
@@ -1061,6 +1090,7 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
initialized,
|
initialized,
|
||||||
networkSpeed,
|
networkSpeed,
|
||||||
elevateOnLaunch,
|
elevateOnLaunch,
|
||||||
|
autoStart,
|
||||||
hardwareConfig,
|
hardwareConfig,
|
||||||
// getters
|
// getters
|
||||||
connState,
|
connState,
|
||||||
@@ -1071,6 +1101,8 @@ export const useMonitorStore = defineStore('monitor', () => {
|
|||||||
refreshStatus,
|
refreshStatus,
|
||||||
refreshKernelInfo,
|
refreshKernelInfo,
|
||||||
refreshElevateOnLaunch,
|
refreshElevateOnLaunch,
|
||||||
|
refreshAutoStart,
|
||||||
|
setAutoStart,
|
||||||
start,
|
start,
|
||||||
elevateSelf,
|
elevateSelf,
|
||||||
cancelElevation,
|
cancelElevation,
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ export const useProcessStore = defineStore('process', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 释放进程状态事件监听(应用退出或热重载时调用) */
|
||||||
|
const destroyListener = () => {
|
||||||
|
if (unlistenFn) {
|
||||||
|
unlistenFn()
|
||||||
|
unlistenFn = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 通过模块 ID 启动进程(自动从注册表读取进程配置) */
|
/** 通过模块 ID 启动进程(自动从注册表读取进程配置) */
|
||||||
const startByModule = async (moduleId: string): Promise<ProcessInfo> => {
|
const startByModule = async (moduleId: string): Promise<ProcessInfo> => {
|
||||||
const config = moduleRegistry.getConfig(moduleId)
|
const config = moduleRegistry.getConfig(moduleId)
|
||||||
@@ -104,6 +112,7 @@ export const useProcessStore = defineStore('process', () => {
|
|||||||
return {
|
return {
|
||||||
processes,
|
processes,
|
||||||
initListener,
|
initListener,
|
||||||
|
destroyListener,
|
||||||
startByModule,
|
startByModule,
|
||||||
stopByModule,
|
stopByModule,
|
||||||
refreshStatus,
|
refreshStatus,
|
||||||
|
|||||||
@@ -191,6 +191,15 @@ export const useProxyStore = defineStore('proxy', () => {
|
|||||||
return settings.value
|
return settings.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 刷新系统代理真实状态(读取 Windows 注册表,不依赖 settings.json 缓存) */
|
||||||
|
const refreshSystemProxy = async () => {
|
||||||
|
try {
|
||||||
|
systemProxy.value = await commands.proxyGetSystemProxy()
|
||||||
|
} catch {
|
||||||
|
/* 忽略:后端可能未就绪 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const saveSettings = async (s: FullProxySettings) => {
|
const saveSettings = async (s: FullProxySettings) => {
|
||||||
await commands.proxySaveSettings(s)
|
await commands.proxySaveSettings(s)
|
||||||
settings.value = s
|
settings.value = s
|
||||||
@@ -342,6 +351,7 @@ export const useProxyStore = defineStore('proxy', () => {
|
|||||||
// kernel & process
|
// kernel & process
|
||||||
refreshKernel,
|
refreshKernel,
|
||||||
refreshStatus,
|
refreshStatus,
|
||||||
|
refreshSystemProxy,
|
||||||
start,
|
start,
|
||||||
stop,
|
stop,
|
||||||
restart,
|
restart,
|
||||||
|
|||||||
+254
-13
@@ -5,7 +5,7 @@ import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
|
|||||||
import { availableMonitors } from '@tauri-apps/api/window'
|
import { availableMonitors } from '@tauri-apps/api/window'
|
||||||
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
|
import { emit, listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { EVENTS, WINDOWS } from '@/lib/constants'
|
import { EVENTS, STORAGE_KEYS, WINDOWS } from '@/lib/constants'
|
||||||
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts)
|
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts)
|
||||||
import { commands } from '@/lib/bindings'
|
import { commands } from '@/lib/bindings'
|
||||||
|
|
||||||
@@ -19,6 +19,9 @@ export interface RecentCapture {
|
|||||||
height: number
|
height: number
|
||||||
time: number
|
time: number
|
||||||
mode: string
|
mode: string
|
||||||
|
/** 截图时框选区域的物理屏幕坐标(贴图窗口按原位置显示;编辑器导出可能缺失) */
|
||||||
|
posX?: number
|
||||||
|
posY?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 截图设置(localStorage 持久化) */
|
/** 截图设置(localStorage 持久化) */
|
||||||
@@ -31,16 +34,21 @@ export interface ScreenshotSettings {
|
|||||||
historyLimit: number
|
historyLimit: number
|
||||||
/** 全局截图快捷键(空字符串表示禁用) */
|
/** 全局截图快捷键(空字符串表示禁用) */
|
||||||
shortcut: string
|
shortcut: string
|
||||||
|
/** 贴图全局快捷键(空字符串表示禁用),再次按下关闭贴图 */
|
||||||
|
pinShortcut: string
|
||||||
/** 截图延时(秒),0 表示立即截图 */
|
/** 截图延时(秒),0 表示立即截图 */
|
||||||
delay: number
|
delay: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_KEY = 'screenshot-settings'
|
const SETTINGS_KEY = 'screenshot-settings'
|
||||||
|
/** 设置版本:结构变更(默认值/字段增删)时递增,清除旧数据以应用新默认值 */
|
||||||
|
const SETTINGS_VERSION = 2
|
||||||
const DEFAULT_SETTINGS: ScreenshotSettings = {
|
const DEFAULT_SETTINGS: ScreenshotSettings = {
|
||||||
autoSave: false,
|
autoSave: false,
|
||||||
saveDir: '',
|
saveDir: '',
|
||||||
historyLimit: 12,
|
historyLimit: 48,
|
||||||
shortcut: 'ctrl+alt+a',
|
shortcut: 'ctrl+1',
|
||||||
|
pinShortcut: 'alt+1',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +59,8 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
|
|
||||||
let exportUnlisten: UnlistenFn | null = null
|
let exportUnlisten: UnlistenFn | null = null
|
||||||
let shortcutUnlisten: UnlistenFn | null = null
|
let shortcutUnlisten: UnlistenFn | null = null
|
||||||
|
/** 覆盖层就绪事件监听(initOverlay 注册,destroyExportListener 一并释放) */
|
||||||
|
let overlayReadyUnlisten: UnlistenFn | null = null
|
||||||
/** 常驻截图覆盖层窗口(启动时创建,之后每次截图复用,避免重复 WebView 初始化) */
|
/** 常驻截图覆盖层窗口(启动时创建,之后每次截图复用,避免重复 WebView 初始化) */
|
||||||
const OVERLAY_LABEL = WINDOWS.screenshotOverlay
|
const OVERLAY_LABEL = WINDOWS.screenshotOverlay
|
||||||
let overlayWin: WebviewWindow | null = null
|
let overlayWin: WebviewWindow | null = null
|
||||||
@@ -58,14 +68,36 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
let overlayReadyPromise: Promise<void> | null = null
|
let overlayReadyPromise: Promise<void> | null = null
|
||||||
let readyListenerInit = false
|
let readyListenerInit = false
|
||||||
|
|
||||||
|
// ===== 贴图窗口 =====
|
||||||
|
const PIN_LABEL = WINDOWS.screenshotPin
|
||||||
|
const PIN_INDEX_KEY = STORAGE_KEYS.screenshotPinIndex
|
||||||
|
/** 常驻贴图窗口(首次贴图时创建,之后复用;无边框透明置顶) */
|
||||||
|
let pinWin: WebviewWindow | null = null
|
||||||
|
let pinReadyUnlisten: UnlistenFn | null = null
|
||||||
|
let pinReadyListenerInit = false
|
||||||
|
let pinReadyResolve: (() => void) | null = null
|
||||||
|
let pinReadyPromise: Promise<void> | null = null
|
||||||
|
/** 贴图全局快捷键事件监听 */
|
||||||
|
let pinShortcutUnlisten: UnlistenFn | null = null
|
||||||
|
|
||||||
|
// ===== 历史持久化 =====
|
||||||
|
const HISTORY_KEY = STORAGE_KEYS.screenshotHistory
|
||||||
|
const HISTORY_VERSION = 1
|
||||||
|
|
||||||
// ===== 设置持久化 =====
|
// ===== 设置持久化 =====
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(SETTINGS_KEY)
|
const raw = localStorage.getItem(SETTINGS_KEY)
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw) as Partial<ScreenshotSettings>
|
const parsed = JSON.parse(raw) as Partial<ScreenshotSettings> & { version?: number }
|
||||||
|
if (parsed.version !== SETTINGS_VERSION) {
|
||||||
|
// 结构变更:重置为新默认值(应用新的默认快捷键 / 贴图快捷键 / 保留数量)
|
||||||
|
settings.value = { ...DEFAULT_SETTINGS }
|
||||||
|
saveSettings()
|
||||||
|
} else {
|
||||||
settings.value = { ...DEFAULT_SETTINGS, ...parsed }
|
settings.value = { ...DEFAULT_SETTINGS, ...parsed }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[screenshot] 加载设置失败', e)
|
console.error('[screenshot] 加载设置失败', e)
|
||||||
localStorage.removeItem(SETTINGS_KEY)
|
localStorage.removeItem(SETTINGS_KEY)
|
||||||
@@ -74,7 +106,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings.value))
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ version: SETTINGS_VERSION, ...settings.value }))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[screenshot] 保存设置失败', e)
|
console.error('[screenshot] 保存设置失败', e)
|
||||||
}
|
}
|
||||||
@@ -83,6 +115,11 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
function setSettings(patch: Partial<ScreenshotSettings>) {
|
function setSettings(patch: Partial<ScreenshotSettings>) {
|
||||||
settings.value = { ...settings.value, ...patch }
|
settings.value = { ...settings.value, ...patch }
|
||||||
saveSettings()
|
saveSettings()
|
||||||
|
// 保留数量变更:立即裁剪历史(含删除超限缓存文件)
|
||||||
|
if (patch.historyLimit !== undefined) {
|
||||||
|
enforceHistoryLimit()
|
||||||
|
persistHistory()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 窗口定位 =====
|
// ===== 窗口定位 =====
|
||||||
@@ -193,7 +230,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
if (readyListenerInit) return
|
if (readyListenerInit) return
|
||||||
readyListenerInit = true
|
readyListenerInit = true
|
||||||
resetOverlayReady()
|
resetOverlayReady()
|
||||||
await listen(EVENTS.screenshotOverlayReady, () => {
|
overlayReadyUnlisten = await listen(EVENTS.screenshotOverlayReady, () => {
|
||||||
overlayReadyResolve?.()
|
overlayReadyResolve?.()
|
||||||
})
|
})
|
||||||
await ensureOverlay()
|
await ensureOverlay()
|
||||||
@@ -247,9 +284,8 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 追加历史项:删除超出上限的最旧缓存文件 */
|
/** 按保留数量裁剪历史(超出部分删除缓存文件) */
|
||||||
function addRecent(thumb: string, filePath: string, width: number, height: number, mode: string) {
|
function enforceHistoryLimit() {
|
||||||
recent.value.unshift({ id: crypto.randomUUID(), thumb, filePath, width, height, time: Date.now(), mode })
|
|
||||||
const limit = Math.max(1, settings.value.historyLimit)
|
const limit = Math.max(1, settings.value.historyLimit)
|
||||||
if (recent.value.length > limit) {
|
if (recent.value.length > limit) {
|
||||||
const removed = recent.value.splice(limit)
|
const removed = recent.value.splice(limit)
|
||||||
@@ -259,10 +295,68 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 持久化历史元数据(含缩略图与定位坐标)到 localStorage,供重启恢复 */
|
||||||
|
function persistHistory() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
HISTORY_KEY,
|
||||||
|
JSON.stringify({ version: HISTORY_VERSION, items: recent.value }),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[screenshot] 持久化历史失败(可能超出存储限额)', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用启动时恢复历史:版本不匹配/损坏则丢弃;并按当前保留数量清理超限文件 */
|
||||||
|
function loadHistory() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(HISTORY_KEY)
|
||||||
|
if (!raw) return
|
||||||
|
const parsed = JSON.parse(raw) as { version?: number; items?: RecentCapture[] }
|
||||||
|
if (parsed.version !== HISTORY_VERSION || !Array.isArray(parsed.items)) {
|
||||||
|
localStorage.removeItem(HISTORY_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const limit = Math.max(1, settings.value.historyLimit)
|
||||||
|
const items = parsed.items.slice(0, limit)
|
||||||
|
for (const extra of parsed.items.slice(limit)) {
|
||||||
|
void commands.screenshotDeleteCache(extra.filePath)
|
||||||
|
}
|
||||||
|
recent.value = items
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[screenshot] 恢复历史失败', e)
|
||||||
|
recent.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 追加历史项:持久化 + 删除超出上限的最旧缓存文件 */
|
||||||
|
function addRecent(
|
||||||
|
thumb: string,
|
||||||
|
filePath: string,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
mode: string,
|
||||||
|
pos?: { x: number; y: number },
|
||||||
|
) {
|
||||||
|
recent.value.unshift({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
thumb,
|
||||||
|
filePath,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
time: Date.now(),
|
||||||
|
mode,
|
||||||
|
...(pos ? { posX: pos.x, posY: pos.y } : {}),
|
||||||
|
})
|
||||||
|
enforceHistoryLimit()
|
||||||
|
persistHistory()
|
||||||
|
}
|
||||||
|
|
||||||
/** 移除单个历史项(同时删除缓存文件) */
|
/** 移除单个历史项(同时删除缓存文件) */
|
||||||
async function removeRecent(item: RecentCapture) {
|
async function removeRecent(item: RecentCapture) {
|
||||||
const idx = recent.value.findIndex((r) => r.id === item.id)
|
const idx = recent.value.findIndex((r) => r.id === item.id)
|
||||||
if (idx >= 0) recent.value.splice(idx, 1)
|
if (idx >= 0) recent.value.splice(idx, 1)
|
||||||
|
persistHistory()
|
||||||
try {
|
try {
|
||||||
await commands.screenshotDeleteCache(item.filePath)
|
await commands.screenshotDeleteCache(item.filePath)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -275,6 +369,7 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
void commands.screenshotDeleteCache(item.filePath)
|
void commands.screenshotDeleteCache(item.filePath)
|
||||||
}
|
}
|
||||||
recent.value = []
|
recent.value = []
|
||||||
|
persistHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从历史缓存加载完整 PNG base64(一次性,不常驻) */
|
/** 从历史缓存加载完整 PNG base64(一次性,不常驻) */
|
||||||
@@ -303,13 +398,27 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 覆盖层/编辑器导出处理:记录历史(缩略图 + 缓存落盘)+ 按设置自动保存 */
|
/** 覆盖层/编辑器导出处理:记录历史(缩略图 + 缓存落盘)+ 按设置自动保存 */
|
||||||
async function handleExport(payload: { pngBase64: string; width: number; height: number }) {
|
async function handleExport(payload: {
|
||||||
const { pngBase64, width, height } = payload
|
pngBase64: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
/** 截图时框选区域的物理屏幕坐标(贴图窗口按原位置显示;编辑器导出缺失) */
|
||||||
|
posX?: number
|
||||||
|
posY?: number
|
||||||
|
}) {
|
||||||
|
const { pngBase64, width, height, posX, posY } = payload
|
||||||
try {
|
try {
|
||||||
// 完整图落盘缓存目录,历史只保留缩略图,避免完整 base64 常驻内存
|
// 完整图落盘缓存目录,历史只保留缩略图,避免完整 base64 常驻内存
|
||||||
const filePath = await commands.screenshotSaveCache(pngBase64)
|
const filePath = await commands.screenshotSaveCache(pngBase64)
|
||||||
const thumb = await makeThumb(pngBase64, width, height)
|
const thumb = await makeThumb(pngBase64, width, height)
|
||||||
addRecent(thumb, filePath, width, height, 'capture')
|
addRecent(
|
||||||
|
thumb,
|
||||||
|
filePath,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
'capture',
|
||||||
|
posX !== undefined && posY !== undefined ? { x: posX, y: posY } : undefined,
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[screenshot] 历史缓存失败', e)
|
console.error('[screenshot] 历史缓存失败', e)
|
||||||
}
|
}
|
||||||
@@ -335,7 +444,9 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
pngBase64: string
|
pngBase64: string
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}>('screenshot-exported', (e) => {
|
posX?: number
|
||||||
|
posY?: number
|
||||||
|
}>(EVENTS.screenshotExported, (e) => {
|
||||||
void handleExport(e.payload)
|
void handleExport(e.payload)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -371,11 +482,132 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 贴图窗口 =====
|
||||||
|
function resetPinReady() {
|
||||||
|
pinReadyPromise = new Promise<void>((resolve) => {
|
||||||
|
pinReadyResolve = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 等待贴图窗口就绪(首次创建时等 onMounted 完成,之后立即返回) */
|
||||||
|
async function waitPinReady() {
|
||||||
|
await Promise.race([
|
||||||
|
pinReadyPromise,
|
||||||
|
new Promise<void>((r) => setTimeout(r, 3000)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册贴图窗口就绪监听(首次贴图前调用) */
|
||||||
|
async function ensurePinReadyListener() {
|
||||||
|
if (pinReadyListenerInit) return
|
||||||
|
pinReadyListenerInit = true
|
||||||
|
resetPinReady()
|
||||||
|
pinReadyUnlisten = await listen(EVENTS.screenshotPinReady, () => {
|
||||||
|
pinReadyResolve?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建常驻贴图窗口(无边框、透明、置顶、无任务栏;首次贴图时创建,之后复用) */
|
||||||
|
async function ensurePinWindow() {
|
||||||
|
if (pinWin) {
|
||||||
|
const existing = await WebviewWindow.getByLabel(PIN_LABEL).catch(() => null)
|
||||||
|
if (existing) {
|
||||||
|
pinWin = existing
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pinWin = null
|
||||||
|
}
|
||||||
|
resetPinReady()
|
||||||
|
pinWin = new WebviewWindow(PIN_LABEL, {
|
||||||
|
url: 'index.html#screenshot-pin',
|
||||||
|
title: '贴图',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
// 创建时必须 resizable:true(Windows 上非 resizable 窗口 setSize 会失效);
|
||||||
|
// 每次布局结束后在贴图窗口内恢复 setResizable(false),禁用边缘拖动缩放,仅保留左键自由拖动
|
||||||
|
resizable: true,
|
||||||
|
decorations: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
skipTaskbar: true,
|
||||||
|
transparent: true,
|
||||||
|
shadow: false,
|
||||||
|
focus: false,
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
await waitPinReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换贴图:已打开则关闭;否则把第 index 张截图贴到屏幕(原框选位置)。
|
||||||
|
* index 省略时为最近一张截图。
|
||||||
|
*/
|
||||||
|
async function togglePin(index?: number) {
|
||||||
|
if (!recent.value.length) {
|
||||||
|
toast('暂无截图可贴图')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await ensurePinReadyListener()
|
||||||
|
if (pinWin) {
|
||||||
|
try {
|
||||||
|
if (await pinWin.isVisible()) {
|
||||||
|
await pinWin.hide()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 窗口可能尚未就绪,忽略 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const i = Math.max(0, Math.min(index ?? 0, recent.value.length - 1))
|
||||||
|
localStorage.setItem(PIN_INDEX_KEY, String(i))
|
||||||
|
await ensurePinWindow()
|
||||||
|
await emit(EVENTS.screenshotPinShow)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听 Rust 侧 emit 的 'screenshot-pin-shortcut' 事件(贴图快捷键按下时切换) */
|
||||||
|
async function initPinShortcutListener() {
|
||||||
|
if (pinShortcutUnlisten) return
|
||||||
|
pinShortcutUnlisten = await listen(EVENTS.screenshotPinShortcut, () => {
|
||||||
|
void togglePin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用启动时按已保存的设置注册贴图全局快捷键(默认 Alt+1) */
|
||||||
|
async function initPinShortcutRegistration() {
|
||||||
|
try {
|
||||||
|
await commands.screenshotRegisterPinShortcut(settings.value.pinShortcut)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[screenshot] 贴图快捷键注册失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改贴图快捷键:持久化 + 重新注册(传空字符串禁用) */
|
||||||
|
async function setPinShortcut(shortcut: string) {
|
||||||
|
const next = shortcut.trim()
|
||||||
|
setSettings({ pinShortcut: next })
|
||||||
|
try {
|
||||||
|
await commands.screenshotRegisterPinShortcut(next)
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[screenshot] 贴图快捷键注册失败', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function destroyExportListener() {
|
function destroyExportListener() {
|
||||||
if (exportUnlisten) {
|
if (exportUnlisten) {
|
||||||
exportUnlisten()
|
exportUnlisten()
|
||||||
exportUnlisten = null
|
exportUnlisten = null
|
||||||
}
|
}
|
||||||
|
if (overlayReadyUnlisten) {
|
||||||
|
overlayReadyUnlisten()
|
||||||
|
overlayReadyUnlisten = null
|
||||||
|
}
|
||||||
|
if (pinReadyUnlisten) {
|
||||||
|
pinReadyUnlisten()
|
||||||
|
pinReadyUnlisten = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroyShortcutListener() {
|
function destroyShortcutListener() {
|
||||||
@@ -383,6 +615,10 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
shortcutUnlisten()
|
shortcutUnlisten()
|
||||||
shortcutUnlisten = null
|
shortcutUnlisten = null
|
||||||
}
|
}
|
||||||
|
if (pinShortcutUnlisten) {
|
||||||
|
pinShortcutUnlisten()
|
||||||
|
pinShortcutUnlisten = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -400,12 +636,17 @@ export const useScreenshotStore = defineStore('screenshot', () => {
|
|||||||
removeRecent,
|
removeRecent,
|
||||||
loadFullImage,
|
loadFullImage,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
|
loadHistory,
|
||||||
setSettings,
|
setSettings,
|
||||||
setShortcut,
|
setShortcut,
|
||||||
|
togglePin,
|
||||||
|
setPinShortcut,
|
||||||
initExportListener,
|
initExportListener,
|
||||||
destroyExportListener,
|
destroyExportListener,
|
||||||
initShortcutListener,
|
initShortcutListener,
|
||||||
initShortcutRegistration,
|
initShortcutRegistration,
|
||||||
destroyShortcutListener,
|
destroyShortcutListener,
|
||||||
|
initPinShortcutListener,
|
||||||
|
initPinShortcutRegistration,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ export default defineConfig(async () => ({
|
|||||||
input: {
|
input: {
|
||||||
main: fileURLToPath(new URL("./index.html", import.meta.url)),
|
main: fileURLToPath(new URL("./index.html", import.meta.url)),
|
||||||
},
|
},
|
||||||
|
// 分包策略:vendor 独立 chunk,业务迭代时不强制重下载依赖
|
||||||
|
// - vue/pinia/@vueuse: Vue 生态核心,变更频率低
|
||||||
|
// - reka-ui/@lucide: UI 组件库,体积较大且稳定
|
||||||
|
// - tauri: Tauri 前端 API,仅在 Tauri 升级时变化
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'vue-vendor': ['vue', 'pinia', '@vueuse/core'],
|
||||||
|
'ui-vendor': ['reka-ui', '@lucide/vue'],
|
||||||
|
'tauri-vendor': [
|
||||||
|
'@tauri-apps/api',
|
||||||
|
'@tauri-apps/plugin-autostart',
|
||||||
|
'@tauri-apps/plugin-dialog',
|
||||||
|
'@tauri-apps/plugin-global-shortcut',
|
||||||
|
'@tauri-apps/plugin-opener',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user