diff --git a/main.go b/main.go index 9e18fa5..3f9e972 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,3 @@ -// main.go package main import ( @@ -11,6 +10,7 @@ import ( "math/rand" "net/http" "os" + "sort" "strings" "sync" "time" @@ -25,7 +25,9 @@ type Task struct { InStock bool `json:"in_stock"` LastCheck time.Time `json:"last_check"` Status string `json:"status"` - Notified bool `json:"notified"` + Notified bool `json:"notified"` + NotifyEnabled bool `json:"notify_enabled"` + Order int `json:"order"` History []HistoryItem `json:"history"` } @@ -64,6 +66,7 @@ func main() { http.HandleFunc("/", handleIndex) http.HandleFunc("/api/tasks", auth(handleTasks)) http.HandleFunc("/api/task", auth(handleTask)) + http.HandleFunc("/api/task/toggle-notify", auth(handleToggleNotify)) http.HandleFunc("/api/config", auth(handleConfig)) http.HandleFunc("/api/test-notification", auth(handleTestNotification)) @@ -139,6 +142,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { .stock-no{color:var(--gray)} .btn{padding:5px 10px;margin:0 2px;cursor:pointer;border:none;border-radius:3px;background:var(--blue);color:#fff;font-size:12px} .btn-del{background:var(--red)} + .btn-icon{background:transparent;color:var(--text);font-size:16px;padding:5px 8px} + .btn-icon.active{color:var(--green)} .uptime-bar{display:flex;gap:2px;height:30px} .uptime-item{flex:1;border-radius:2px;cursor:pointer;position:relative;transition:transform .2s} .uptime-item:hover{transform:scaleY(1.2)} @@ -181,7 +186,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { 监控历史 库存 最后检测 - 操作 + 操作 @@ -209,6 +214,10 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { +
+ + +
@@ -255,7 +264,6 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { if(token) localStorage.setItem('token', token); const headers = {'Authorization': 'Bearer ' + token}; - function loadTasks() { fetch('/api/tasks', {headers}) .then(r => r.json()) @@ -265,6 +273,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { const statusClass = t.status || 'checking'; const stockText = t.in_stock ? '有货' : '无货'; const lastCheck = t.last_check ? new Date(t.last_check).toLocaleString('zh-CN') : '-'; + const notifyIcon = t.notify_enabled ? '🔔' : '🔕'; + const notifyClass = t.notify_enabled ? 'active' : ''; const history = t.history || []; const uptimeBar = history.map(h => { @@ -279,6 +289,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { '' + stockText + '' + '' + lastCheck + '' + '' + + '' + + '🔗' + '' + '' + ''; @@ -286,6 +298,11 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { }); } + function toggleNotify(id) { + fetch('/api/task/toggle-notify?id=' + id, {method: 'POST', headers}) + .then(() => loadTasks()); + } + function showModal(task) { document.getElementById('modalTitle').textContent = task ? '编辑监控' : '添加监控'; document.getElementById('taskId').value = task?.id || ''; @@ -293,6 +310,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { document.getElementById('url').value = task?.url || ''; document.getElementById('pageLoaded').value = task?.page_loaded || ''; document.getElementById('outOfStock').value = task?.out_of_stock || ''; + document.getElementById('order').value = task?.order || 0; document.getElementById('modal').classList.add('show'); } @@ -327,7 +345,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { method: 'POST', headers: {...headers, 'Content-Type': 'application/json'}, body: JSON.stringify(testConfig) - }).then(r => r.ok ? alert('测试通知已发送,请检查 Gotify') : alert('发送失败,查看后台日志')); + }).then(r => r.ok ? alert('测试通知已发送,请检查 Gotify') : alert('发送失败,查看后台日志')); } function editTask(id) { @@ -342,9 +360,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { function delTask(id) { if(!confirm('确认删除?')) return; fetch('/api/task?id=' + id, {method: 'DELETE', headers}) - .then(() => { - loadTasks(); - }); + .then(() => loadTasks()); } document.getElementById('taskForm').onsubmit = e => { @@ -354,7 +370,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { name: document.getElementById('name').value, url: document.getElementById('url').value, page_loaded: document.getElementById('pageLoaded').value, - out_of_stock: document.getElementById('outOfStock').value + out_of_stock: document.getElementById('outOfStock').value, + order: parseInt(document.getElementById('order').value) || 0 }; fetch('/api/task', { method: 'POST', @@ -396,11 +413,16 @@ func handleIndex(w http.ResponseWriter, r *http.Request) { func handleTasks(w http.ResponseWriter, r *http.Request) { mu.RLock() - defer mu.RUnlock() list := make([]*Task, 0, len(tasks)) for _, t := range tasks { list = append(list, t) } + mu.RUnlock() + + sort.Slice(list, func(i, j int) bool { + return list[i].Order < list[j].Order + }) + json.NewEncoder(w).Encode(list) } @@ -419,6 +441,7 @@ func handleTask(w http.ResponseWriter, r *http.Request) { isNew := task.ID == "" if isNew { task.ID = time.Now().Format("20060102150405") + task.NotifyEnabled = true task.History = make([]HistoryItem, 90) now := time.Now() for i := 0; i < 90; i++ { @@ -435,6 +458,7 @@ func handleTask(w http.ResponseWriter, r *http.Request) { task.LastCheck = existing.LastCheck task.Status = existing.Status task.Notified = existing.Notified + task.NotifyEnabled = existing.NotifyEnabled } mu.RUnlock() } @@ -445,6 +469,16 @@ func handleTask(w http.ResponseWriter, r *http.Request) { saveTasks() } +func handleToggleNotify(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + mu.Lock() + if task, ok := tasks[id]; ok { + task.NotifyEnabled = !task.NotifyEnabled + } + mu.Unlock() + saveTasks() +} + func handleConfig(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { var cfg Config @@ -517,6 +551,7 @@ func checkTask(task *Task) { mu.Lock() wasInStock := task.InStock wasNotified := task.Notified + taskNotifyEnabled := task.NotifyEnabled task.Status = status task.InStock = inStock task.LastCheck = now @@ -536,10 +571,10 @@ func checkTask(task *Task) { saveTasks() configMu.RLock() - notifyEnabled := config.NotifyEnabled + globalNotifyEnabled := config.NotifyEnabled configMu.RUnlock() - if notifyEnabled && inStock && !wasInStock && !wasNotified { + if globalNotifyEnabled && taskNotifyEnabled && inStock && !wasInStock && !wasNotified { notify(task.Name + " 有货了!", task.URL) mu.Lock() task.Notified = true @@ -635,12 +670,12 @@ func updateClient() { } func saveJSON(filename string, v interface{}) { + os.MkdirAll("data", 0755) data, _ := json.MarshalIndent(v, "", " ") - os.WriteFile(filename, data, 0644) + os.WriteFile("data/"+filename, data, 0644) } - func loadTasks() { - data, err := os.ReadFile("tasks.json") + data, err := os.ReadFile("data/tasks.json") if err != nil { return } @@ -652,19 +687,8 @@ func loadTasks() { } mu.Unlock() } - -func saveTasks() { - mu.RLock() - list := make([]*Task, 0, len(tasks)) - for _, t := range tasks { - list = append(list, t) - } - mu.RUnlock() - saveJSON("tasks.json", list) -} - func loadConfig() { - data, err := os.ReadFile("config.json") + data, err := os.ReadFile("data/config.json") if err != nil { return }