diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/assets/icon.png b/assets/icon.png index 78872a9..ea518cf 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/build/appicon.png b/build/appicon.png index 63617fe..29183a1 100644 Binary files a/build/appicon.png and b/build/appicon.png differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..29183a1 Binary files /dev/null and b/build/icon.png differ diff --git a/build/windows/icon.ico b/build/windows/icon.ico index f334798..b1577b4 100644 Binary files a/build/windows/icon.ico and b/build/windows/icon.ico differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1c1b0b1..b37b35b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 56e9e6b..ccea674 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -ebabc2ea396231dd6d5148053c60fb95 \ No newline at end of file +30c0aca688848662b14455d10c0b255b \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css index 6c09c02..8daaa7e 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -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); + } +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d617fd0..f776697 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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([]); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [message, setMessage] = useState(''); + const [theme, setTheme] = useState(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,67 +79,114 @@ function App() { return (
-
-

Wails + React + Go

-

Windows 桌面应用框架示例

-
+ -
-
-

创建用户

-
- setName(e.target.value)} - /> - setEmail(e.target.value)} - /> - -
- {message &&

{message}

} -
+
+
+

青梧映星

+

Windows 桌面应用框架示例

+
-
-
-

用户列表

- -
- {users.length === 0 ? ( -

暂无数据

- ) : ( - - - - - - - - - - - {users.map((u) => ( - - - - - +
+
+

创建用户

+
+ setName(e.target.value)} + /> + setEmail(e.target.value)} + /> + +
+ {message &&

{message}

} +
+ +
+
+

用户列表

+ +
+ {users.length === 0 ? ( +

暂无数据

+ ) : ( +
ID名称邮箱创建时间
{u.id}{u.name}{u.email}{u.created_at}
+ + + + + + - ))} - -
ID名称邮箱创建时间
- )} -
-
+ + + {users.map((u) => ( + + {u.id} + {u.name} + {u.email} + {u.created_at} + + ))} + + + )} + + +
+ + + + {toast &&
{toast}
} ); } diff --git a/frontend/src/assets/fonts/fa/fa-solid-900.woff2 b/frontend/src/assets/fonts/fa/fa-solid-900.woff2 new file mode 100644 index 0000000..1672102 Binary files /dev/null and b/frontend/src/assets/fonts/fa/fa-solid-900.woff2 differ diff --git a/go.mod b/go.mod index 684a5ba..7f0625e 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 8235dbe..50b99f5 100644 --- a/go.sum +++ b/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= diff --git a/go_win_template.exe b/go_win_template.exe index 40598fb..e7c45bc 100644 Binary files a/go_win_template.exe and b/go_win_template.exe differ diff --git a/internal/tray/tray.go b/internal/tray/tray.go new file mode 100644 index 0000000..56eff77 --- /dev/null +++ b/internal/tray/tray.go @@ -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) + } + } + } +} diff --git a/internal/tray/win/tray.go b/internal/tray/win/tray.go deleted file mode 100644 index 50d5f28..0000000 --- a/internal/tray/win/tray.go +++ /dev/null @@ -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 -} diff --git a/main.go b/main.go index c658f6e..58c4b07 100644 --- a/main.go +++ b/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) +} diff --git a/pkg/icongen/main.go b/pkg/icongen/main.go new file mode 100644 index 0000000..aa5fb13 --- /dev/null +++ b/pkg/icongen/main.go @@ -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) +} diff --git a/trayicons/icon.png b/trayicons/icon.png index 78872a9..ea518cf 100644 Binary files a/trayicons/icon.png and b/trayicons/icon.png differ diff --git a/wails.json b/wails.json index 9105b88..21fd995 100644 --- a/wails.json +++ b/wails.json @@ -11,8 +11,9 @@ "email": "1297754537@qq.com" }, "info": { - "productname": "go_win_template", + "productname": "青梧映星", "version": "1.0.0", - "copyright": "" + "companyname": "青梧映星软件开发工作室", + "copyright": "© 2026 青梧映星软件开发工作室" } }