Compare commits

..

2 Commits

Author SHA1 Message Date
424847ff46 移除产物 2026-09-18 01:30:35 +08:00
a2a1161f10 feat: 接入托盘 / 添加图标生成工具 / 重命名并完善前端
- 托盘:用 getlantern/systray 重写 internal/tray,支持显示/隐藏窗口与退出
- 图标:新增 pkg/icongen 工具,支持 png/jpg/gif/bmp/webp,输出 ico/png 多个尺寸
- 重命名:项目默认名改为「青梧映星」,wails.json/productname/companyname/copyright 同步更新
- 顶栏:新增顶部导航栏(主题切换、设置、用户),iconfont 自托管 fa-solid-900.woff2
- 主题:CSS 变量 + html[data-theme] 控制白昼/黑夜切换,同时调用 WindowSetDarkTheme/LightTheme
- 底部:版权栏包含工作室、开发者与点击跳转客服链接
- 打包:wails build + exe 版本更新至 1.20 MB
2026-09-18 01:30:13 +08:00
19 changed files with 755 additions and 259 deletions

10
.idea/go.imports.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 637 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -50,7 +50,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.7", "@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7", "@babel/generator": "^7.29.7",
@@ -1226,7 +1225,6 @@
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -1295,7 +1293,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.11.23", "baseline-browser-mapping": "^2.11.23",
"caniuse-lite": "^1.0.30001810", "caniuse-lite": "^1.0.30001810",
@@ -1557,7 +1554,6 @@
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -1599,7 +1595,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -1766,7 +1761,6 @@
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0 || ^0.28.0", "esbuild": "^0.27.0 || ^0.28.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",

View File

@@ -1 +1 @@
ebabc2ea396231dd6d5148053c60fb95 30c0aca688848662b14455d10c0b255b

View File

@@ -1,34 +1,198 @@
@font-face {
font-family: 'iconfont';
src: url('./assets/fonts/fa/fa-solid-900.woff2') format('woff2');
font-weight: 900;
font-style: normal;
font-display: block;
}
:root {
--bg-page: #f0f2f5;
--bg-surface: #ffffff;
--bg-header: #ffffff;
--bg-input: #ffffff;
--bg-th: #fafafa;
--bg-btn-secondary: #f5f5f5;
--bg-btn-secondary-hover: #e8e8e8;
--border: #e5e5e5;
--border-input: #d9d9d9;
--border-cell: #f0f0f0;
--text-title: #1a1a2e;
--text-body: #333333;
--text-secondary: #666666;
--text-muted: #888888;
--text-faint: #999999;
--text-th: #555555;
--placeholder: #999999;
--accent: #1677ff;
--accent-hover: #4096ff;
--accent-soft: rgba(22, 119, 255, 0.1);
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--toast-bg: rgba(26, 26, 46, 0.9);
--toast-color: #ffffff;
}
html[data-theme='dark'] {
--bg-page: #12151f;
--bg-surface: #1c2230;
--bg-header: #1c2230;
--bg-input: #12151f;
--bg-th: #171c28;
--bg-btn-secondary: #262d3d;
--bg-btn-secondary-hover: #38435a;
--border: #262d3d;
--border-input: #2e3648;
--border-cell: #262d3d;
--text-title: #e6e9f0;
--text-body: #d0d5e0;
--text-secondary: #8a93a6;
--text-muted: #8a93a6;
--text-faint: #6b7488;
--text-th: #a6aebd;
--placeholder: #6b7488;
--accent: #1677ff;
--accent-hover: #66a3ff;
--accent-soft: rgba(22, 119, 255, 0.18);
--shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
--toast-bg: rgba(230, 233, 240, 0.95);
--toast-color: #1c2230;
}
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
html,
body {
background: var(--bg-page);
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f0f2f5; color: var(--text-body);
color: #333; }
.icon {
font-family: 'iconfont' !important;
font-style: normal;
font-weight: 900;
line-height: 1;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-sun::before {
content: '\f185';
}
.icon-moon::before {
content: '\f186';
}
.icon-gear::before {
content: '\f013';
}
.icon-user::before {
content: '\f007';
} }
.app { .app {
min-height: 100vh;
display: flex;
flex-direction: column;
color: var(--text-body);
}
.topbar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: flex-end;
height: 44px;
padding: 0 16px;
background: var(--bg-header);
border-bottom: 1px solid var(--border);
}
.topbar-actions {
display: flex;
align-items: center;
gap: 6px;
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.icon-btn:hover {
background: var(--accent-soft);
color: var(--accent);
}
.app-content {
max-width: 900px; max-width: 900px;
width: 100%;
flex: 1;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 16px 24px 24px;
}
.footer {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-top: 1px solid var(--border);
background: var(--bg-surface);
color: var(--text-muted);
font-size: 13px;
}
.footer-sep {
color: var(--border-input);
}
.footer-link {
color: var(--accent);
cursor: pointer;
text-decoration: none;
}
.footer-link:hover {
color: var(--accent-hover);
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 32px; margin-bottom: 16px;
} }
.header h1 { .header h1 {
font-size: 28px; font-size: 22px;
color: #1a1a2e; color: var(--text-title);
} }
.header p { .header p {
color: #666; color: var(--text-secondary);
margin-top: 4px; margin-top: 2px;
font-size: 13px;
} }
.main { .main {
@@ -38,16 +202,16 @@ body {
} }
.card { .card {
background: #fff; background: var(--bg-surface);
border-radius: 12px; border-radius: 12px;
padding: 24px; padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08); box-shadow: var(--shadow);
} }
.card h2 { .card h2 {
font-size: 18px; font-size: 18px;
margin-bottom: 16px; margin-bottom: 16px;
color: #1a1a2e; color: var(--text-title);
} }
.card-header { .card-header {
@@ -65,19 +229,25 @@ body {
.input { .input {
flex: 1; flex: 1;
padding: 8px 12px; padding: 8px 12px;
border: 1px solid #d9d9d9; border: 1px solid var(--border-input);
border-radius: 6px; border-radius: 6px;
font-size: 14px; font-size: 14px;
background: var(--bg-input);
color: var(--text-body);
}
.input::placeholder {
color: var(--placeholder);
} }
.input:focus { .input:focus {
outline: none; outline: none;
border-color: #1677ff; border-color: var(--accent);
} }
.btn-primary { .btn-primary {
padding: 8px 20px; padding: 8px 20px;
background: #1677ff; background: var(--accent);
color: #fff; color: #fff;
border: none; border: none;
border-radius: 6px; border-radius: 6px;
@@ -86,20 +256,21 @@ body {
} }
.btn-primary:hover { .btn-primary:hover {
background: #4096ff; background: var(--accent-hover);
} }
.btn-secondary { .btn-secondary {
padding: 6px 14px; padding: 6px 14px;
background: #f5f5f5; background: var(--bg-btn-secondary);
border: 1px solid #d9d9d9; border: 1px solid var(--border-input);
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 13px;
color: var(--text-body);
} }
.btn-secondary:hover { .btn-secondary:hover {
background: #e8e8e8; background: var(--bg-btn-secondary-hover);
} }
.message { .message {
@@ -116,19 +287,45 @@ body {
.table th, .table th,
.table td { .table td {
padding: 10px 12px; padding: 10px 12px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid var(--border-cell);
text-align: left; text-align: left;
font-size: 14px; font-size: 14px;
} }
.table th { .table th {
background: #fafafa; background: var(--bg-th);
font-weight: 600; font-weight: 600;
color: #555; color: var(--text-th);
} }
.empty { .empty {
text-align: center; text-align: center;
color: #999; color: var(--text-faint);
padding: 24px; padding: 24px;
} }
.toast {
position: fixed;
left: 50%;
bottom: 72px;
transform: translateX(-50%);
z-index: 100;
padding: 10px 18px;
border-radius: 8px;
font-size: 13px;
background: var(--toast-bg);
color: var(--toast-color);
box-shadow: var(--shadow);
animation: toast-in 0.2s ease;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateX(-50%) translateY(8px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}

View File

@@ -1,9 +1,14 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import './App.css'; import './App.css';
import { import {
GetUserList, GetUserList,
CreateUser, CreateUser,
} from '../wailsjs/go/main/App'; } from '../wailsjs/go/main/App';
import {
BrowserOpenURL,
WindowSetDarkTheme,
WindowSetLightTheme,
} from '../wailsjs/runtime/runtime';
interface User { interface User {
id: number; id: number;
@@ -12,11 +17,44 @@ interface User {
created_at: string; created_at: string;
} }
type Theme = 'light' | 'dark';
function getInitialTheme(): Theme {
const saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') {
return saved;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
function App() { function App() {
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [theme, setTheme] = useState<Theme>(getInitialTheme);
const [toast, setToast] = useState('');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
if (theme === 'dark') {
WindowSetDarkTheme();
} else {
WindowSetLightTheme();
}
}, [theme]);
const toggleTheme = () => {
setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
};
const showToast = (text: string) => {
setToast(text);
window.setTimeout(() => setToast(''), 2200);
};
const loadUsers = async () => { const loadUsers = async () => {
const result = await GetUserList(); const result = await GetUserList();
@@ -41,67 +79,114 @@ function App() {
return ( return (
<div className="app"> <div className="app">
<header className="header"> <nav className="topbar">
<h1>Wails + React + Go</h1> <div className="topbar-actions">
<p>Windows 桌面应用框架示例</p> <button
</header> className="icon-btn"
title={theme === 'dark' ? '切换到白天' : '切换到黑夜'}
onClick={toggleTheme}
>
<i className={`icon ${theme === 'dark' ? 'icon-sun' : 'icon-moon'}`} />
</button>
<button
className="icon-btn"
title="设置"
onClick={() => showToast('设置功能开发中…')}
>
<i className="icon icon-gear" />
</button>
<button
className="icon-btn"
title="用户"
onClick={() => showToast('用户中心开发中…')}
>
<i className="icon icon-user" />
</button>
</div>
</nav>
<main className="main"> <div className="app-content">
<section className="card"> <header className="header">
<h2>创建用户</h2> <h1>青梧映星</h1>
<div className="form-row"> <p>Windows 桌面应用框架示例</p>
<input </header>
className="input"
placeholder="名称"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
className="input"
placeholder="邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button className="btn-primary" onClick={handleCreate}>
创建
</button>
</div>
{message && <p className="message">{message}</p>}
</section>
<section className="card"> <main className="main">
<div className="card-header"> <section className="card">
<h2>用户列表</h2> <h2>创建用户</h2>
<button className="btn-secondary" onClick={loadUsers}> <div className="form-row">
刷新 <input
</button> className="input"
</div> placeholder="名称"
{users.length === 0 ? ( value={name}
<p className="empty">暂无数据</p> onChange={(e) => setName(e.target.value)}
) : ( />
<table className="table"> <input
<thead> className="input"
<tr> placeholder="邮箱"
<th>ID</th> value={email}
<th>名称</th> onChange={(e) => setEmail(e.target.value)}
<th>邮箱</th> />
<th>创建时间</th> <button className="btn-primary" onClick={handleCreate}>
</tr> 创建
</thead> </button>
<tbody> </div>
{users.map((u) => ( {message && <p className="message">{message}</p>}
<tr key={u.id}> </section>
<td>{u.id}</td>
<td>{u.name}</td> <section className="card">
<td>{u.email}</td> <div className="card-header">
<td>{u.created_at}</td> <h2>用户列表</h2>
<button className="btn-secondary" onClick={loadUsers}>
刷新
</button>
</div>
{users.length === 0 ? (
<p className="empty">暂无数据</p>
) : (
<table className="table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>邮箱</th>
<th>创建时间</th>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {users.map((u) => (
)} <tr key={u.id}>
</section> <td>{u.id}</td>
</main> <td>{u.name}</td>
<td>{u.email}</td>
<td>{u.created_at}</td>
</tr>
))}
</tbody>
</table>
)}
</section>
</main>
</div>
<footer className="footer">
<span>青梧映星软件开发工作室</span>
<span className="footer-sep">|</span>
<span>开发者: Geliebte</span>
<span className="footer-sep">|</span>
<a
className="footer-link"
href="https://yser.ging.plus/"
onClick={(e) => {
e.preventDefault();
BrowserOpenURL('https://yser.ging.plus/');
}}
>
客户服务中心
</a>
</footer>
{toast && <div className="toast">{toast}</div>}
</div> </div>
); );
} }

Binary file not shown.

12
go.mod
View File

@@ -3,14 +3,22 @@ module go_win_template
go 1.25.0 go 1.25.0
require ( require (
github.com/getlantern/systray v1.2.2
github.com/wailsapp/wails/v2 v2.16.0 github.com/wailsapp/wails/v2 v2.16.0
golang.org/x/sys v0.46.0 golang.org/x/image v0.41.0
) )
require ( require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
@@ -23,6 +31,7 @@ require (
github.com/leaanthony/u v1.1.1 // indirect github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
@@ -34,6 +43,7 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.53.0 // indirect golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect golang.org/x/text v0.39.0 // indirect
) )

28
go.sum
View File

@@ -2,10 +2,27 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -28,6 +45,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M= github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
@@ -36,6 +55,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -47,6 +68,9 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ= github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
@@ -63,10 +87,13 @@ github.com/wailsapp/wails/v2 v2.16.0 h1:hLYrSHUUq6hRXl3rWT9ZXiyFIgM94nVN9Ns6iEJz
github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M= github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -79,5 +106,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

Binary file not shown.

125
internal/tray/tray.go Normal file
View File

@@ -0,0 +1,125 @@
// Package tray 系统托盘管理器,基于 getlantern/systray 跨平台实现。
// Windows 下图标需为 .ico 数据,macOS/Linux 下可为 .png/.jpg/.ico。
package tray
import (
"context"
"sync"
"github.com/getlantern/systray"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// Options 托盘初始化参数
type Options struct {
// Icon 托盘图标数据
Icon []byte
// Tooltip 悬停提示文本
Tooltip string
// OnShow 点击"显示窗口"时回调,为空时默认调用 runtime.WindowShow
OnShow func()
// OnHide 点击"隐藏窗口"时回调,为空时默认调用 runtime.WindowHide
OnHide func()
// OnQuit 点击"退出"时回调,为空时默认调用 runtime.Quit
OnQuit func()
}
// Manager 系统托盘管理器
type Manager struct {
mu sync.Mutex
opts Options
started bool
ready chan struct{}
quitItem *systray.MenuItem
}
// New 创建托盘管理器,Run 之前不会显示图标
func New(opts Options) *Manager {
return &Manager{
opts: opts,
ready: make(chan struct{}),
}
}
// Run 在后台 goroutine 中启动托盘事件循环(非阻塞)。
// ctx 用于窗口显示/隐藏/退出。
func (m *Manager) Run(ctx context.Context) {
m.mu.Lock()
if m.started {
m.mu.Unlock()
return
}
m.started = true
m.mu.Unlock()
go func() {
systray.Run(func() {
m.onReady(ctx)
close(m.ready)
}, func() {
m.mu.Lock()
m.started = false
m.mu.Unlock()
})
}()
}
// WaitReady 阻塞直到托盘初始化完成(可选)
func (m *Manager) WaitReady() {
<-m.ready
}
// Active 托盘是否已成功启动
func (m *Manager) Active() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.started
}
// Quit 移除托盘图标并停止托盘 goroutine
func (m *Manager) Quit() {
systray.Quit()
}
func (m *Manager) onReady(ctx context.Context) {
if len(m.opts.Icon) > 0 {
systray.SetIcon(m.opts.Icon)
}
if m.opts.Tooltip != "" {
systray.SetTooltip(m.opts.Tooltip)
}
show := systray.AddMenuItem("显示窗口", "Show the main window")
hide := systray.AddMenuItem("隐藏窗口", "Hide the main window")
systray.AddSeparator()
quitItem := systray.AddMenuItem("退出", "Quit the application")
m.quitItem = quitItem
m.handleMenuClicks(ctx, show, hide, quitItem)
}
func (m *Manager) handleMenuClicks(ctx context.Context, show, hide, quit *systray.MenuItem) {
for {
select {
case <-show.ClickedCh:
if m.opts.OnShow != nil {
m.opts.OnShow()
} else {
runtime.WindowShow(ctx)
}
case <-hide.ClickedCh:
if m.opts.OnHide != nil {
m.opts.OnHide()
} else {
runtime.WindowHide(ctx)
}
case <-quit.ClickedCh:
if m.opts.OnQuit != nil {
m.opts.OnQuit()
} else {
runtime.Quit(ctx)
}
}
}
}

View File

@@ -1,167 +0,0 @@
// Package tray Windows 系统托盘实现(使用 win32 API)
package tray
import (
"log"
"sync"
"unsafe"
"golang.org/x/sys/windows"
)
// Hook 托盘图标句柄
var hook windows.Handle
// MenuHandle 菜单句柄
type MenuHandle uintptr
// Tray 系统托盘管理器
type Tray struct {
mu sync.Mutex
visible bool
callback uintptr // WNDPROC 回调
hWnd windows.Handle
}
// MenuItem 菜单项
type MenuItem struct {
ID uint32
Text string
}
// NewTray 创建系统托盘
func NewTray(iconData []byte, tooltip string) *Tray {
t := &Tray{}
if err := t.init(iconData, tooltip); err != nil {
log.Printf("[tray] init failed: %v", err)
return nil
}
return t
}
func (t *Tray) init(iconData []byte, tooltip string) error {
// 获取当前进程窗口
hWnd, _ := windows.GetForegroundWindow()
if hWnd == 0 {
return windows.ERROR_ACCESS_DENIED
}
t.hWnd = hWnd
// 注册托盘图标
// NOTIFYICONDATA structure
// https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
data := &notifyIconData{
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
HWnd: uint32(hWnd),
UID: 1000,
UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP,
UCallbackMessage: WM_USER + 1000,
HIcon: 0, // 使用默认图标
}
// 设置 tooltip
for i := 0; i < len(tooltip) && i < 128; i++ {
data.Tip[i] = uint8(tooltip[i])
}
data.Tip[len(tooltip)] = 0
// 添加托盘图标
ret, _, _ := procShellNotifyIcon.Call(
SHM_ADD,
uintptr(unsafe.Pointer(data)),
)
if ret == 0 {
return windows.GetLastError()
}
t.visible = true
return nil
}
// Destroy 销毁托盘图标
func (t *Tray) Destroy() {
t.mu.Lock()
defer t.mu.Unlock()
if !t.visible {
return
}
data := &notifyIconData{
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
HWnd: uint32(t.hWnd),
UID: 1000,
UFlags: NIF_MESSAGE,
}
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
t.visible = false
}
// Show 显示托盘
func (t *Tray) Show(iconData []byte, tooltip string) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.visible {
return nil
}
return t.init(iconData, tooltip)
}
// Hide 隐藏托盘
func (t *Tray) Hide() {
t.mu.Lock()
defer t.mu.Unlock()
if !t.visible {
return
}
data := &notifyIconData{
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
HWnd: uint32(t.hWnd),
UID: 1000,
UFlags: NIF_MESSAGE,
}
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
t.visible = false
}
// IsVisible 是否可见
func (t *Tray) IsVisible() bool {
t.mu.Lock()
defer t.mu.Unlock()
return t.visible
}
// ─────────────────────────────────────────────
// Win32 API 定义
// ─────────────────────────────────────────────
const (
WM_USER = 0x0400
SHM_ADD uintptr = 0x00000000
SHM_DELETE uintptr = 0x00000001
NIF_MESSAGE = 0x00000001
NIF_ICON = 0x00000002
NIF_TIP = 0x00000004
)
// NOTIFYICONDATA struct
type notifyIconData struct {
CbSize uint32
HWnd uint32
UID uint32
UFlags uint32
UCallbackMessage uint32
HIcon uintptr
Tip [128]uint8
}
var (
procShellNotifyIcon = mustLoad("shell32.dll").MustFindProc("Shell_NotifyIconA")
)
func mustLoad(dllName string) *windows.DynLib {
dll, err := windows.LoadDLL(dllName)
if err != nil {
log.Fatalf("[tray] failed to load %s: %v", dllName, err)
}
return dll
}

55
main.go
View File

@@ -4,23 +4,33 @@ import (
"context" "context"
"embed" "embed"
"log" "log"
"sync/atomic"
"github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"github.com/wailsapp/wails/v2/pkg/runtime"
"go_win_template/internal/plugins" "go_win_template/internal/plugins"
"go_win_template/internal/tray"
) )
//go:embed all:frontend/dist //go:embed all:frontend/dist
//go:embed assets/icon.png //go:embed assets/icon.png
//go:embed build/windows/icon.ico
var assets embed.FS var assets embed.FS
var (
trayMgr *tray.Manager
forceQuit atomic.Bool
)
func main() { func main() {
wailsApp := NewApp() wailsApp := NewApp()
cfg := &options.App{ cfg := &options.App{
Title: "go_win_template", Title: "青梧映星",
Width: 1280, Width: 1280,
Height: 800, Height: 800,
MinWidth: 1024, MinWidth: 1024,
@@ -30,6 +40,10 @@ func main() {
Assets: assets, Assets: assets,
}, },
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
Windows: &windows.Options{
// 窗口标题栏跟随系统白昼/黑夜主题
Theme: windows.SystemDefault,
},
OnStartup: func(ctx context.Context) { OnStartup: func(ctx context.Context) {
log.Println("[main] startup complete") log.Println("[main] startup complete")
// 启动插件 // 启动插件
@@ -37,6 +51,25 @@ func main() {
log.Printf("[main] plugin start error: %v", err) log.Printf("[main] plugin start error: %v", err)
} }
plugins.PrintStatus() plugins.PrintStatus()
// 启动系统托盘
startTray(ctx)
},
OnBeforeClose: func(ctx context.Context) bool {
// 真正退出时不再拦截
if forceQuit.Load() {
return false
}
// 托盘可用时,关闭窗口改为隐藏到托盘
if trayMgr != nil && trayMgr.Active() {
runtime.WindowHide(ctx)
return true
}
return false
},
OnShutdown: func(context.Context) {
if trayMgr != nil {
trayMgr.Quit()
}
}, },
Bind: []interface{}{ Bind: []interface{}{
wailsApp, wailsApp,
@@ -48,3 +81,23 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
} }
// startTray 初始化系统托盘。Windows 下托盘图标需为 .ico 格式,
// 使用 build/windows/icon.ico;macOS/Linux 可改用 .png。
func startTray(ctx context.Context) {
icon, err := assets.ReadFile("build/windows/icon.ico")
if err != nil {
log.Printf("[tray] load icon failed: %v", err)
return
}
trayMgr = tray.New(tray.Options{
Icon: icon,
Tooltip: "青梧映星",
OnQuit: func() {
forceQuit.Store(true)
runtime.Quit(ctx)
},
})
trayMgr.Run(ctx)
}

160
pkg/icongen/main.go Normal file
View File

@@ -0,0 +1,160 @@
// Command icongen 根据一张图片快速生成当前项目的托盘图标与打包图标,
// 已存在的图标文件会被直接覆盖。
//
// 用法(在项目根目录执行):
//
// go run ./pkg/icongen -src /path/to/image.png
//
// 可选参数:
//
// go run ./pkg/icongen -src logo.jpg -root . -appicon 1024 -tray 256
package main
import (
"bytes"
"encoding/binary"
"flag"
"fmt"
"image"
imagedraw "image/draw"
"image/png"
"log"
"os"
"path/filepath"
"golang.org/x/image/draw"
_ "golang.org/x/image/webp"
_ "image/gif"
_ "image/jpeg"
)
// ico 文件中包含的尺寸
var icoSizes = []int{256, 128, 64, 48, 32, 16}
func main() {
src := flag.String("src", "", "源图片路径(png/jpg/jpeg/gif/bmp/webp)")
root := flag.String("root", ".", "项目根目录")
appicon := flag.Int("appicon", 1024, "build/appicon.png 尺寸")
icon := flag.Int("icon", 1024, "build/icon.png 尺寸")
tray := flag.Int("tray", 256, "trayicons/assets 图标尺寸")
flag.Parse()
if *src == "" {
log.Fatal("[-src] 源图片路径不能为空")
}
f, err := os.Open(*src)
if err != nil {
log.Fatalf("打开源图片失败: %v", err)
}
defer f.Close()
srcImg, _, err := image.Decode(f)
if err != nil {
log.Fatalf("解码源图片失败(支持 png/jpg/gif/bmp/webp): %v", err)
}
log.Printf("源图片: %s (%dx%d)", *src, srcImg.Bounds().Dx(), srcImg.Bounds().Dy())
cropped := centerCrop(srcImg)
// PNG 目标:源图居中裁剪后缩放
writePNG(filepath.Join(*root, "build", "appicon.png"), cropped, *appicon)
writePNG(filepath.Join(*root, "build", "icon.png"), cropped, *icon)
writePNG(filepath.Join(*root, "assets", "icon.png"), cropped, *tray)
writePNG(filepath.Join(*root, "trayicons", "icon.png"), cropped, *tray)
// ICO 目标:Windows exe / 托盘图标(多尺寸)
writeICO(filepath.Join(*root, "build", "windows", "icon.ico"), cropped, icoSizes)
log.Println("完成,所有图标已按需覆盖。")
}
// centerCrop 从图像中心裁剪出正方形区域。
func centerCrop(srcImg image.Image) image.Image {
b := srcImg.Bounds()
w, h := b.Dx(), b.Dy()
side := w
if h < side {
side = h
}
x0 := b.Min.X + (w-side)/2
y0 := b.Min.Y + (h-side)/2
rect := image.Rect(0, 0, side, side)
out := image.NewRGBA(rect)
imagedraw.Draw(out, rect, srcImg, image.Pt(x0, y0), imagedraw.Src)
return out
}
// writePNG 按 size 缩放后写出 PNG 文件。
func writePNG(path string, cropped image.Image, size int) {
dst := image.NewRGBA(image.Rect(0, 0, size, size))
draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err)
}
out, err := os.Create(path)
if err != nil {
log.Fatalf("创建文件失败 %s: %v", path, err)
}
defer out.Close()
if err := png.Encode(out, dst); err != nil {
log.Fatalf("编码 PNG 失败 %s: %v", path, err)
}
fmt.Printf(" [ok] %s (%dx%d)\n", path, size, size)
}
// writeICO 写出多尺寸 ICO(Windows 图标,PNG 条目压缩,Vista+ 支持)。
func writeICO(path string, cropped image.Image, sizes []int) {
type entry struct {
w, h byte
data []byte
}
entries := make([]entry, 0, len(sizes))
for _, s := range sizes {
dst := image.NewRGBA(image.Rect(0, 0, s, s))
draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil)
var pngBuf bytes.Buffer
if err := png.Encode(&pngBuf, dst); err != nil {
log.Fatalf("编码 ICO 条目失败 (%dpx): %v", s, err)
}
w, h := byte(s), byte(s)
if s == 256 {
w, h = 0, 0 // 0 表示 256
}
entries = append(entries, entry{w: w, h: h, data: pngBuf.Bytes()})
}
var buf bytes.Buffer
// ICONDIR(6 字节头)
binary.Write(&buf, binary.LittleEndian, uint16(0)) // Reserved
binary.Write(&buf, binary.LittleEndian, uint16(1)) // Type: icon
binary.Write(&buf, binary.LittleEndian, uint16(len(entries)))
// ICONDIRENTRY(16 字节 x N)
cur := 6 + 16*len(entries)
for _, e := range entries {
binary.Write(&buf, binary.LittleEndian, e.w)
binary.Write(&buf, binary.LittleEndian, e.h)
binary.Write(&buf, binary.LittleEndian, uint8(0)) // 颜色数(忽略)
binary.Write(&buf, binary.LittleEndian, uint8(0)) // 保留
binary.Write(&buf, binary.LittleEndian, uint16(1)) // 平面数
binary.Write(&buf, binary.LittleEndian, uint16(32))
binary.Write(&buf, binary.LittleEndian, uint32(len(e.data)))
binary.Write(&buf, binary.LittleEndian, uint32(cur))
cur += len(e.data)
}
// PNG 图像数据
for _, e := range entries {
buf.Write(e.data)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err)
}
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
log.Fatalf("写入 ICO 失败 %s: %v", path, err)
}
fmt.Printf(" [ok] %s (ico %v)\n", path, sizes)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -11,8 +11,9 @@
"email": "1297754537@qq.com" "email": "1297754537@qq.com"
}, },
"info": { "info": {
"productname": "go_win_template", "productname": "青梧映星",
"version": "1.0.0", "version": "1.0.0",
"copyright": "" "companyname": "青梧映星软件开发工作室",
"copyright": "© 2026 青梧映星软件开发工作室"
} }
} }