user viper

This commit is contained in:
张超
2025-04-15 16:47:00 +08:00
parent 33cb413a12
commit f52d5a698b
12 changed files with 329 additions and 214 deletions

View File

@@ -1,23 +0,0 @@
[mysql]
Type = mysql
Host = 47.93.160.42
Port = 3630
User = blog
Password = qI7=bL4@iJ
DBName = blog
Charset = utf8mb4
Prefix = gin_
;Prefix = gin_
[jwt]
SecretKey = \x13\xbf\xd2 1\xce\x8b\xc1\t\xc1=\xec\x07\x93\xd4\x9e\xbco\xb0Z
[project]
StaticUrlMapPath = {"assets/static/": "static/", "assets/docs/": "docs/", "media/": "media/"}
TemplateGlob = templates/**/*
MediaFilePath = "media/upload/"
[server]
Port = 7890
ReadTimeout = 60
WriteTimeout = 60

View File

@@ -8,16 +8,13 @@ package config
import ( import (
"fmt" "fmt"
"log"
"time" "time"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/go-ini/ini"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
type SqlDataBase struct { type DataBaseConfig struct {
Type string
Host string Host string
Port string Port string
User string User string
@@ -25,6 +22,9 @@ type SqlDataBase struct {
DBName string DBName string
Charset string Charset string
Prefix string Prefix string
Driver string
DSN string
MaxConns int `mapstructure:"max_conns"`
} }
type Jwt struct { type Jwt struct {
@@ -32,75 +32,95 @@ type Jwt struct {
} }
type Project struct { type Project struct {
StaticUrlMapPath string StaticUrlMapPath []interface{}
TemplateGlob string TemplateGlob string
MediaFilePath string MediaFilePath string
CurrentTheme string CurrentTheme string
} }
type Server struct { type ServerConfig struct {
Port string Port string
EnableGzip bool `mapstructure:"enable_gzip"`
CSRFSecret string `mapstructure:"csrf_secret"`
ReadTimeout time.Duration ReadTimeout time.Duration
WriteTimeout time.Duration WriteTimeout time.Duration
} }
var ( type ThemeConfig struct {
DataBase = &SqlDataBase{} Current string
JwtSecretKey = &Jwt{} AllowUpload bool `mapstructure:"allow_upload"`
ProjectCfg = &Project{}
HttpServer = &Server{}
)
func SetUp() {
cfg, err := ini.Load("config/conf.ini")
if err != nil {
panic(err)
} }
if err := cfg.Section("mysql").MapTo(DataBase); err != nil { type SecurityConfig struct {
panic(err) JWTSecret string `mapstructure:"jwt_secret"`
} CORSAllowedOrigins []string `mapstructure:"cors_allowed_origins"`
if err := cfg.Section("jwt").MapTo(JwtSecretKey); err != nil {
panic(err)
}
if err := cfg.Section("project").MapTo(ProjectCfg); err != nil {
panic(err)
}
if err := cfg.Section("server").MapTo(HttpServer); err != nil {
panic(err)
} }
type Config struct {
Env string
Server ServerConfig
DataBase DataBaseConfig
Theme ThemeConfig
JwtSecretKey Jwt
Project Project
Security SecurityConfig
} }
func SetUp1() { func LoadConfig(configPath string) (*Config, error) {
// 1. 基础配置
viper.SetConfigFile(configPath)
// 设置配置文件的名称(不包括扩展名) // 设置配置文件的名称(不包括扩展名)
viper.SetConfigName("config.yml") viper.SetConfigName("config")
// 设置配置文件所在的目录 // 设置配置文件所在的目录
viper.AddConfigPath("./conf") viper.AddConfigPath("./config")
// 设置配置文件的类型为YAML // 设置配置文件的类型为YAML
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
// 3. 自动读取环境变量(自动转换大写和下划线)
viper.AutomaticEnv()
viper.SetEnvPrefix("BLOG") // 环境变量前缀 BLOG_xxx
// 2. 设置默认值
viper.SetDefault("server.port", 3000)
viper.SetDefault("database.max_conns", 10)
viper.SetDefault("theme.allow_upload", false)
// 读取配置文件 // 读取配置文件
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Error reading config file, %s", err) if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return nil, fmt.Errorf("配置文件未找到: %s", configPath)
}
return nil, fmt.Errorf("读取配置文件失败: %w", err)
} }
//监控并重新读取配置文件
viper.WatchConfig() viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) { viper.OnConfigChange(func(e fsnotify.Event) {
// 配置文件发生变更之后会调用的回调函数 // 配置文件发生变更之后会调用的回调函数
fmt.Println("Config file changed:", e.Name) fmt.Println("Config file changed:", e.Name)
}) })
// 5. 反序列化到结构体
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("配置解析失败: %w", err)
}
// 6. 校验必要配置项
// if cfg.Security.JWTSecret == "" {
// return nil, fmt.Errorf("安全配置错误: jwt_secret 必须设置")
// }
// 获取配置值 // 获取配置值
mysqlHost := viper.GetString("mysql.Host") mysqlHost := viper.GetString("database.Host")
jwtSecretKey := viper.GetString("jwt.SecretKey") jwtSecretKey := viper.GetString("jwt.SecretKey")
serverPort := viper.GetInt("server.Port") serverPort := viper.GetInt("server.Port")
templateGlob := viper.GetString("project.TemplateGlob") templateGlob := viper.GetString("project.TemplateGlob")
// 打印获取到的配置值 // 打印获取到的配置值
fmt.Printf("MySQL Host: %s\n", mysqlHost) fmt.Printf("MySQL Host: %s\n", mysqlHost)
fmt.Printf("JWT Secret Key: %s\n", jwtSecretKey) fmt.Printf("JWT Secret Key: %s\n", jwtSecretKey)
fmt.Printf("Server Port: %d\n", serverPort) fmt.Printf("Server Port: %d\n", serverPort)
fmt.Printf("TemplateGlob: %s\n", templateGlob) fmt.Printf("TemplateGlob: %s\n", templateGlob)
return &cfg, nil
} }
func GetCurrentTheme() string { func GetCurrentTheme() string {
return "default" return "default"

View File

@@ -1,5 +1,5 @@
database: database:
Type: mysql Driver: mysql
Host: 47.93.160.42 Host: 47.93.160.42
Port: 3630 Port: 3630
User: blog User: blog
@@ -7,10 +7,13 @@ database:
DBName: blog DBName: blog
Charset: utf8mb4 Charset: utf8mb4
Prefix: gin_ Prefix: gin_
DSN: "mysql:mysql@tcp(47.93.160.42:3630)/gin_blog?charset=utf8mb4&parseTime=True&loc=Local"
# Prefix: gin_ # This line is commented out in the original .ini file # Prefix: gin_ # This line is commented out in the original .ini file
jwt: jwt:
SecretKey: "\x13\xbf\xd2 1\xce\x8b\xc1\t\xc1=\xec\x07\x93\xd4\x9e\xbco\xb0Z" SecretKey: "\x13\xbf\xd2 1\xce\x8b\xc1\t\xc1=\xec\x07\x93\xd4\x9e\xbco\xb0Z"
security:
JWTSecret: "jwt_secret"
project: project:
StaticUrlMapPath: StaticUrlMapPath:
@@ -21,7 +24,7 @@ project:
MediaFilePath: "media/upload/" MediaFilePath: "media/upload/"
server: server:
Port: 7890 Port: 8090
ReadTimeout: 60 ReadTimeout: 60
WriteTimeout: 60 WriteTimeout: 60

View File

@@ -1,13 +1,17 @@
package controllers package controllers
import ( import (
"go_blog/models"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func Home(c *gin.Context) { func Home(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", gin.H{
"title": "首页", var items []models.Content
models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
}) })
} }

View File

@@ -1,13 +1,20 @@
package controllers package controllers
import ( import (
"go_blog/models"
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func ShowPost(c *gin.Context) { func ShowPost(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", gin.H{
"title": "首页", id, err := strconv.ParseInt(c.Param("id"), 10, 32)
}) if err != nil {
return
}
var content = models.Content{Cid: int32(id)}
models.DB.First(&content)
c.HTML(http.StatusOK, "post.tmpl", content)
} }

146
main.go
View File

@@ -1,47 +1,34 @@
package main package main
import ( import (
"fmt" "go_blog/config"
conf "go_blog/config"
"go_blog/controllers"
"go_blog/models" "go_blog/models"
"go_blog/routers" "go_blog/routers"
"go_blog/serializers"
"log"
"net/http"
"strconv"
"go_blog/themes" "go_blog/themes"
"gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/schema"
"honnef.co/go/tools/config"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
const templatePath = "./templates/*" const templatePath = "./templates/*"
func init() {
conf.SetUp()
models.SetUp()
}
func main() { func main() {
conf, err := config.LoadConfig("config.yml")
if err != nil {
panic("配置文件加载失败: " + err.Error())
}
models.InitDatabase(conf)
// 4. 初始化主题管理器 // 4. 初始化主题管理器
themeManager := themes.NewManager() themeManager := themes.NewManager()
if err := themeManager.LoadTheme(conf.GetCurrentTheme()); err != nil { // if err := themeManager.LoadTheme(conf.GetCurrentTheme()); err != nil {
panic("主题加载失败: " + err.Error()) // panic("主题加载失败: " + err.Error())
} // }
// 5. 创建Gin实例 // 5. 创建Gin实例
router := gin.Default() router := gin.Default()
router.LoadHTMLGlob(templatePath) router.LoadHTMLGlob(templatePath)
// 注册WebSocket路由
registerRoutes(router)
router.GET("/events", esSSE)
// 6. 注册中间件 // 6. 注册中间件
router.Use( router.Use(
@@ -58,120 +45,7 @@ func main() {
// 8. 注册路由 // 8. 注册路由
routers.RegisterRoutes(router) routers.RegisterRoutes(router)
// 9. 启动服务 // 9. 启动服务
router.Run(":8910") //router.Run(":" + cfg.Server.Port) router.Run(":" + conf.Server.Port) //router.Run()
}
func esSSE(c *gin.Context) {
w := c.Writer
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
_, ok := w.(http.Flusher)
if !ok {
log.Panic("server not support") //浏览器不兼容
}
_, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf")
_, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime")
if err != nil || er1r != nil {
print("error", err)
return
}
}
func registerRoutes(r *gin.Engine) {
r.GET("/", func(c *gin.Context) {
var items []models.Content
models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
})
})
r.GET("/page", func(c *gin.Context) {
var items []models.Content
var pager serializers.Pager
pager.InitPager(c)
offset := (pager.Page - 1) * pager.PageSize
models.DB.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
"Pager": pager,
"Title": "文章列表",
})
})
r.GET("/createcontent", func(c *gin.Context) {
c.HTML(http.StatusOK, "content.tmpl", nil)
})
r.GET("/post/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 32)
if err != nil {
return
}
var content = models.Content{Cid: int32(id)}
models.DB.First(&content)
c.HTML(http.StatusOK, "post.tmpl", content)
})
user := getUserInfo()
r.GET("/login", func(c *gin.Context) {
c.HTML(200, "login.tmpl", map[string]interface{}{
"title": "这个是titile,传入templates中的",
"user": user,
})
})
r.GET("/register", func(c *gin.Context) {
c.HTML(200, "register.tmpl", map[string]interface{}{
"title": "这个是titile,传入templates中的",
"user": user,
})
})
r.GET("/ws", controllers.WebSocketHandler)
r.POST("/content", controllers.CreateContentHandler)
r.POST("/login", controllers.UsersLoginHandler)
r.POST("/register", controllers.UsersRegisterHandler)
r.POST("/setinfo", controllers.UsersSetInfoHandler)
r.POST("/setpwd", controllers.UsersSetPwdHandler)
}
func getUserInfo() models.User {
user := models.User{
Name: "user",
Gender: "male",
Age: 18,
Password: "nothings",
PasswordHash: []byte("nothings"),
}
return user
}
func initDatabase() {
// 1. 初始化配置
cfg, err := config.Load("config.yaml")
if err != nil {
panic("加载配置失败: " + err.Error())
}
// 2. 初始化数据库
db, err := gorm.Open(mysql.Open(cfg.String()), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: conf.DataBase.Prefix,
},
})
if err != nil {
panic("数据库连接失败: " + err.Error())
}
// 3. 自动迁移数据模型
if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil {
panic("数据库迁移失败: " + err.Error())
}
} }
// 自定义模板渲染器 // 自定义模板渲染器

View File

@@ -8,7 +8,7 @@ package models
import ( import (
"fmt" "fmt"
conf "go_blog/config" "go_blog/config"
"go_blog/pkg/util" "go_blog/pkg/util"
"time" "time"
@@ -19,7 +19,8 @@ import (
var DB *gorm.DB var DB *gorm.DB
func SetUp() { func InitDatabase(conf *config.Config) {
conUri := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local", conUri := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
conf.DataBase.User, conf.DataBase.User,
conf.DataBase.Password, conf.DataBase.Password,
@@ -27,20 +28,23 @@ func SetUp() {
conf.DataBase.Port, conf.DataBase.Port,
conf.DataBase.DBName, conf.DataBase.DBName,
conf.DataBase.Charset) conf.DataBase.Charset)
// 2. 初始化数据库
db, err := gorm.Open(mysql.Open(conUri), &gorm.Config{ db, err := gorm.Open(mysql.Open(conUri), &gorm.Config{
NamingStrategy: schema.NamingStrategy{ NamingStrategy: schema.NamingStrategy{
TablePrefix: conf.DataBase.Prefix, TablePrefix: conf.DataBase.Prefix,
}, },
}) })
if err != nil { if err != nil {
panic(err) panic("数据库连接失败: " + err.Error())
} }
DB = db
DB = db
// 3. 自动迁移数据模型
DB.AutoMigrate(&Account{}) DB.AutoMigrate(&Account{})
DB.AutoMigrate(&Content{}) DB.AutoMigrate(&Content{})
// if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil {
// panic("数据库迁移失败: " + err.Error())
// }
} }
type BaseModel struct { type BaseModel struct {

View File

@@ -7,12 +7,12 @@
package jwt package jwt
import ( import (
conf "go_blog/config"
"go_blog/models" "go_blog/models"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/spf13/viper"
) )
// 定义jwt载荷 // 定义jwt载荷
@@ -44,14 +44,14 @@ func GenToken(id uint64, username string) (string, error) {
username, username,
} }
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString([]byte(conf.JwtSecretKey.SecretKey)) token, err := tokenClaims.SignedString([]byte(viper.GetString("config.JwtSecretKey.secretKey")))
return token, err return token, err
} }
// 验证token合法性 // 验证token合法性
func ValidateJwtToken(token string) (*UserClaims, error) { func ValidateJwtToken(token string) (*UserClaims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(conf.JwtSecretKey.SecretKey), nil return []byte(viper.GetString("config.JwtSecretKey.secretKey")), nil
}) })
if tokenClaims != nil { if tokenClaims != nil {

View File

@@ -1,13 +1,43 @@
package routers // Add the package declaration at the top package routers // Add the package declaration at the top
import ( import (
"fmt"
"go_blog/controllers" "go_blog/controllers"
"go_blog/models"
"go_blog/serializers"
"log"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func RegisterRoutes(r *gin.Engine) { func RegisterRoutes(r *gin.Engine) {
// 注册WebSocket路由
r.GET("/events", esSSE)
r.GET("/page", func(c *gin.Context) {
var items []models.Content
var pager serializers.Pager
pager.InitPager(c)
offset := (pager.Page - 1) * pager.PageSize
models.DB.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post")
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"Items": items,
"Pager": pager,
"Title": "文章列表",
})
})
r.GET("/createcontent", func(c *gin.Context) {
c.HTML(http.StatusOK, "content.tmpl", nil)
})
r.GET("/ws", controllers.WebSocketHandler)
r.POST("/content", controllers.CreateContentHandler)
r.POST("/login", controllers.UsersLoginHandler)
r.POST("/register", controllers.UsersRegisterHandler)
r.POST("/setinfo", controllers.UsersSetInfoHandler)
r.POST("/setpwd", controllers.UsersSetPwdHandler)
// Frontend routes (dynamic themes) // Frontend routes (dynamic themes)
r.GET("/", controllers.Home) r.GET("/", controllers.Home)
r.GET("/post/:id", controllers.ShowPost) r.GET("/post/:id", controllers.ShowPost)
@@ -22,3 +52,35 @@ func RegisterRoutes(r *gin.Engine) {
// Static files for themes // Static files for themes
r.StaticFS("/themes", http.Dir("web/themes")) r.StaticFS("/themes", http.Dir("web/themes"))
} }
func getUserInfo() models.User {
user := models.User{
Name: "user",
Gender: "male",
Age: 18,
Password: "nothings",
PasswordHash: []byte("nothings"),
}
return user
}
func esSSE(c *gin.Context) {
w := c.Writer
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
_, ok := w.(http.Flusher)
if !ok {
log.Panic("server not support") //浏览器不兼容
}
_, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf")
_, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime")
if err != nil || er1r != nil {
print("error", err)
return
}
}

View File

@@ -22,19 +22,19 @@ func (tm *ThemeManager) LoadTheme(themeName string) error {
tm.CurrentTheme = themeName tm.CurrentTheme = themeName
// Step 1: Validate the theme by reading theme.yaml // Step 1: Validate the theme by reading theme.yaml
themeConfigPath := fmt.Sprintf("h:/code/go_blog/themes/%s/theme.yaml", themeName) themeConfigPath := fmt.Sprintf("h:/code/go_blog/web/themes/%s/theme.yaml", themeName)
if _, err := os.Stat(themeConfigPath); os.IsNotExist(err) { if _, err := os.Stat(themeConfigPath); os.IsNotExist(err) {
return fmt.Errorf("theme %s does not exist or is invalid", themeName) return fmt.Errorf("theme %s does not exist or is invalid", themeName)
} }
// Step 2: Precompile all HTML templates and cache them // Step 2: Precompile all HTML templates and cache them
tm.Templates = make(map[string]*template.Template) tm.Templates = make(map[string]*template.Template)
templateDir := fmt.Sprintf("h:/code/go_blog/themes/%s/templates", themeName) templateDir := fmt.Sprintf("h:/code/go_blog/web/themes/%s/tmplates/", themeName)
err := filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
if !info.IsDir() && strings.HasSuffix(info.Name(), ".html") { if !info.IsDir() && strings.HasSuffix(info.Name(), ".tmpl") {
tmpl, err := template.ParseFiles(path) tmpl, err := template.ParseFiles(path)
if err != nil { if err != nil {
return err return err

View File

@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ .Title}}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body{
background: #f8f8f8;
}
.layui-main {
width: 1140px;
margin: 0 auto;
}
.container {
width: 1170px;
margin: 0 auto;
}
.header .layui-nav {
position: absolute;
right: 0;
top: 0;
padding: 0;
background: none;
}
.post-list {
width: 75%;
float: left;
display: block;
}
.list-card {
background: #fff;
overflow: hidden;
padding: 20px 20px 10px 20px;
position: relative;
border-radius: 10px;
margin-bottom: 10px;
height: 200px;
}
.sidebar {
float: left;
width: 25%;
}
.header {}
</style>
</head>
<body>
<div class="header">
<div class="layui-main">
<h1><a class="logo" href="https://www.hanxiaonuan.cn/">韩小暖的博客</a></h1>
</div>
<ul class="layui-nav">
<li class="layui-nav-item layui-hide-xs layui-this">
<a href="https://www.hanxiaonuan.cn/">首页</a>
</li>
<li class="layui-nav-item layui-hide-xs ">
<a href="https://www.hanxiaonuan.cn/start-page.html" title="关于">关于</a>
</li>
<span class="layui-nav-bar"></span></ul>
</div>
<div class="container">
<div class="post-list">
{{ range .Items }}
<div class="list-card">
<div><a href="/post/{{ .Cid }}">
<h1>Title: {{ .Title }}</h1>
</a>
<p> Slug: {{ .Slug }}</p>
<p>Text: {{ .Text }}</p>
<div>Created: {{ .Created }},</div>
</div>
</div>
{{ end }}
<div class="page-navigator">
共 {{ .Total }} 条,每页 {{ .PageSize }} 条,当前第 {{ .Page }} 页</div>
<div class="pagination">
{{ if .PrevPage }}
<a href="/page/{{ .PrevPage }}">上一页</a>
{{ end }}
{{ if .NextPage }}
<a href="/page/{{ .NextPage }}">下一页</a>
{{ end }}
<a href="/">首页</a>
<a href="/page/{{ .Total }}">尾页</a>
</div>
</div>
<div class="sidebar">
<div class="column">
<h3 class="title-sidebar"><i class="layui-icon"></i> 博客信息</h3>
<div class="personal-information">
<div class="user">
<img src="https://www.hanxiaonuan.cn/usr/uploads/2021/07/3991382612.jpg" alt="韩小暖的博客的头像"
class="rounded-circle avatar">
<div class="p-2">
<a class="user-name" target="_blank" href="https://www.hanxiaonuan.cn/">
韩小暖的博客</a>
<p class="introduction mt-1">这里是小暖的日常记录,欢迎!</p>
</div>
</div>
</div>
</div>
<div class="component">
<form class="layui-form" id="search" method="post" action="https://www.hanxiaonuan.cn/"
role="search">
<div class="layui-inline input">
<input type="text" id="s" name="s" class="layui-input" required="" lay-verify="required"
placeholder="输入关键字搜索">
</div>
<div class="layui-inline">
<button class="layui-btn layui-btn-sm layui-btn-primary"><i
class="layui-icon"></i></button>
</div>
</form>
</div>
<div class="column">
<h3 class="title-sidebar"><i class="layui-icon"></i> 栏目分类</h3>
<ul class="layui-row layui-col-space5">
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/default/"><i class="layui-icon"></i>
默认分类<span class="layui-badge layui-bg-gray">12</span></a></li>
<li class="layui-col-md12 layui-col-xs6"><a href="https://www.hanxiaonuan.cn/category/zc/"><i
class="layui-icon"></i> 日常随想<span class="layui-badge layui-bg-gray">2</span></a>
</li>
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/%E5%85%B3%E4%BA%8E%E6%88%BF%E5%AD%90/"><i
class="layui-icon"></i> 关于房子<span class="layui-badge layui-bg-gray">1</span></a>
</li>
<li class="layui-col-md12 layui-col-xs6"><a
href="https://www.hanxiaonuan.cn/category/%E5%B7%A5%E4%BD%9C%E6%97%A5%E5%BF%97/"><i
class="layui-icon"></i> 工作日志<span class="layui-badge layui-bg-gray">0</span></a>
</li>
</ul>
</div>
<div class="tags">
<h3 class="title-sidebar"><i class="layui-icon"></i>标签云</h3>
<div>
<a class="layui-btn layui-btn-xs layui-btn-primary" style="color: rgb(231, 229, 26)"
href="https://www.hanxiaonuan.cn/tag/%E5%B0%8F%E6%9A%96/" title="小暖">小暖</a>
<a class="layui-btn layui-btn-xs layui-btn-primary" style="color: rgb(125, 196, 207)"
href="https://www.hanxiaonuan.cn/tag/%E6%84%9F%E6%82%9F/" title="感悟">感悟</a>
</div>
</div>
<div class="tags">
<h3 class="title-sidebar"><i class="layui-icon"></i>系统</h3>
<div>
<li class="last"><a href="https://www.hanxiaonuan.cn/admin/">进入后台 (admin)</a></li>
<li><a href="https://www.hanxiaonuan.cn/action/logout?_=5b09b0e1048bbd45bdddc56fc43667b2">退出</a>
</li>
</div>
</div>
</div>
</div>
</body>
</html>

View File