Update:完善操作;Fix:持久化配置路径
This commit is contained in:
78
main.go
78
main.go
@@ -1,4 +1,3 @@
|
|||||||
// main.go
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,6 +10,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,7 +25,9 @@ type Task struct {
|
|||||||
InStock bool `json:"in_stock"`
|
InStock bool `json:"in_stock"`
|
||||||
LastCheck time.Time `json:"last_check"`
|
LastCheck time.Time `json:"last_check"`
|
||||||
Status string `json:"status"`
|
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"`
|
History []HistoryItem `json:"history"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ func main() {
|
|||||||
http.HandleFunc("/", handleIndex)
|
http.HandleFunc("/", handleIndex)
|
||||||
http.HandleFunc("/api/tasks", auth(handleTasks))
|
http.HandleFunc("/api/tasks", auth(handleTasks))
|
||||||
http.HandleFunc("/api/task", auth(handleTask))
|
http.HandleFunc("/api/task", auth(handleTask))
|
||||||
|
http.HandleFunc("/api/task/toggle-notify", auth(handleToggleNotify))
|
||||||
http.HandleFunc("/api/config", auth(handleConfig))
|
http.HandleFunc("/api/config", auth(handleConfig))
|
||||||
http.HandleFunc("/api/test-notification", auth(handleTestNotification))
|
http.HandleFunc("/api/test-notification", auth(handleTestNotification))
|
||||||
|
|
||||||
@@ -139,6 +142,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
.stock-no{color:var(--gray)}
|
.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{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-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-bar{display:flex;gap:2px;height:30px}
|
||||||
.uptime-item{flex:1;border-radius:2px;cursor:pointer;position:relative;transition:transform .2s}
|
.uptime-item{flex:1;border-radius:2px;cursor:pointer;position:relative;transition:transform .2s}
|
||||||
.uptime-item:hover{transform:scaleY(1.2)}
|
.uptime-item:hover{transform:scaleY(1.2)}
|
||||||
@@ -181,7 +186,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
<th>监控历史</th>
|
<th>监控历史</th>
|
||||||
<th width="80">库存</th>
|
<th width="80">库存</th>
|
||||||
<th width="150">最后检测</th>
|
<th width="150">最后检测</th>
|
||||||
<th width="120">操作</th>
|
<th width="180">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
@@ -209,6 +214,10 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
<label>缺货标识</label>
|
<label>缺货标识</label>
|
||||||
<input id="outOfStock" placeholder="如: Out of Stock" required>
|
<input id="outOfStock" placeholder="如: Out of Stock" required>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>排序</label>
|
||||||
|
<input id="order" type="number" value="0">
|
||||||
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn" onclick="hideModal()">取消</button>
|
<button type="button" class="btn" onclick="hideModal()">取消</button>
|
||||||
<button type="submit" class="btn">保存</button>
|
<button type="submit" class="btn">保存</button>
|
||||||
@@ -255,7 +264,6 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
if(token) localStorage.setItem('token', token);
|
if(token) localStorage.setItem('token', token);
|
||||||
const headers = {'Authorization': 'Bearer ' + token};
|
const headers = {'Authorization': 'Bearer ' + token};
|
||||||
|
|
||||||
|
|
||||||
function loadTasks() {
|
function loadTasks() {
|
||||||
fetch('/api/tasks', {headers})
|
fetch('/api/tasks', {headers})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@@ -265,6 +273,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
const statusClass = t.status || 'checking';
|
const statusClass = t.status || 'checking';
|
||||||
const stockText = t.in_stock ? '<span class="stock-yes">有货</span>' : '<span class="stock-no">无货</span>';
|
const stockText = t.in_stock ? '<span class="stock-yes">有货</span>' : '<span class="stock-no">无货</span>';
|
||||||
const lastCheck = t.last_check ? new Date(t.last_check).toLocaleString('zh-CN') : '-';
|
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 history = t.history || [];
|
||||||
const uptimeBar = history.map(h => {
|
const uptimeBar = history.map(h => {
|
||||||
@@ -279,6 +289,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
'<td>' + stockText + '</td>' +
|
'<td>' + stockText + '</td>' +
|
||||||
'<td>' + lastCheck + '</td>' +
|
'<td>' + lastCheck + '</td>' +
|
||||||
'<td>' +
|
'<td>' +
|
||||||
|
'<button class="btn btn-icon ' + notifyClass + '" onclick="toggleNotify(\'' + t.id + '\')" title="通知开关">' + notifyIcon + '</button>' +
|
||||||
|
'<a href="' + t.url + '" target="_blank" class="btn btn-icon" title="访问">🔗</a>' +
|
||||||
'<button class="btn" onclick="editTask(\'' + t.id + '\')">编辑</button>' +
|
'<button class="btn" onclick="editTask(\'' + t.id + '\')">编辑</button>' +
|
||||||
'<button class="btn btn-del" onclick="delTask(\'' + t.id + '\')">删除</button>' +
|
'<button class="btn btn-del" onclick="delTask(\'' + t.id + '\')">删除</button>' +
|
||||||
'</td></tr>';
|
'</td></tr>';
|
||||||
@@ -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) {
|
function showModal(task) {
|
||||||
document.getElementById('modalTitle').textContent = task ? '编辑监控' : '添加监控';
|
document.getElementById('modalTitle').textContent = task ? '编辑监控' : '添加监控';
|
||||||
document.getElementById('taskId').value = task?.id || '';
|
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('url').value = task?.url || '';
|
||||||
document.getElementById('pageLoaded').value = task?.page_loaded || '';
|
document.getElementById('pageLoaded').value = task?.page_loaded || '';
|
||||||
document.getElementById('outOfStock').value = task?.out_of_stock || '';
|
document.getElementById('outOfStock').value = task?.out_of_stock || '';
|
||||||
|
document.getElementById('order').value = task?.order || 0;
|
||||||
document.getElementById('modal').classList.add('show');
|
document.getElementById('modal').classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +345,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {...headers, 'Content-Type': 'application/json'},
|
headers: {...headers, 'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(testConfig)
|
body: JSON.stringify(testConfig)
|
||||||
}).then(r => r.ok ? alert('测试通知已发送,请检查 Gotify') : alert('发送失败,查看后台日志'));
|
}).then(r => r.ok ? alert('测试通知已发送,请检查 Gotify') : alert('发送失败,查看后台日志'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function editTask(id) {
|
function editTask(id) {
|
||||||
@@ -342,9 +360,7 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
function delTask(id) {
|
function delTask(id) {
|
||||||
if(!confirm('确认删除?')) return;
|
if(!confirm('确认删除?')) return;
|
||||||
fetch('/api/task?id=' + id, {method: 'DELETE', headers})
|
fetch('/api/task?id=' + id, {method: 'DELETE', headers})
|
||||||
.then(() => {
|
.then(() => loadTasks());
|
||||||
loadTasks();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('taskForm').onsubmit = e => {
|
document.getElementById('taskForm').onsubmit = e => {
|
||||||
@@ -354,7 +370,8 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
name: document.getElementById('name').value,
|
name: document.getElementById('name').value,
|
||||||
url: document.getElementById('url').value,
|
url: document.getElementById('url').value,
|
||||||
page_loaded: document.getElementById('pageLoaded').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', {
|
fetch('/api/task', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -396,11 +413,16 @@ func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func handleTasks(w http.ResponseWriter, r *http.Request) {
|
func handleTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
defer mu.RUnlock()
|
|
||||||
list := make([]*Task, 0, len(tasks))
|
list := make([]*Task, 0, len(tasks))
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
list = append(list, t)
|
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)
|
json.NewEncoder(w).Encode(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +441,7 @@ func handleTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
isNew := task.ID == ""
|
isNew := task.ID == ""
|
||||||
if isNew {
|
if isNew {
|
||||||
task.ID = time.Now().Format("20060102150405")
|
task.ID = time.Now().Format("20060102150405")
|
||||||
|
task.NotifyEnabled = true
|
||||||
task.History = make([]HistoryItem, 90)
|
task.History = make([]HistoryItem, 90)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for i := 0; i < 90; i++ {
|
for i := 0; i < 90; i++ {
|
||||||
@@ -435,6 +458,7 @@ func handleTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
task.LastCheck = existing.LastCheck
|
task.LastCheck = existing.LastCheck
|
||||||
task.Status = existing.Status
|
task.Status = existing.Status
|
||||||
task.Notified = existing.Notified
|
task.Notified = existing.Notified
|
||||||
|
task.NotifyEnabled = existing.NotifyEnabled
|
||||||
}
|
}
|
||||||
mu.RUnlock()
|
mu.RUnlock()
|
||||||
}
|
}
|
||||||
@@ -445,6 +469,16 @@ func handleTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
saveTasks()
|
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) {
|
func handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
var cfg Config
|
var cfg Config
|
||||||
@@ -517,6 +551,7 @@ func checkTask(task *Task) {
|
|||||||
mu.Lock()
|
mu.Lock()
|
||||||
wasInStock := task.InStock
|
wasInStock := task.InStock
|
||||||
wasNotified := task.Notified
|
wasNotified := task.Notified
|
||||||
|
taskNotifyEnabled := task.NotifyEnabled
|
||||||
task.Status = status
|
task.Status = status
|
||||||
task.InStock = inStock
|
task.InStock = inStock
|
||||||
task.LastCheck = now
|
task.LastCheck = now
|
||||||
@@ -536,10 +571,10 @@ func checkTask(task *Task) {
|
|||||||
saveTasks()
|
saveTasks()
|
||||||
|
|
||||||
configMu.RLock()
|
configMu.RLock()
|
||||||
notifyEnabled := config.NotifyEnabled
|
globalNotifyEnabled := config.NotifyEnabled
|
||||||
configMu.RUnlock()
|
configMu.RUnlock()
|
||||||
|
|
||||||
if notifyEnabled && inStock && !wasInStock && !wasNotified {
|
if globalNotifyEnabled && taskNotifyEnabled && inStock && !wasInStock && !wasNotified {
|
||||||
notify(task.Name + " 有货了!", task.URL)
|
notify(task.Name + " 有货了!", task.URL)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
task.Notified = true
|
task.Notified = true
|
||||||
@@ -635,12 +670,12 @@ func updateClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveJSON(filename string, v interface{}) {
|
func saveJSON(filename string, v interface{}) {
|
||||||
|
os.MkdirAll("data", 0755)
|
||||||
data, _ := json.MarshalIndent(v, "", " ")
|
data, _ := json.MarshalIndent(v, "", " ")
|
||||||
os.WriteFile(filename, data, 0644)
|
os.WriteFile("data/"+filename, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTasks() {
|
func loadTasks() {
|
||||||
data, err := os.ReadFile("tasks.json")
|
data, err := os.ReadFile("data/tasks.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -652,19 +687,8 @@ func loadTasks() {
|
|||||||
}
|
}
|
||||||
mu.Unlock()
|
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() {
|
func loadConfig() {
|
||||||
data, err := os.ReadFile("config.json")
|
data, err := os.ReadFile("data/config.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user