feat: 添加安装向导功能
- 后端:安装状态检测API、数据库连接测试、配置文件生成 - 后端:支持SQLite/MySQL数据库选择 - 后端:JWT密钥自动生成或手动设置 - 前端:安装向导界面(数据库配置、安全配置、管理员账号) - 前端:路由守卫检测安装状态,未安装自动跳转安装页面
This commit is contained in:
@@ -35,6 +35,7 @@ func setDefaults() {
|
|||||||
viper.SetDefault("app.jwt_expire", "24h")
|
viper.SetDefault("app.jwt_expire", "24h")
|
||||||
|
|
||||||
// 数据库配置
|
// 数据库配置
|
||||||
|
viper.SetDefault("database.type", "auto")
|
||||||
viper.SetDefault("database.host", "localhost")
|
viper.SetDefault("database.host", "localhost")
|
||||||
viper.SetDefault("database.port", "3306")
|
viper.SetDefault("database.port", "3306")
|
||||||
viper.SetDefault("database.name", "verification_platform")
|
viper.SetDefault("database.name", "verification_platform")
|
||||||
|
|||||||
@@ -30,15 +30,92 @@ var (
|
|||||||
RDB *redis.Client
|
RDB *redis.Client
|
||||||
)
|
)
|
||||||
|
|
||||||
// Init 初始化数据库连接
|
|
||||||
func Init() {
|
func Init() {
|
||||||
initMySQL()
|
initDatabase()
|
||||||
initRedis()
|
initRedis()
|
||||||
}
|
}
|
||||||
|
|
||||||
// initMySQL 初始化MySQL连接
|
func initDatabase() {
|
||||||
func initMySQL() {
|
dbType := config.GetString("database.type")
|
||||||
// 尝试连接MySQL
|
if dbType == "" {
|
||||||
|
dbType = "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dbType {
|
||||||
|
case "sqlite":
|
||||||
|
initSQLite()
|
||||||
|
case "mysql":
|
||||||
|
initMySQLExplicit()
|
||||||
|
default:
|
||||||
|
initAutoDetect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSQLite() {
|
||||||
|
dbPath := "verification_platform.db"
|
||||||
|
absPath, err := filepath.Abs(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to get absolute path:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err == nil && filepath.Base(wd) == "cmd" {
|
||||||
|
absPath = filepath.Join(filepath.Dir(wd), "verification_platform.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
var errOpen error
|
||||||
|
DB, errOpen = gorm.Open(sqlite.Open(absPath), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
})
|
||||||
|
if errOpen != nil {
|
||||||
|
log.Fatal("Failed to create SQLite database:", errOpen.Error())
|
||||||
|
}
|
||||||
|
log.Printf("Using SQLite database: %s\n", absPath)
|
||||||
|
|
||||||
|
if sqlDB, err := DB.DB(); err == nil {
|
||||||
|
sqlDB.SetMaxIdleConns(10)
|
||||||
|
sqlDB.SetMaxOpenConns(100)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
runMigrations()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initMySQLExplicit() {
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
|
||||||
|
config.GetString("database.username"),
|
||||||
|
config.GetString("database.password"),
|
||||||
|
config.GetString("database.host"),
|
||||||
|
config.GetString("database.port"),
|
||||||
|
config.GetString("database.name"),
|
||||||
|
config.GetString("database.charset"),
|
||||||
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to connect to MySQL:", err.Error())
|
||||||
|
}
|
||||||
|
log.Printf("Connected to MySQL: %s@%s:%s/%s\n",
|
||||||
|
config.GetString("database.username"),
|
||||||
|
config.GetString("database.host"),
|
||||||
|
config.GetString("database.port"),
|
||||||
|
config.GetString("database.name"))
|
||||||
|
|
||||||
|
if sqlDB, err := DB.DB(); err == nil {
|
||||||
|
sqlDB.SetMaxIdleConns(config.GetInt("database.max_idle_conns"))
|
||||||
|
sqlDB.SetMaxOpenConns(config.GetInt("database.max_open_conns"))
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
runMigrations()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initAutoDetect() {
|
||||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
|
||||||
config.GetString("database.username"),
|
config.GetString("database.username"),
|
||||||
config.GetString("database.password"),
|
config.GetString("database.password"),
|
||||||
@@ -54,28 +131,18 @@ func initMySQL() {
|
|||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果MySQL连接失败,创建一个SQLite数据库用于开发
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to connect to MySQL, using SQLite database for development:", err.Error())
|
log.Println("Failed to connect to MySQL, using SQLite database for development:", err.Error())
|
||||||
|
|
||||||
log.Println("Using file database for development")
|
|
||||||
|
|
||||||
dbPath := "verification_platform.db"
|
dbPath := "verification_platform.db"
|
||||||
|
|
||||||
absPath, err := filepath.Abs(dbPath)
|
absPath, err := filepath.Abs(dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to get absolute path:", err.Error())
|
log.Fatal("Failed to get absolute path:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err == nil && filepath.Base(wd) == "cmd" {
|
||||||
log.Fatal("Failed to get working directory:", err.Error())
|
absPath = filepath.Join(filepath.Dir(wd), "verification_platform.db")
|
||||||
}
|
|
||||||
log.Printf("Working directory: %s, basename: %s\n", wd, filepath.Base(wd))
|
|
||||||
|
|
||||||
if filepath.Base(wd) == "cmd" {
|
|
||||||
dbPath = filepath.Join(filepath.Dir(wd), "verification_platform.db")
|
|
||||||
absPath = dbPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DB, err = gorm.Open(sqlite.Open(absPath), &gorm.Config{
|
DB, err = gorm.Open(sqlite.Open(absPath), &gorm.Config{
|
||||||
@@ -88,14 +155,16 @@ func initMySQL() {
|
|||||||
log.Printf("Database file path: %s\n", absPath)
|
log.Printf("Database file path: %s\n", absPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置连接池(仅对非内存数据库有效)
|
|
||||||
if sqlDB, err := DB.DB(); err == nil {
|
if sqlDB, err := DB.DB(); err == nil {
|
||||||
sqlDB.SetMaxIdleConns(config.GetInt("database.max_idle_conns"))
|
sqlDB.SetMaxIdleConns(config.GetInt("database.max_idle_conns"))
|
||||||
sqlDB.SetMaxOpenConns(config.GetInt("database.max_open_conns"))
|
sqlDB.SetMaxOpenConns(config.GetInt("database.max_open_conns"))
|
||||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查applications表结构
|
runMigrations()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrations() {
|
||||||
var columns []struct {
|
var columns []struct {
|
||||||
CID int
|
CID int
|
||||||
Name string
|
Name string
|
||||||
@@ -126,7 +195,7 @@ func initMySQL() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 自动迁移数据库表
|
// 自动迁移数据库表
|
||||||
err = DB.AutoMigrate(
|
if err := DB.AutoMigrate(
|
||||||
&model.User{},
|
&model.User{},
|
||||||
&model.AppUser{},
|
&model.AppUser{},
|
||||||
&model.UserLevel{},
|
&model.UserLevel{},
|
||||||
@@ -180,8 +249,7 @@ func initMySQL() {
|
|||||||
&model.EmailTemplate{},
|
&model.EmailTemplate{},
|
||||||
&model.VersionFile{},
|
&model.VersionFile{},
|
||||||
&model.CloudVariableRecord{},
|
&model.CloudVariableRecord{},
|
||||||
)
|
); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to migrate database:", err.Error())
|
log.Println("Failed to migrate database:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"verification-platform-backend/internal/config"
|
||||||
|
"verification-platform-backend/internal/database"
|
||||||
|
"verification-platform-backend/internal/model"
|
||||||
|
"verification-platform-backend/pkg/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/glebarez/sqlite"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(r *gin.Engine) {
|
||||||
|
install := r.Group("/api/install")
|
||||||
|
{
|
||||||
|
install.GET("/status", handleGetInstallStatus)
|
||||||
|
install.POST("/setup", handleSetup)
|
||||||
|
install.POST("/test-db", handleTestDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstallStatus struct {
|
||||||
|
Installed bool `json:"installed"`
|
||||||
|
DbType string `json:"db_type"`
|
||||||
|
HasAdmin bool `json:"has_admin"`
|
||||||
|
ConfigExist bool `json:"config_exist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetInstallStatus(c *gin.Context) {
|
||||||
|
configExist := checkConfigFile()
|
||||||
|
hasAdmin := checkAdminExists()
|
||||||
|
installed := hasAdmin
|
||||||
|
|
||||||
|
var dbType string
|
||||||
|
if configExist {
|
||||||
|
dbType = config.GetString("database.type")
|
||||||
|
if dbType == "" {
|
||||||
|
if strings.Contains(config.GetString("database.host"), "") && config.GetString("database.host") != "localhost" {
|
||||||
|
dbType = "mysql"
|
||||||
|
} else {
|
||||||
|
dbType = "sqlite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, InstallStatus{
|
||||||
|
Installed: installed,
|
||||||
|
DbType: dbType,
|
||||||
|
HasAdmin: hasAdmin,
|
||||||
|
ConfigExist: configExist,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestDBRequest struct {
|
||||||
|
DbType string `json:"db_type"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTestDatabase(c *gin.Context) {
|
||||||
|
var req TestDBRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DbType == "sqlite" {
|
||||||
|
dbPath := "verification_platform.db"
|
||||||
|
absPath, _ := filepath.Abs(dbPath)
|
||||||
|
testDB, err := gorm.Open(sqlite.Open(absPath), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 400, "无法创建SQLite数据库: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sqlDB, _ := testDB.DB()
|
||||||
|
sqlDB.Close()
|
||||||
|
response.Success(c, gin.H{"message": "SQLite数据库连接成功"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DbType == "mysql" {
|
||||||
|
if req.Host == "" || req.Port == "" || req.Name == "" || req.Username == "" {
|
||||||
|
response.Error(c, 400, "MySQL配置不完整")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
|
req.Username, req.Password, req.Host, req.Port, req.Name)
|
||||||
|
testDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 400, "MySQL连接失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sqlDB, _ := testDB.DB()
|
||||||
|
sqlDB.Close()
|
||||||
|
response.Success(c, gin.H{"message": "MySQL连接成功"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Error(c, 400, "不支持的数据库类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetupRequest struct {
|
||||||
|
DbType string `json:"db_type"`
|
||||||
|
DbHost string `json:"db_host"`
|
||||||
|
DbPort string `json:"db_port"`
|
||||||
|
DbName string `json:"db_name"`
|
||||||
|
DbUsername string `json:"db_username"`
|
||||||
|
DbPassword string `json:"db_password"`
|
||||||
|
JwtSecret string `json:"jwt_secret"`
|
||||||
|
AdminUser string `json:"admin_user"`
|
||||||
|
AdminPass string `json:"admin_pass"`
|
||||||
|
AdminEmail string `json:"admin_email"`
|
||||||
|
UseRedis bool `json:"use_redis"`
|
||||||
|
RedisHost string `json:"redis_host"`
|
||||||
|
RedisPort string `json:"redis_port"`
|
||||||
|
RedisPass string `json:"redis_password"`
|
||||||
|
RedisDb int `json:"redis_db"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSetup(c *gin.Context) {
|
||||||
|
if checkAdminExists() {
|
||||||
|
response.Error(c, 400, "系统已安装")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req SetupRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.AdminUser == "" || req.AdminPass == "" {
|
||||||
|
response.Error(c, 400, "管理员账号和密码不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.AdminPass) < 6 {
|
||||||
|
response.Error(c, 400, "密码长度至少6位")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtSecret := req.JwtSecret
|
||||||
|
if jwtSecret == "" {
|
||||||
|
jwtSecret = generateRandomSecret(32)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newDB *gorm.DB
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if req.DbType == "sqlite" {
|
||||||
|
dbPath := "verification_platform.db"
|
||||||
|
absPath, _ := filepath.Abs(dbPath)
|
||||||
|
newDB, err = gorm.Open(sqlite.Open(absPath), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 500, "SQLite数据库创建失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if req.DbType == "mysql" {
|
||||||
|
if req.DbHost == "" || req.DbPort == "" || req.DbName == "" || req.DbUsername == "" {
|
||||||
|
response.Error(c, 400, "MySQL配置不完整")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
|
req.DbUsername, req.DbPassword, req.DbHost, req.DbPort, req.DbName)
|
||||||
|
newDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 500, "MySQL连接失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.Error(c, 400, "不支持的数据库类型")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sqlDB, err := newDB.DB(); err == nil {
|
||||||
|
sqlDB.SetMaxIdleConns(10)
|
||||||
|
sqlDB.SetMaxOpenConns(100)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runMigrations(newDB); err != nil {
|
||||||
|
response.Error(c, 500, "数据库迁移失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.AdminPass), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 500, "密码加密失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
admin := model.User{
|
||||||
|
Username: req.AdminUser,
|
||||||
|
Password: string(hashedPassword),
|
||||||
|
Email: &req.AdminEmail,
|
||||||
|
Role: "admin",
|
||||||
|
Status: "active",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := newDB.Create(&admin).Error; err != nil {
|
||||||
|
response.Error(c, 500, "创建管理员失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configContent := generateConfigFile(req.DbType, req, jwtSecret)
|
||||||
|
if err := saveConfigFile(configContent); err != nil {
|
||||||
|
log.Printf("Warning: failed to save config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB = newDB
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"message": "安装成功",
|
||||||
|
"jwt_secret": jwtSecret,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfigFile() bool {
|
||||||
|
paths := []string{"config.yaml", "./config/config.yaml"}
|
||||||
|
for _, p := range paths {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAdminExists() bool {
|
||||||
|
if database.DB == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
database.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&count)
|
||||||
|
return count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomSecret(length int) string {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return hex.EncodeToString(bytes)[:length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrations(db *gorm.DB) error {
|
||||||
|
return db.AutoMigrate(
|
||||||
|
&model.User{},
|
||||||
|
&model.AppUser{},
|
||||||
|
&model.Application{},
|
||||||
|
&model.CardType{},
|
||||||
|
&model.Card{},
|
||||||
|
&model.RechargeRecord{},
|
||||||
|
&model.UserDevice{},
|
||||||
|
&model.DeviceSession{},
|
||||||
|
&model.Setting{},
|
||||||
|
&model.ExtensionAPIKey{},
|
||||||
|
&model.AgentApplication{},
|
||||||
|
&model.Package{},
|
||||||
|
&model.Order{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateConfigFile(dbType string, req SetupRequest, jwtSecret string) string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
content.WriteString("# 应用配置\n")
|
||||||
|
content.WriteString("app:\n")
|
||||||
|
content.WriteString(" name: verification-platform\n")
|
||||||
|
content.WriteString(" env: production\n")
|
||||||
|
content.WriteString(" port: \"8080\"\n")
|
||||||
|
content.WriteString(fmt.Sprintf(" jwt_secret: %s\n", jwtSecret))
|
||||||
|
content.WriteString(" jwt_expire: 24h\n\n")
|
||||||
|
|
||||||
|
content.WriteString("# 数据库配置\n")
|
||||||
|
content.WriteString("database:\n")
|
||||||
|
content.WriteString(fmt.Sprintf(" type: %s\n", dbType))
|
||||||
|
|
||||||
|
if dbType == "mysql" {
|
||||||
|
content.WriteString(fmt.Sprintf(" host: %s\n", req.DbHost))
|
||||||
|
content.WriteString(fmt.Sprintf(" port: %s\n", req.DbPort))
|
||||||
|
content.WriteString(fmt.Sprintf(" name: %s\n", req.DbName))
|
||||||
|
content.WriteString(fmt.Sprintf(" username: %s\n", req.DbUsername))
|
||||||
|
content.WriteString(fmt.Sprintf(" password: %s\n", req.DbPassword))
|
||||||
|
} else {
|
||||||
|
content.WriteString(" host: localhost\n")
|
||||||
|
content.WriteString(" port: \"3306\"\n")
|
||||||
|
content.WriteString(" name: verification_platform\n")
|
||||||
|
content.WriteString(" username: root\n")
|
||||||
|
content.WriteString(" password: \"\"\n")
|
||||||
|
}
|
||||||
|
content.WriteString(" charset: utf8mb4\n")
|
||||||
|
content.WriteString(" max_idle_conns: 10\n")
|
||||||
|
content.WriteString(" max_open_conns: 100\n\n")
|
||||||
|
|
||||||
|
content.WriteString("# Redis配置\n")
|
||||||
|
content.WriteString("redis:\n")
|
||||||
|
if req.UseRedis {
|
||||||
|
content.WriteString(fmt.Sprintf(" host: %s\n", req.RedisHost))
|
||||||
|
content.WriteString(fmt.Sprintf(" port: %s\n", req.RedisPort))
|
||||||
|
content.WriteString(fmt.Sprintf(" password: %s\n", req.RedisPass))
|
||||||
|
content.WriteString(fmt.Sprintf(" db: %d\n", req.RedisDb))
|
||||||
|
} else {
|
||||||
|
content.WriteString(" host: localhost\n")
|
||||||
|
content.WriteString(" port: \"6379\"\n")
|
||||||
|
content.WriteString(" password: \"\"\n")
|
||||||
|
content.WriteString(" db: 0\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("\n# 日志配置\n")
|
||||||
|
content.WriteString("log:\n")
|
||||||
|
content.WriteString(" level: info\n")
|
||||||
|
content.WriteString(" format: json\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfigFile(content string) error {
|
||||||
|
file, err := os.Create("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
_, err = file.WriteString(content)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"verification-platform-backend/internal/router/admin"
|
"verification-platform-backend/internal/router/admin"
|
||||||
"verification-platform-backend/internal/router/extension"
|
"verification-platform-backend/internal/router/extension"
|
||||||
"verification-platform-backend/internal/router/frontend"
|
"verification-platform-backend/internal/router/frontend"
|
||||||
|
"verification-platform-backend/internal/router/install"
|
||||||
"verification-platform-backend/pkg/response"
|
"verification-platform-backend/pkg/response"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -25,6 +26,8 @@ func SetupRoutes(r *gin.Engine) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
install.SetupRoutes(r)
|
||||||
|
|
||||||
frontend.SetupRoutes(r)
|
frontend.SetupRoutes(r)
|
||||||
|
|
||||||
api := r.Group("/api/v1")
|
api := r.Group("/api/v1")
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Check, ChevronRight, Database, Loader2, RefreshCw, Server, ShieldCheck, User } from 'lucide-vue-next'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
|
import api from '@/services/api'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const testing = ref(false)
|
||||||
|
const step = ref(1)
|
||||||
|
const installStatus = ref<any>(null)
|
||||||
|
const checkingStatus = ref(true)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
db_type: 'sqlite',
|
||||||
|
db_host: 'localhost',
|
||||||
|
db_port: '3306',
|
||||||
|
db_name: 'verification_platform',
|
||||||
|
db_username: 'root',
|
||||||
|
db_password: '',
|
||||||
|
jwt_secret: '',
|
||||||
|
jwt_auto: true,
|
||||||
|
admin_user: 'admin',
|
||||||
|
admin_pass: '',
|
||||||
|
admin_email: '',
|
||||||
|
use_redis: false,
|
||||||
|
redis_host: 'localhost',
|
||||||
|
redis_port: '6379',
|
||||||
|
redis_password: '',
|
||||||
|
redis_db: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dbTypes = [
|
||||||
|
{ value: 'sqlite', label: 'SQLite', desc: '轻量级,无需额外服务,适合小型部署' },
|
||||||
|
{ value: 'mysql', label: 'MySQL', desc: '高性能,适合生产环境' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function checkInstallStatus() {
|
||||||
|
checkingStatus.value = true
|
||||||
|
try {
|
||||||
|
const data = await api.get<any>('/api/install/status')
|
||||||
|
installStatus.value = data
|
||||||
|
if (data?.installed) {
|
||||||
|
router.push('/auth/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('检查安装状态失败:', error)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
checkingStatus.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testDatabase() {
|
||||||
|
testing.value = true
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
db_type: form.value.db_type,
|
||||||
|
}
|
||||||
|
if (form.value.db_type === 'mysql') {
|
||||||
|
payload.host = form.value.db_host
|
||||||
|
payload.port = form.value.db_port
|
||||||
|
payload.name = form.value.db_name
|
||||||
|
payload.username = form.value.db_username
|
||||||
|
payload.password = form.value.db_password
|
||||||
|
}
|
||||||
|
const data = await api.post<any>('/api/install/test-db', payload)
|
||||||
|
toast.success(data?.message || '数据库连接成功')
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
toast.error(error.message || '数据库连接失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateJwtSecret() {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
let result = ''
|
||||||
|
for (let i = 0; i < 32; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
form.value.jwt_secret = result
|
||||||
|
form.value.jwt_auto = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const canProceed = computed(() => {
|
||||||
|
if (step.value === 1) {
|
||||||
|
if (form.value.db_type === 'sqlite') return true
|
||||||
|
return form.value.db_host && form.value.db_port && form.value.db_name && form.value.db_username
|
||||||
|
}
|
||||||
|
if (step.value === 2) {
|
||||||
|
return form.value.jwt_secret || form.value.jwt_auto
|
||||||
|
}
|
||||||
|
if (step.value === 3) {
|
||||||
|
return form.value.admin_user && form.value.admin_pass && form.value.admin_pass.length >= 6
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleInstall() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
db_type: form.value.db_type,
|
||||||
|
jwt_secret: form.value.jwt_auto ? '' : form.value.jwt_secret,
|
||||||
|
admin_user: form.value.admin_user,
|
||||||
|
admin_pass: form.value.admin_pass,
|
||||||
|
admin_email: form.value.admin_email,
|
||||||
|
use_redis: form.value.use_redis,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.value.db_type === 'mysql') {
|
||||||
|
payload.db_host = form.value.db_host
|
||||||
|
payload.db_port = form.value.db_port
|
||||||
|
payload.db_name = form.value.db_name
|
||||||
|
payload.db_username = form.value.db_username
|
||||||
|
payload.db_password = form.value.db_password
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.value.use_redis) {
|
||||||
|
payload.redis_host = form.value.redis_host
|
||||||
|
payload.redis_port = form.value.redis_port
|
||||||
|
payload.redis_password = form.value.redis_password
|
||||||
|
payload.redis_db = form.value.redis_db
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await api.post<any>('/api/install/setup', payload)
|
||||||
|
toast.success(data?.message || '安装成功')
|
||||||
|
step.value = 4
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
toast.error(error.message || '安装失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goNext() {
|
||||||
|
if (step.value < 3) {
|
||||||
|
step.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goLogin() {
|
||||||
|
router.push('/auth/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkInstallStatus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-primary/5 via-background to-background">
|
||||||
|
<div v-if="checkingStatus" class="flex items-center gap-2">
|
||||||
|
<Loader2 class="size-5 animate-spin" />
|
||||||
|
<span>检查安装状态...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="w-full max-w-2xl">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="flex items-center justify-center gap-3 mb-4">
|
||||||
|
<div class="size-12 rounded-xl bg-primary flex items-center justify-center">
|
||||||
|
<ShieldCheck class="size-7 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold">
|
||||||
|
系统安装向导
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted-foreground mt-2">
|
||||||
|
欢迎使用软件授权管理平台,请完成以下配置
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-2 mb-8">
|
||||||
|
<div
|
||||||
|
v-for="s in 4"
|
||||||
|
:key="s"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="size-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors"
|
||||||
|
:class="step >= s ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'"
|
||||||
|
>
|
||||||
|
<Check v-if="step > s" class="size-4" />
|
||||||
|
<span v-else>{{ s }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="s < 4"
|
||||||
|
class="w-12 h-0.5 mx-1"
|
||||||
|
:class="step > s ? 'bg-primary' : 'bg-muted'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardContent class="p-6">
|
||||||
|
<div v-if="step === 1">
|
||||||
|
<div class="flex items-center gap-2 mb-6">
|
||||||
|
<Database class="size-5 text-primary" />
|
||||||
|
<h2 class="text-lg font-semibold">数据库配置</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<UiLabel>数据库类型</UiLabel>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
v-for="db in dbTypes"
|
||||||
|
:key="db.value"
|
||||||
|
type="button"
|
||||||
|
class="p-4 rounded-lg border-2 text-left transition-all"
|
||||||
|
:class="form.db_type === db.value ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'"
|
||||||
|
@click="form.db_type = db.value"
|
||||||
|
>
|
||||||
|
<div class="font-medium">{{ db.label }}</div>
|
||||||
|
<div class="text-sm text-muted-foreground">{{ db.desc }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="form.db_type === 'mysql'">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="db_host">主机地址</UiLabel>
|
||||||
|
<UiInput id="db_host" v-model="form.db_host" placeholder="localhost" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="db_port">端口</UiLabel>
|
||||||
|
<UiInput id="db_port" v-model="form.db_port" placeholder="3306" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="db_name">数据库名</UiLabel>
|
||||||
|
<UiInput id="db_name" v-model="form.db_name" placeholder="verification_platform" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="db_username">用户名</UiLabel>
|
||||||
|
<UiInput id="db_username" v-model="form.db_username" placeholder="root" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="db_password">密码</UiLabel>
|
||||||
|
<UiInput id="db_password" v-model="form.db_password" type="password" placeholder="••••••••" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UiButton variant="outline" :disabled="testing" @click="testDatabase">
|
||||||
|
<Loader2 v-if="testing" class="mr-2 size-4 animate-spin" />
|
||||||
|
测试连接
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="step === 2">
|
||||||
|
<div class="flex items-center gap-2 mb-6">
|
||||||
|
<ShieldCheck class="size-5 text-primary" />
|
||||||
|
<h2 class="text-lg font-semibold">安全配置</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<UiLabel for="jwt_secret">JWT 密钥</UiLabel>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UiCheckbox id="jwt_auto" v-model:checked="form.jwt_auto" />
|
||||||
|
<UiLabel for="jwt_auto" class="text-sm font-normal cursor-pointer">自动生成</UiLabel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UiInput
|
||||||
|
id="jwt_secret"
|
||||||
|
v-model="form.jwt_secret"
|
||||||
|
:disabled="form.jwt_auto"
|
||||||
|
placeholder="留空则自动生成"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<UiButton variant="outline" :disabled="form.jwt_auto" @click="generateJwtSecret">
|
||||||
|
<RefreshCw class="size-4" />
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
JWT 密钥用于签名认证令牌,请妥善保管
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 rounded-lg bg-muted/50">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<Server class="size-4 text-muted-foreground" />
|
||||||
|
<span class="font-medium">Redis 缓存(可选)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<UiCheckbox id="use_redis" v-model:checked="form.use_redis" />
|
||||||
|
<UiLabel for="use_redis" class="text-sm font-normal cursor-pointer">启用 Redis</UiLabel>
|
||||||
|
</div>
|
||||||
|
<template v-if="form.use_redis">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="redis_host">主机</UiLabel>
|
||||||
|
<UiInput id="redis_host" v-model="form.redis_host" placeholder="localhost" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="redis_port">端口</UiLabel>
|
||||||
|
<UiInput id="redis_port" v-model="form.redis_port" placeholder="6379" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 space-y-2">
|
||||||
|
<UiLabel for="redis_password">密码</UiLabel>
|
||||||
|
<UiInput id="redis_password" v-model="form.redis_password" type="password" placeholder="可选" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="step === 3">
|
||||||
|
<div class="flex items-center gap-2 mb-6">
|
||||||
|
<User class="size-5 text-primary" />
|
||||||
|
<h2 class="text-lg font-semibold">管理员账号</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="admin_user">用户名</UiLabel>
|
||||||
|
<UiInput id="admin_user" v-model="form.admin_user" placeholder="admin" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="admin_pass">密码</UiLabel>
|
||||||
|
<UiInput id="admin_pass" v-model="form.admin_pass" type="password" placeholder="至少6位" />
|
||||||
|
<p v-if="form.admin_pass && form.admin_pass.length < 6" class="text-sm text-destructive">
|
||||||
|
密码长度至少6位
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<UiLabel for="admin_email">邮箱(可选)</UiLabel>
|
||||||
|
<UiInput id="admin_email" v-model="form.admin_email" type="email" placeholder="admin@example.com" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="step === 4">
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<div class="size-16 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Check class="size-8 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold mb-2">安装完成</h2>
|
||||||
|
<p class="text-muted-foreground mb-6">
|
||||||
|
系统已成功安装,您现在可以使用管理员账号登录
|
||||||
|
</p>
|
||||||
|
<UiButton @click="goLogin">
|
||||||
|
前往登录
|
||||||
|
<ChevronRight class="ml-2 size-4" />
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
|
||||||
|
<UiCardFooter v-if="step < 4" class="flex justify-between border-t p-6">
|
||||||
|
<UiButton
|
||||||
|
v-if="step > 1"
|
||||||
|
variant="outline"
|
||||||
|
@click="step--"
|
||||||
|
>
|
||||||
|
上一步
|
||||||
|
</UiButton>
|
||||||
|
<div v-else />
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
v-if="step < 3"
|
||||||
|
:disabled="!canProceed"
|
||||||
|
@click="goNext"
|
||||||
|
>
|
||||||
|
下一步
|
||||||
|
<ChevronRight class="ml-2 size-4" />
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-else
|
||||||
|
:disabled="!canProceed || loading"
|
||||||
|
@click="handleInstall"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="loading" class="mr-2 size-4 animate-spin" />
|
||||||
|
{{ loading ? '安装中...' : '开始安装' }}
|
||||||
|
</UiButton>
|
||||||
|
</UiCardFooter>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,35 @@
|
|||||||
import type { Router } from 'vue-router'
|
import type { Router } from 'vue-router'
|
||||||
|
|
||||||
|
let installChecked = false
|
||||||
|
let isInstalled = false
|
||||||
|
|
||||||
|
async function checkInstallStatus(): Promise<boolean> {
|
||||||
|
if (installChecked) return isInstalled
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/install/status')
|
||||||
|
const data = await res.json()
|
||||||
|
isInstalled = data?.data?.installed || false
|
||||||
|
installChecked = true
|
||||||
|
return isInstalled
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
installChecked = true
|
||||||
|
isInstalled = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createRouterGuard(router: Router) {
|
export function createRouterGuard(router: Router) {
|
||||||
router.beforeEach((to) => {
|
router.beforeEach(async (to) => {
|
||||||
|
if (to.path === '/install') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const installed = await checkInstallStatus()
|
||||||
|
if (!installed) {
|
||||||
|
return '/install'
|
||||||
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
const userStr = localStorage.getItem('user')
|
const userStr = localStorage.getItem('user')
|
||||||
|
|
||||||
|
|||||||
Vendored
+13
-3
@@ -14,9 +14,6 @@ import type {
|
|||||||
ParamValueZeroOrMore,
|
ParamValueZeroOrMore,
|
||||||
ParamValueZeroOrOne,
|
ParamValueZeroOrOne,
|
||||||
} from 'vue-router'
|
} from 'vue-router'
|
||||||
import type {
|
|
||||||
_ExtractParamParserType,
|
|
||||||
} from 'vue-router/experimental'
|
|
||||||
|
|
||||||
declare module 'vue-router' {
|
declare module 'vue-router' {
|
||||||
interface TypesConfig {
|
interface TypesConfig {
|
||||||
@@ -621,6 +618,13 @@ declare module 'vue-router/auto-routes' {
|
|||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/install/': RouteRecordInfo<
|
||||||
|
'/install/',
|
||||||
|
'/install',
|
||||||
|
Record<never, never>,
|
||||||
|
Record<never, never>,
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'/profile/': RouteRecordInfo<
|
'/profile/': RouteRecordInfo<
|
||||||
'/profile/',
|
'/profile/',
|
||||||
'/profile',
|
'/profile',
|
||||||
@@ -1155,6 +1159,12 @@ declare module 'vue-router/auto-routes' {
|
|||||||
views:
|
views:
|
||||||
| never
|
| never
|
||||||
}
|
}
|
||||||
|
'src/pages/install/index.vue': {
|
||||||
|
routes:
|
||||||
|
| '/install/'
|
||||||
|
views:
|
||||||
|
| never
|
||||||
|
}
|
||||||
'src/pages/profile/index.vue': {
|
'src/pages/profile/index.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/profile/'
|
| '/profile/'
|
||||||
|
|||||||
Reference in New Issue
Block a user