Compare commits
2 Commits
d8789e9034
...
424847ff46
| Author | SHA1 | Date | |
|---|---|---|---|
| 424847ff46 | |||
| a2a1161f10 |
10
.idea/go.imports.xml
generated
Normal 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>
|
||||
BIN
assets/icon.png
|
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 637 KiB |
BIN
build/icon.png
Normal file
|
After Width: | Height: | Size: 637 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 113 KiB |
6
frontend/package-lock.json
generated
@@ -50,7 +50,6 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -1226,7 +1225,6 @@
|
||||
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -1295,7 +1293,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.11.23",
|
||||
"caniuse-lite": "^1.0.30001810",
|
||||
@@ -1557,7 +1554,6 @@
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -1599,7 +1595,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -1766,7 +1761,6 @@
|
||||
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -1 +1 @@
|
||||
ebabc2ea396231dd6d5148053c60fb95
|
||||
30c0aca688848662b14455d10c0b255b
|
||||
@@ -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;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f0f2f5;
|
||||
color: #333;
|
||||
color: var(--text-body);
|
||||
}
|
||||
|
||||
.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 {
|
||||
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;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
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 {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
color: #1a1a2e;
|
||||
font-size: 22px;
|
||||
color: var(--text-title);
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -38,16 +202,16 @@ body {
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
background: var(--bg-surface);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 16px;
|
||||
color: #1a1a2e;
|
||||
color: var(--text-title);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -65,19 +229,25 @@ body {
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid var(--border-input);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-body);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--placeholder);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: #1677ff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 8px 20px;
|
||||
background: #1677ff;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
@@ -86,20 +256,21 @@ body {
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #4096ff;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 6px 14px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: var(--bg-btn-secondary);
|
||||
border: 1px solid var(--border-input);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-body);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e8e8e8;
|
||||
background: var(--bg-btn-secondary-hover);
|
||||
}
|
||||
|
||||
.message {
|
||||
@@ -116,19 +287,45 @@ body {
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid var(--border-cell);
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #fafafa;
|
||||
background: var(--bg-th);
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
color: var(--text-th);
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
color: var(--text-faint);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import './App.css';
|
||||
import {
|
||||
GetUserList,
|
||||
CreateUser,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import {
|
||||
BrowserOpenURL,
|
||||
WindowSetDarkTheme,
|
||||
WindowSetLightTheme,
|
||||
} from '../wailsjs/runtime/runtime';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -12,11 +17,44 @@ interface User {
|
||||
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() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = 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 result = await GetUserList();
|
||||
@@ -41,8 +79,35 @@ function App() {
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<nav className="topbar">
|
||||
<div className="topbar-actions">
|
||||
<button
|
||||
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>
|
||||
|
||||
<div className="app-content">
|
||||
<header className="header">
|
||||
<h1>Wails + React + Go</h1>
|
||||
<h1>青梧映星</h1>
|
||||
<p>Windows 桌面应用框架示例</p>
|
||||
</header>
|
||||
|
||||
@@ -103,6 +168,26 @@ function App() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
BIN
frontend/src/assets/fonts/fa/fa-solid-900.woff2
Normal file
12
go.mod
@@ -3,14 +3,22 @@ module go_win_template
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/wailsapp/wails/v2 v2.16.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/image v0.41.0
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // 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-stack/stack v1.8.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
@@ -23,6 +31,7 @@ require (
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // 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/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
@@ -34,6 +43,7 @@ require (
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.53.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
|
||||
)
|
||||
|
||||
|
||||
28
go.sum
@@ -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=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
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/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/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/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
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.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
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/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
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/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
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/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
125
internal/tray/tray.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 := ¬ifyIconData{
|
||||
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 := ¬ifyIconData{
|
||||
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 := ¬ifyIconData{
|
||||
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
@@ -4,23 +4,33 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"log"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"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/tray"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
//go:embed assets/icon.png
|
||||
//go:embed build/windows/icon.ico
|
||||
var assets embed.FS
|
||||
|
||||
var (
|
||||
trayMgr *tray.Manager
|
||||
forceQuit atomic.Bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
wailsApp := NewApp()
|
||||
|
||||
cfg := &options.App{
|
||||
Title: "go_win_template",
|
||||
Title: "青梧映星",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
MinWidth: 1024,
|
||||
@@ -30,6 +40,10 @@ func main() {
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
Windows: &windows.Options{
|
||||
// 窗口标题栏跟随系统白昼/黑夜主题
|
||||
Theme: windows.SystemDefault,
|
||||
},
|
||||
OnStartup: func(ctx context.Context) {
|
||||
log.Println("[main] startup complete")
|
||||
// 启动插件
|
||||
@@ -37,6 +51,25 @@ func main() {
|
||||
log.Printf("[main] plugin start error: %v", err)
|
||||
}
|
||||
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{}{
|
||||
wailsApp,
|
||||
@@ -48,3 +81,23 @@ func main() {
|
||||
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
@@ -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)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 75 KiB |
@@ -11,8 +11,9 @@
|
||||
"email": "1297754537@qq.com"
|
||||
},
|
||||
"info": {
|
||||
"productname": "go_win_template",
|
||||
"productname": "青梧映星",
|
||||
"version": "1.0.0",
|
||||
"copyright": ""
|
||||
"companyname": "青梧映星软件开发工作室",
|
||||
"copyright": "© 2026 青梧映星软件开发工作室"
|
||||
}
|
||||
}
|
||||
|
||||