From 10e85127231cb4c14881633359a96efa9d7eaf72 Mon Sep 17 00:00:00 2001 From: aotian16 Date: Wed, 26 Mar 2025 00:42:53 +0800 Subject: [PATCH] init --- Makefile | 12 ++ backend/main.go | 187 +++++++++++++++++++++ config/config.yaml | 5 + frontend/index.html | 86 ++++++++++ frontend/script.js | 0 frontend/styles.css | 401 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 + go.sum | 6 + 8 files changed, 705 insertions(+) create mode 100644 Makefile create mode 100644 backend/main.go create mode 100644 config/config.yaml create mode 100644 frontend/index.html create mode 100644 frontend/script.js create mode 100644 frontend/styles.css create mode 100644 go.mod create mode 100644 go.sum diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7ad5c58 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: build run clean + +build: + cd backend && \ + GOOS=linux GOARCH=amd64 go build -o ../bin/navigation-server + +run: build + ./bin/navigation-server + +clean: + rm -f bin/navigation-server + rm -f data/navigation.db \ No newline at end of file diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..2569141 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,187 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" + "gopkg.in/yaml.v2" +) + +type Config struct { + Server struct { + Port string `yaml:"port"` + } `yaml:"server"` + Database struct { + Path string `yaml:"path"` + } `yaml:"database"` +} + +type Site struct { + ID int `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Icon string `json:"icon,omitempty"` + Category string `json:"category"` +} + +var db *sql.DB + +func main() { + // 加载配置 + config, err := loadConfig("config/config.yaml") + if err != nil { + log.Fatalf("Error loading config: %v", err) + } + + // 初始化数据库 + err = initDB(config.Database.Path) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + // 设置路由 + http.HandleFunc("/api/sites", sitesHandler) + http.HandleFunc("/api/add", addSiteHandler) + http.HandleFunc("/api/delete", deleteSiteHandler) + + // 静态文件服务 + fs := http.FileServer(http.Dir("../frontend")) + http.Handle("/", fs) + + // 启动服务器 + log.Printf("Starting server on :%s", config.Server.Port) + log.Fatal(http.ListenAndServe(":"+config.Server.Port, nil)) +} + +func loadConfig(path string) (*Config, error) { + config := &Config{} + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + d := yaml.NewDecoder(file) + if err := d.Decode(&config); err != nil { + return nil, err + } + + return config, nil +} + +func initDB(dbPath string) error { + var err error + // 确保数据库目录存在 + os.MkdirAll(filepath.Dir(dbPath), 0755) + + db, err = sql.Open("sqlite3", dbPath) + if err != nil { + return err + } + + // 创建表 + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS sites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL, + icon TEXT, + category TEXT NOT NULL + )`) + return err +} + +func sitesHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + getSites(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func getSites(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query("SELECT id, name, url, icon, category FROM sites") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + var sites []Site + for rows.Next() { + var s Site + if err := rows.Scan(&s.ID, &s.Name, &s.URL, &s.Icon, &s.Category); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + sites = append(sites, s) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(sites) +} + +func addSiteHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var s Site + if err := json.NewDecoder(r.Body).Decode(&s); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // 验证数据 + if s.Name == "" || s.URL == "" || s.Category == "" { + http.Error(w, "Name, URL and Category are required", http.StatusBadRequest) + return + } + + // 插入数据库 + res, err := db.Exec("INSERT INTO sites (name, url, icon, category) VALUES (?, ?, ?, ?)", + s.Name, s.URL, s.Icon, s.Category) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + id, _ := res.LastInsertId() + s.ID = int(id) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(s) +} + +func deleteSiteHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var request struct { + ID int `json:"id"` + } + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, err := db.Exec("DELETE FROM sites WHERE id = ?", request.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"status": "success"}`) +} \ No newline at end of file diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..1ab302f --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,5 @@ +server: + port: "8080" # 服务端口 + +database: + path: "data/navigation.db" # 数据库文件路径 \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..5758a22 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,86 @@ + + + + + + 我的导航网站 + + + + +
+
+
+

我的导航网站

+ +
+
+
+ +
+
+ + + + + +
+
+
+ +
+ +
+ + + + + + +
+ + + + + \ No newline at end of file diff --git a/frontend/script.js b/frontend/script.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..00c45de --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,401 @@ +:root { + --primary-color: #3498db; + --secondary-color: #2ecc71; + --danger-color: #e74c3c; + --text-color: #333; + --bg-color: #f5f5f5; + --card-color: #fff; + --border-color: #ddd; + --shadow-color: rgba(0, 0, 0, 0.1); +} + +[data-theme="dark"] { + --primary-color: #2980b9; + --secondary-color: #27ae60; + --danger-color: #c0392b; + --text-color: #f5f5f5; + --bg-color: #121212; + --card-color: #1e1e1e; + --border-color: #333; + --shadow-color: rgba(0, 0, 0, 0.3); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Arial', sans-serif; + transition: background-color 0.3s, color 0.3s; +} + +body { + background-color: var(--bg-color); + color: var(--text-color); + line-height: 1.6; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border-color); + flex-wrap: wrap; +} + +.header-left, .header-right { + display: flex; + align-items: center; + gap: 20px; +} + +h1 { + font-size: 2rem; + color: var(--primary-color); +} + +.search-box { + position: relative; + min-width: 250px; +} + +.search-box input { + width: 100%; + padding: 8px 15px 8px 35px; + border: 1px solid var(--border-color); + border-radius: 20px; + background-color: var(--card-color); + color: var(--text-color); +} + +.search-box i { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-color); + opacity: 0.7; +} + +.actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.actions button { + padding: 8px 16px; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; +} + +.actions button:hover { + opacity: 0.9; +} + +.actions button i { + font-size: 0.9rem; +} + +.theme-switcher button { + background: none; + border: none; + color: var(--text-color); + font-size: 1.2rem; + cursor: pointer; + padding: 5px; +} + +.categories { + display: flex; + flex-direction: column; + gap: 30px; +} + +.category { + background-color: var(--card-color); + border-radius: 8px; + padding: 20px; + box-shadow: 0 2px 5px var(--shadow-color); +} + +.category h2 { + margin-bottom: 15px; + color: var(--primary-color); + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + gap: 10px; +} + +.sites { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 15px; +} + +.site { + display: flex; + flex-direction: column; + align-items: center; + text-decoration: none; + color: var(--text-color); + transition: transform 0.2s; + position: relative; + padding: 10px; + border-radius: 8px; + background-color: var(--bg-color); +} + +.site:hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px var(--shadow-color); +} + +.site-icon { + width: 40px; + height: 40px; + margin-bottom: 8px; + border-radius: 50%; + object-fit: cover; + background-color: var(--border-color); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: var(--primary-color); +} + +.site-name { + text-align: center; + font-size: 0.9rem; + word-break: break-word; + width: 100%; +} + +.delete-btn { + position: absolute; + top: -8px; + right: -8px; + background-color: var(--danger-color); + color: white; + border: none; + border-radius: 50%; + width: 20px; + height: 20px; + font-size: 12px; + cursor: pointer; + display: none; +} + +.site:hover .delete-btn { + display: flex; + align-items: center; + justify-content: center; +} + +/* 模态框样式 */ +.modal { + display: none; + position: fixed; + z-index: 100; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: var(--card-color); + margin: 10% auto; + padding: 25px; + border-radius: 8px; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 8px var(--shadow-color); + position: relative; + max-height: 80vh; + overflow-y: auto; +} + +.modal h2 { + margin-bottom: 20px; + color: var(--primary-color); + display: flex; + align-items: center; + gap: 10px; +} + +.close { + position: absolute; + top: 15px; + right: 20px; + font-size: 28px; + font-weight: bold; + color: var(--text-color); + cursor: pointer; + opacity: 0.7; +} + +.close:hover { + opacity: 1; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + display: flex; + align-items: center; + gap: 8px; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--bg-color); + color: var(--text-color); +} + +form button { + background-color: var(--secondary-color); + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +form button:hover { + opacity: 0.9; +} + +/* 分类管理样式 */ +.category-list { + margin-bottom: 20px; + max-height: 300px; + overflow-y: auto; +} + +.category-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + margin-bottom: 10px; + background-color: var(--bg-color); + border-radius: 4px; +} + +.category-item span { + flex-grow: 1; +} + +.category-item button { + background-color: var(--danger-color); + color: white; + border: none; + border-radius: 4px; + padding: 5px 10px; + cursor: pointer; +} + +.add-category { + display: flex; + gap: 10px; +} + +.add-category input { + flex-grow: 1; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 4px; + background-color: var(--bg-color); + color: var(--text-color); +} + +.add-category button { + background-color: var(--secondary-color); + color: white; + border: none; + padding: 10px 15px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; +} + +/* 拖拽样式 */ +.sortable-ghost { + opacity: 0.5; + background: var(--primary-color); +} + +.sortable-chosen { + box-shadow: 0 0 10px var(--shadow-color); +} + +/* 响应式设计 */ +@media (max-width: 768px) { + header { + flex-direction: column; + align-items: stretch; + gap: 15px; + } + + .header-left, .header-right { + flex-direction: column; + align-items: stretch; + gap: 15px; + } + + .search-box { + width: 100%; + } + + .actions { + justify-content: space-between; + } + + .actions button { + flex-grow: 1; + justify-content: center; + } + + .sites { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + } + + .add-category { + flex-direction: column; + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0df0c3f --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module navigation/backend + +go 1.24.1 + +require ( + github.com/mattn/go-sqlite3 v1.14.24 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..80780c6 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=