From 950e2fada5a85308edfacadde7f9f530d0d5e82c Mon Sep 17 00:00:00 2001 From: mei Date: Tue, 7 Oct 2025 15:01:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(webui):=20=E6=96=B0=E5=A2=9Ewebui=E5=92=8C?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9F=A5=E7=9C=8B=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- main.go | 31 +++++++ static/css/style.css | 194 +++++++++++++++++++++++++++++++++++++++++++ static/js/script.js | 136 ++++++++++++++++++++++++++++++ templates/index.html | 56 +++++++++++++ 5 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 static/css/style.css create mode 100644 static/js/script.js create mode 100644 templates/index.html diff --git a/README.md b/README.md index c1c1f06..0636914 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,4 @@ docker logs goodbaby ## 扫码登陆后才能使用 ## 使用 -定期请求siganl接口,示例:`http://192.168.1.245:8088/signal?secret=xxxx` +访问你开放的端口(默认为8088),进入WebUI,输入你的密钥,点击保存(密钥会存储到浏览器的localStorage中),此时你可以在WebUI查看倒计时和发送Signal diff --git a/main.go b/main.go index b089bdd..004c924 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,12 @@ +// main.go package main import ( + "embed" + "html/template" + "io/fs" "log" + "net/http" "time" "github.com/CuteReimu/bilibili/v2" @@ -10,6 +15,12 @@ import ( "github.com/ssdomei232/goodBaby/internal" ) +//go:embed static/* +var staticFiles embed.FS + +//go:embed templates/* +var templateFiles embed.FS + var timer *time.Timer var duration time.Duration var biliClient *bilibili.Client @@ -58,11 +69,25 @@ func trigger(config configs.Config) { func main() { r := gin.Default() + + // 设置嵌入的模板 + templFS, _ := fs.Sub(templateFiles, "templates") + r.SetHTMLTemplate(loadTemplates(templFS)) + + // 提供嵌入的静态文件 + staticFS, _ := fs.Sub(staticFiles, "static") + r.StaticFS("/static", http.FS(staticFS)) + + r.GET("/", indexPage) r.GET("/signal", handleSignal) r.GET("/timer/status", internal.HandleTimerStatus) r.Run(":8088") } +func indexPage(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", nil) +} + func initBilibili() { // 尝试加载已存储的cookie if !internal.LoadCookies(biliClient) { @@ -107,3 +132,9 @@ func handleSignal(c *gin.Context) { }) log.Println("触发信号") } + +// loadTemplates 从嵌入的文件系统加载模板 +func loadTemplates(filesystem fs.FS) *template.Template { + templ := template.Must(template.New("").ParseFS(filesystem, "*.html")) + return templ +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..0bf8938 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,194 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 800px; + margin: 0 auto; +} + +header { + text-align: center; + color: white; + margin-bottom: 30px; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; +} + +header p { + font-size: 1.2rem; + opacity: 0.9; +} + +.card { + background: rgba(255, 255, 255, 0.95); + border-radius: 15px; + padding: 25px; + margin-bottom: 25px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); +} + +.card h2 { + color: #333; + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 15px; +} + +.form-group label { + font-weight: 600; + color: #555; +} + +.form-group input { + padding: 12px 15px; + border: 2px solid #ddd; + border-radius: 8px; + font-size: 16px; + transition: border-color 0.3s; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; +} + +.btn-primary, .btn-secondary, .btn-danger { + padding: 12px 20px; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-secondary { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; +} + +.btn-danger { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%); + color: white; + padding: 15px 30px; + font-size: 18px; +} + +.btn-primary:hover, .btn-secondary:hover, .btn-danger:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +.status-container { + text-align: center; +} + +.timer-display { + font-size: 3rem; + font-weight: 700; + color: #333; + margin: 20px 0; + font-family: 'Courier New', monospace; +} + +.progress-bar { + width: 100%; + height: 20px; + background-color: #eee; + border-radius: 10px; + overflow: hidden; + margin: 20px 0; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%); + width: 0%; + transition: width 0.5s ease; +} + +.controls { + text-align: center; +} + +.hint { + color: #666; + margin-top: 15px; + font-style: italic; +} + +footer { + text-align: center; + color: white; + margin-top: 30px; + opacity: 0.8; +} + +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 25px; + border-radius: 8px; + color: white; + font-weight: 600; + transform: translateX(400px); + transition: transform 0.3s ease; + z-index: 1000; +} + +.notification.show { + transform: translateX(0); +} + +.notification.success { + background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%); +} + +.notification.error { + background: linear-gradient(135deg, #ff416c 0%, #ff4b2b 100%); +} + +@media (max-width: 768px) { + .container { + padding: 10px; + } + + header h1 { + font-size: 2rem; + } + + .card { + padding: 20px; + } + + .timer-display { + font-size: 2rem; + } +} \ No newline at end of file diff --git a/static/js/script.js b/static/js/script.js new file mode 100644 index 0000000..608e668 --- /dev/null +++ b/static/js/script.js @@ -0,0 +1,136 @@ +class TimerController { + constructor() { + this.secret = localStorage.getItem('timerSecret') || ''; + this.baseURL = window.location.origin; + this.initElements(); + this.bindEvents(); + this.updateSecretDisplay(); + + // 自动刷新状态 + this.startAutoRefresh(); + } + + initElements() { + this.secretInput = document.getElementById('secret'); + this.saveSecretBtn = document.getElementById('save-secret'); + this.refreshStatusBtn = document.getElementById('refresh-status'); + this.sendSignalBtn = document.getElementById('send-signal'); + this.remainingTimeEl = document.getElementById('remaining-time'); + this.progressFillEl = document.getElementById('progress-fill'); + this.notificationEl = document.getElementById('notification'); + } + + bindEvents() { + this.saveSecretBtn.addEventListener('click', () => this.saveSecret()); + this.refreshStatusBtn.addEventListener('click', () => this.fetchStatus()); + this.sendSignalBtn.addEventListener('click', () => this.sendSignal()); + + // 回车键保存密钥 + this.secretInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.saveSecret(); + } + }); + } + + saveSecret() { + const secret = this.secretInput.value.trim(); + if (!secret) { + this.showNotification('请输入密钥', 'error'); + return; + } + + this.secret = secret; + localStorage.setItem('timerSecret', secret); + this.updateSecretDisplay(); + this.showNotification('密钥已保存', 'success'); + this.fetchStatus(); // 保存后自动刷新状态 + } + + updateSecretDisplay() { + if (this.secret) { + this.secretInput.placeholder = '密钥已保存'; + this.secretInput.value = ''; + } else { + this.secretInput.placeholder = '输入您的访问密钥'; + } + } + + async fetchStatus() { + try { + const response = await fetch(`${this.baseURL}/timer/status`); + const data = await response.json(); + + if (data.code === 200) { + this.displayTime(data.remaining_time); + } else { + this.showNotification('获取状态失败', 'error'); + } + } catch (error) { + console.error('Error fetching status:', error); + this.showNotification('网络错误,请稍后重试', 'error'); + } + } + + displayTime(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + this.remainingTimeEl.textContent = + `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + + // 更新进度条(假设最大时间为24小时) + const maxTime = 24 * 3600; // 24小时 + const progress = Math.min(100, (seconds / maxTime) * 100); + this.progressFillEl.style.width = `${100 - progress}%`; + } + + async sendSignal() { + if (!this.secret) { + this.showNotification('请先设置密钥', 'error'); + return; + } + + try { + const response = await fetch(`${this.baseURL}/signal?secret=${encodeURIComponent(this.secret)}`); + const data = await response.json(); + + if (data.code === 200) { + this.showNotification('信号发送成功!', 'success'); + setTimeout(() => this.fetchStatus(), 1000); // 1秒后刷新状态 + } else { + this.showNotification(`操作失败: ${data.message}`, 'error'); + } + } catch (error) { + console.error('Error sending signal:', error); + this.showNotification('网络错误,请稍后重试', 'error'); + } + } + + showNotification(message, type) { + this.notificationEl.textContent = message; + this.notificationEl.className = `notification ${type} show`; + + setTimeout(() => { + this.notificationEl.classList.remove('show'); + }, 3000); + } + + startAutoRefresh() { + // 每30秒自动刷新一次状态 + setInterval(() => { + if (this.secret) { + this.fetchStatus(); + } + }, 30000); + + // 页面加载完成后立即获取一次状态 + setTimeout(() => this.fetchStatus(), 1000); + } +} + +// 初始化应用 +document.addEventListener('DOMContentLoaded', () => { + new TimerController(); +}); \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..df0e562 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,56 @@ + + + + + + GoodBaby + + + +
+
+

GoodBaby控制面板

+
+ +
+
+

🔐 身份验证

+
+ + + +
+
+ +
+

📊 定时器状态

+
+
+ --:--:-- +
+
+
+
+ +
+
+ +
+

⚡ 操作控制

+
+ +

点击按钮重置定时器

+
+
+
+ +
+

© 2025 GoodBaby

+
+
+ +
+ + + + \ No newline at end of file