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==",
|
"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",
|
||||||
|
|||||||
@@ -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;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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
@@ -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=
|
||||||
|
|||||||
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"
|
"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
@@ -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"
|
"email": "1297754537@qq.com"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"productname": "go_win_template",
|
"productname": "青梧映星",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"copyright": ""
|
"companyname": "青梧映星软件开发工作室",
|
||||||
|
"copyright": "© 2026 青梧映星软件开发工作室"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||