feat(tauri): macOS 中文菜单栏 + 单实例运行 + 圆角图标

- 自定义 macOS 菜单栏(美家卡智影 / 编辑 / 窗口)
- 添加 tauri-plugin-single-instance,防止多开
- 重新生成 macOS Big Sur 风格圆角图标(22% 圆角半径)
- 新增 icons/generate-icons.py 脚本
This commit is contained in:
小鱼开发
2026-05-18 23:09:07 +08:00
parent 8d39816673
commit 734a3787fa
10 changed files with 181 additions and 0 deletions
+16
View File
@@ -4235,6 +4235,7 @@ dependencies = [
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"thiserror 1.0.69",
"tokio",
@@ -4416,6 +4417,21 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.18",
"tracing",
"windows-sys 0.60.2",
"zbus",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
+1
View File
@@ -44,4 +44,5 @@ thiserror = "1"
fs2 = "0.4"
# 异步运行时定时器(FFmpeg 超时保护)
tokio = { version = "1", features = ["time"] }
tauri-plugin-single-instance = "2"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""生成 macOS 风格圆角图标(Big Sur 22% 圆角半径)"""
from PIL import Image, ImageDraw
import subprocess
import os
# 配置
LOGO_PATH = "../../public/assets/logo.png"
OUTPUT_DIR = "."
BG_COLOR = (255, 255, 255, 255)
# macOS Big Sur 风格圆角:约 22% 半径
CORNER_RADIUS_RATIO = 0.22
# Logo 缩放比例
LOGO_SCALE = 0.65
def create_rounded_rect(size, radius, fill):
"""创建圆角矩形遮罩"""
img = Image.new("RGBA", size, (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
draw.rounded_rectangle((0, 0, size[0] - 1, size[1] - 1), radius=radius, fill=fill)
return img
def generate_icon(size):
"""生成指定尺寸的图标"""
radius = int(size[0] * CORNER_RADIUS_RATIO)
# 白色圆角背景
bg = create_rounded_rect(size, radius, BG_COLOR)
# 加载并缩放 logo
logo = Image.open(LOGO_PATH).convert("RGBA")
logo_size = int(min(size) * LOGO_SCALE)
logo.thumbnail((logo_size, logo_size), Image.LANCZOS)
# 居中绘制 logo
x = (size[0] - logo.width) // 2
y = (size[1] - logo.height) // 2
bg.paste(logo, (x, y), logo)
return bg
def main():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# 生成各尺寸 PNG
sizes = {
"icon_16x16.png": (16, 16),
"icon_32x32.png": (32, 32),
"icon_128x128.png": (128, 128),
"icon_256x256.png": (256, 256),
"icon_512x512.png": (512, 512),
"icon_512x512@2x.png": (1024, 1024),
}
# 用于 ICNS 的临时目录
icns_dir = "/tmp/icon.iconset"
os.makedirs(icns_dir, exist_ok=True)
for filename, size in sizes.items():
img = generate_icon(size)
img.save(f"{icns_dir}/{filename}", "PNG")
print(f"Generated {filename} ({size[0]}x{size[1]})")
# 复制到输出目录
for src, dst in [
("icon_32x32.png", "32x32.png"),
("icon_128x128.png", "128x128.png"),
("icon_256x256.png", "128x128@2x.png"),
("icon_512x512.png", "icon.png"),
]:
subprocess.run(["cp", f"{icns_dir}/{src}", f"{OUTPUT_DIR}/{dst}"])
print(f"Copied to {dst}")
# 生成 ICNS
subprocess.run(
["iconutil", "-c", "icns", "-o", f"{OUTPUT_DIR}/icon.icns", icns_dir],
check=True,
)
print("Generated icon.icns")
# 生成 ICOWindows 需要多分辨率)
ico_sizes = [16, 32, 48, 64, 128, 256]
ico_images = []
for s in ico_sizes:
img = generate_icon((s, s))
# ICO 需要 RGB 模式
if img.mode == "RGBA":
rgb = Image.new("RGB", img.size, (255, 255, 255))
rgb.paste(img, mask=img.split()[3])
ico_images.append(rgb)
else:
ico_images.append(img)
ico_images[0].save(
f"{OUTPUT_DIR}/icon.ico",
format="ICO",
sizes=[(s, s) for s in ico_sizes],
)
print("Generated icon.ico")
# 清理临时目录
subprocess.run(["rm", "-rf", icns_dir])
print("\n✅ 所有图标已生成")
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+52
View File
@@ -55,6 +55,51 @@ pub fn run() {
if let Ok(app_data_dir) = app.path().app_local_data_dir() {
crate::storage::init_app_data_dir(app_data_dir);
}
// macOS 自定义菜单栏(中文本地化)
#[cfg(target_os = "macos")]
{
use tauri::menu::{AboutMetadata, MenuBuilder, SubmenuBuilder};
let app_menu = SubmenuBuilder::new(app, "美家卡智影")
.about_with_text(
"关于 美家卡智影",
Some(AboutMetadata {
name: Some("美家卡智影".to_string()),
..Default::default()
}),
)
.separator()
.quit_with_text("退出")
.build()?;
let edit_menu = SubmenuBuilder::new(app, "编辑")
.undo_with_text("撤销")
.redo_with_text("重做")
.separator()
.cut_with_text("剪切")
.copy_with_text("复制")
.paste_with_text("粘贴")
.separator()
.select_all_with_text("全选")
.build()?;
let window_menu = SubmenuBuilder::new(app, "窗口")
.minimize_with_text("最小化")
.maximize_with_text("最大化")
.separator()
.close_window_with_text("关闭窗口")
.build()?;
let menu = MenuBuilder::new(app)
.item(&app_menu)
.item(&edit_menu)
.item(&window_menu)
.build()?;
app.set_menu(menu)?;
}
Ok(())
})
.plugin(tauri_plugin_shell::init())
@@ -63,6 +108,13 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// 单实例:第二个实例启动时,将已有窗口带到前台
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.set_focus();
}
}))
.invoke_handler(tauri::generate_handler![
storage::config::load_app_config,
storage::config::save_app_config,