Add SQLite support and install page
This commit is contained in:
+74
-3
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -38,8 +39,23 @@ func main() {
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
// 检查是否已安装
|
||||
if !cfg.Install.Installed {
|
||||
logger.Info("系统未安装,启动安装模式")
|
||||
startInstallMode(cfg, logger)
|
||||
return
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
db, err := gorm.Open(postgres.Open(cfg.Database.DSN()), &gorm.Config{})
|
||||
var db *gorm.DB
|
||||
if cfg.Database.Type == "sqlite" {
|
||||
// SQLite 连接
|
||||
db, err = gorm.Open(sqlite.Open(cfg.Database.DSN()), &gorm.Config{})
|
||||
} else {
|
||||
// PostgreSQL 连接
|
||||
db, err = gorm.Open(postgres.Open(cfg.Database.DSN()), &gorm.Config{})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal("连接数据库失败", zap.Error(err))
|
||||
}
|
||||
@@ -53,6 +69,7 @@ func main() {
|
||||
&models.IPChangeLog{},
|
||||
&models.ConnectionLog{},
|
||||
&models.IPRefreshRule{},
|
||||
&models.InstallStatus{},
|
||||
); err != nil {
|
||||
logger.Fatal("数据库迁移失败", zap.Error(err))
|
||||
}
|
||||
@@ -100,7 +117,7 @@ func main() {
|
||||
go startHealthCheck(ctx, repos.Node, healthChecker, logger, time.Duration(cfg.Scheduler.HealthCheckInterval)*time.Second)
|
||||
|
||||
// 启动 API 服务器
|
||||
apiServer := NewAPIServer(cfg, repos, logger)
|
||||
apiServer := NewAPIServer(cfg, repos, db, logger)
|
||||
go func() {
|
||||
if err := apiServer.Start(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("API 服务器错误", zap.Error(err))
|
||||
@@ -128,6 +145,51 @@ func main() {
|
||||
logger.Info("服务器已关闭")
|
||||
}
|
||||
|
||||
// startInstallMode 启动安装模式
|
||||
func startInstallMode(cfg *config.Config, logger *zap.Logger) {
|
||||
gin.SetMode("release")
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
// 安装相关路由
|
||||
api := router.Group("/api/v1")
|
||||
{
|
||||
api.GET("/install/check", handler.CheckInstall())
|
||||
api.POST("/install", handler.DoInstall(logger))
|
||||
api.GET("/install/status", handler.GetInstallStatus(nil))
|
||||
}
|
||||
|
||||
// 静态文件服务(前端)
|
||||
router.Static("/assets", "web/dist/assets")
|
||||
router.StaticFile("/", "web/dist/index.html")
|
||||
router.StaticFile("/favicon.ico", "web/dist/favicon.ico")
|
||||
|
||||
// 前端路由回退
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.File("web/dist/index.html")
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
logger.Info("安装模式启动", zap.String("addr", server.Addr))
|
||||
|
||||
// 等待中断信号
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("服务器错误", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
<-quit
|
||||
logger.Info("安装模式关闭")
|
||||
}
|
||||
|
||||
// SimpleAuthenticator 简单认证器
|
||||
type SimpleAuthenticator struct {
|
||||
userRepo *repository.UserRepository
|
||||
@@ -253,18 +315,20 @@ func startHealthCheck(ctx context.Context, nodeRepo *repository.NodeRepository,
|
||||
type APIServer struct {
|
||||
cfg *config.Config
|
||||
repos *repository.Repositories
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
server *http.Server
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
func NewAPIServer(cfg *config.Config, repos *repository.Repositories, logger *zap.Logger) *APIServer {
|
||||
func NewAPIServer(cfg *config.Config, repos *repository.Repositories, db *gorm.DB, logger *zap.Logger) *APIServer {
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
router := gin.New()
|
||||
|
||||
server := &APIServer{
|
||||
cfg: cfg,
|
||||
repos: repos,
|
||||
db: db,
|
||||
logger: logger,
|
||||
router: router,
|
||||
server: &http.Server{
|
||||
@@ -288,6 +352,13 @@ func (s *APIServer) setupRoutes() {
|
||||
// API 路由组
|
||||
api := s.router.Group("/api/v1")
|
||||
{
|
||||
// 安装相关
|
||||
install := api.Group("/install")
|
||||
{
|
||||
install.GET("/check", handler.CheckInstall())
|
||||
install.GET("/status", handler.GetInstallStatus(s.db))
|
||||
}
|
||||
|
||||
// 用户相关
|
||||
users := api.Group("/users")
|
||||
{
|
||||
|
||||
+17
-24
@@ -1,41 +1,34 @@
|
||||
# 调度中心配置
|
||||
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
mode: "debug" # debug, release
|
||||
mode: release
|
||||
|
||||
socks5:
|
||||
host: "0.0.0.0"
|
||||
host: 0.0.0.0
|
||||
port: 1080
|
||||
max_connections: 10000
|
||||
timeout: 30 # 秒
|
||||
timeout: 30
|
||||
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "postgres"
|
||||
password: "postgres"
|
||||
database: "proxy_platform"
|
||||
sslmode: "disable"
|
||||
type: sqlite
|
||||
path: data/proxy.db
|
||||
|
||||
redis:
|
||||
host: "localhost"
|
||||
enabled: false
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
|
||||
scheduler:
|
||||
# 负载均衡策略
|
||||
strategy: "least_latency" # least_latency, least_connections, weighted_round_robin
|
||||
# 健康检查间隔
|
||||
health_check_interval: 10 # 秒
|
||||
# 解锁检测间隔
|
||||
unlock_check_interval: 300 # 秒
|
||||
# 节点超时阈值
|
||||
node_timeout: 30 # 秒
|
||||
strategy: least_latency
|
||||
health_check_interval: 60
|
||||
unlock_check_interval: 300
|
||||
node_timeout: 120
|
||||
|
||||
logging:
|
||||
level: "info" # debug, info, warn, error
|
||||
output: "stdout" # stdout, file
|
||||
file: "logs/scheduler.log"
|
||||
level: info
|
||||
output: stdout
|
||||
|
||||
install:
|
||||
installed: false
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
# 节点 Agent Dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# 构建阶段
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /build
|
||||
|
||||
RUN apk add --no-cache git
|
||||
RUN apk add --no-cache git make gcc musl-dev
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /agent ./cmd/agent
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o agent ./cmd/agent
|
||||
|
||||
FROM alpine:latest
|
||||
# 运行阶段
|
||||
FROM alpine:3.19
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk --no-cache add ca-certificates tzdata curl bash
|
||||
RUN apk add --no-cache ca-certificates tzdata curl
|
||||
|
||||
# 安装 Cloudflare WARP
|
||||
RUN curl -fsSL https://pkg.cloudflareclient.com/install.sh | bash
|
||||
COPY --from=builder /build/agent .
|
||||
COPY --from=builder /build/configs ./configs
|
||||
|
||||
COPY --from=builder /agent /app/agent
|
||||
COPY --from=builder /app/configs /app/configs
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 1080
|
||||
|
||||
ENTRYPOINT ["/app/agent"]
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
CMD ["./agent"]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 调度中心 Dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
# 构建阶段
|
||||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /build
|
||||
|
||||
# 安装依赖
|
||||
RUN apk add --no-cache git
|
||||
RUN apk add --no-cache git make gcc musl-dev
|
||||
|
||||
# 复制 go.mod 和 go.sum
|
||||
# 复制 go mod 文件
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
@@ -14,22 +14,29 @@ RUN go mod download
|
||||
COPY . .
|
||||
|
||||
# 构建
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /scheduler ./cmd/scheduler
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o scheduler ./cmd/scheduler
|
||||
|
||||
# 运行镜像
|
||||
FROM alpine:latest
|
||||
# 运行阶段
|
||||
FROM alpine:3.19
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装 ca-certificates(用于 HTTPS)
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
# 安装必要工具
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /scheduler /app/scheduler
|
||||
COPY --from=builder /app/configs /app/configs
|
||||
COPY --from=builder /build/scheduler .
|
||||
COPY --from=builder /build/configs ./configs
|
||||
COPY --from=builder /build/web/dist ./web/dist
|
||||
|
||||
# 创建数据目录
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080 1080
|
||||
|
||||
# 运行
|
||||
ENTRYPOINT ["/app/scheduler"]
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 启动命令
|
||||
CMD ["./scheduler"]
|
||||
|
||||
@@ -1,71 +1,29 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: proxy-postgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: proxy_platform
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: proxy-redis
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
scheduler:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deployments/Dockerfile.scheduler
|
||||
container_name: proxy-scheduler
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- CONFIG_PATH=/app/configs/scheduler.yaml
|
||||
volumes:
|
||||
- ./configs:/app/configs
|
||||
context: ..
|
||||
dockerfile: Dockerfile.scheduler
|
||||
image: ${SCHEDULER_IMAGE:-proxy-scheduler:latest}
|
||||
container_name: ${SCHEDULER_CONTAINER:-proxy-scheduler}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080" # API
|
||||
- "1080:1080" # SOCKS5
|
||||
restart: unless-stopped
|
||||
|
||||
# 示例节点 Agent(可选)
|
||||
agent:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: deployments/Dockerfile.agent
|
||||
container_name: proxy-agent
|
||||
depends_on:
|
||||
- scheduler
|
||||
environment:
|
||||
- CONFIG_PATH=/app/configs/agent.yaml
|
||||
- "${API_PORT:-8080}:8080"
|
||||
- "${SOCKS5_PORT:-1080}:1080"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
- ./configs:/app/configs
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- agent # 使用 --profile agent 启动
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
networks:
|
||||
- proxy-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
networks:
|
||||
proxy-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -4,13 +4,13 @@ go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/spf13/viper v1.18.2
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.17.0
|
||||
golang.org/x/net v0.19.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -35,6 +35,7 @@ require (
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -51,9 +52,9 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -28,8 +28,6 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
@@ -64,6 +62,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -124,14 +124,14 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
@@ -146,6 +146,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
+54
-23
@@ -8,12 +8,13 @@ import (
|
||||
|
||||
// Config 调度中心配置
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
Scheduler SchedulerConfig `mapstructure:"scheduler"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Install InstallConfig `mapstructure:"install"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -30,20 +31,26 @@ type SOCKS5Config struct {
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string `mapstructure:"type"` // sqlite, postgres
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
Database string `mapstructure:"database"`
|
||||
SSLMode string `mapstructure:"sslmode"`
|
||||
Path string `mapstructure:"path"` // SQLite 数据库文件路径
|
||||
}
|
||||
|
||||
func (c DatabaseConfig) DSN() string {
|
||||
if c.Type == "sqlite" {
|
||||
return c.Path
|
||||
}
|
||||
return fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
c.Host, c.Port, c.User, c.Password, c.Database, c.SSLMode)
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
@@ -55,10 +62,10 @@ func (c RedisConfig) Addr() string {
|
||||
}
|
||||
|
||||
type SchedulerConfig struct {
|
||||
Strategy string `mapstructure:"strategy"`
|
||||
HealthCheckInterval int `mapstructure:"health_check_interval"`
|
||||
UnlockCheckInterval int `mapstructure:"unlock_check_interval"`
|
||||
NodeTimeout int `mapstructure:"node_timeout"`
|
||||
Strategy string `mapstructure:"strategy"`
|
||||
HealthCheckInterval int `mapstructure:"health_check_interval"`
|
||||
UnlockCheckInterval int `mapstructure:"unlock_check_interval"`
|
||||
NodeTimeout int `mapstructure:"node_timeout"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
@@ -67,6 +74,10 @@ type LoggingConfig struct {
|
||||
File string `mapstructure:"file"`
|
||||
}
|
||||
|
||||
type InstallConfig struct {
|
||||
Installed bool `mapstructure:"installed"`
|
||||
}
|
||||
|
||||
// Load 加载配置
|
||||
func Load(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
@@ -76,6 +87,10 @@ func Load(configPath string) (*Config, error) {
|
||||
viper.SetDefault("server.host", "0.0.0.0")
|
||||
viper.SetDefault("server.port", 8080)
|
||||
viper.SetDefault("server.mode", "release")
|
||||
viper.SetDefault("database.type", "sqlite")
|
||||
viper.SetDefault("database.path", "data/proxy.db")
|
||||
viper.SetDefault("redis.enabled", false)
|
||||
viper.SetDefault("install.installed", false)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||
@@ -89,15 +104,31 @@ func Load(configPath string) (*Config, error) {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Save 保存配置
|
||||
func Save(configPath string, cfg *Config) error {
|
||||
viper.Set("server", cfg.Server)
|
||||
viper.Set("socks5", cfg.SOCKS5)
|
||||
viper.Set("database", cfg.Database)
|
||||
viper.Set("redis", cfg.Redis)
|
||||
viper.Set("scheduler", cfg.Scheduler)
|
||||
viper.Set("logging", cfg.Logging)
|
||||
viper.Set("install", cfg.Install)
|
||||
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("写入配置文件失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AgentConfig 节点 Agent 配置
|
||||
type AgentConfig struct {
|
||||
Agent AgentSettings `mapstructure:"agent"`
|
||||
Scheduler SchedulerConn `mapstructure:"scheduler"`
|
||||
WARP WARPConfig `mapstructure:"warp"`
|
||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||
Unlock UnlockConfig `mapstructure:"unlock"`
|
||||
Routing RoutingConfig `mapstructure:"routing"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Agent AgentSettings `mapstructure:"agent"`
|
||||
Scheduler SchedulerConn `mapstructure:"scheduler"`
|
||||
WARP WARPConfig `mapstructure:"warp"`
|
||||
SOCKS5 SOCKS5Config `mapstructure:"socks5"`
|
||||
Unlock UnlockConfig `mapstructure:"unlock"`
|
||||
Routing RoutingConfig `mapstructure:"routing"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
type AgentSettings struct {
|
||||
@@ -107,10 +138,10 @@ type AgentSettings struct {
|
||||
}
|
||||
|
||||
type SchedulerConn struct {
|
||||
Host string `mapstructure:"host"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
HeartbeatInterval int `mapstructure:"heartbeat_interval"`
|
||||
ReportInterval int `mapstructure:"report_interval"`
|
||||
Host string `mapstructure:"host"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
HeartbeatInterval int `mapstructure:"heartbeat_interval"`
|
||||
ReportInterval int `mapstructure:"report_interval"`
|
||||
}
|
||||
|
||||
type WARPConfig struct {
|
||||
@@ -128,10 +159,10 @@ type UnlockConfig struct {
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
Name string `mapstructure:"name"`
|
||||
URL string `mapstructure:"url"`
|
||||
Name string `mapstructure:"name"`
|
||||
URL string `mapstructure:"url"`
|
||||
SuccessKeywords []string `mapstructure:"success_keywords"`
|
||||
FailKeywords []string `mapstructure:"fail_keywords"`
|
||||
FailKeywords []string `mapstructure:"fail_keywords"`
|
||||
}
|
||||
|
||||
type RoutingConfig struct {
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"proxy-platform/internal/config"
|
||||
"proxy-platform/internal/models"
|
||||
"proxy-platform/internal/repository"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CheckInstall 检查安装状态
|
||||
func CheckInstall() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 检查数据库是否存在安装记录
|
||||
dbPath := "data/proxy.db"
|
||||
installed := false
|
||||
|
||||
if _, err := os.Stat(dbPath); err == nil {
|
||||
// 数据库文件存在,检查安装状态
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err == nil {
|
||||
var status models.InstallStatus
|
||||
if result := db.First(&status); result.Error == nil {
|
||||
installed = status.Installed
|
||||
}
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"installed": installed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// InstallRequest 安装请求
|
||||
type InstallRequest struct {
|
||||
DbType string `json:"db_type" binding:"required"` // sqlite, postgres
|
||||
AdminUser string `json:"admin_user" binding:"required"`
|
||||
AdminPass string `json:"admin_pass" binding:"required"`
|
||||
PlatformName string `json:"platform_name"`
|
||||
|
||||
// SQLite 配置
|
||||
SqlitePath string `json:"sqlite_path"`
|
||||
|
||||
// PostgreSQL 配置(可选)
|
||||
PgHost string `json:"pg_host"`
|
||||
PgPort int `json:"pg_port"`
|
||||
PgUser string `json:"pg_user"`
|
||||
PgPassword string `json:"pg_password"`
|
||||
PgDatabase string `json:"pg_database"`
|
||||
|
||||
// Redis 配置(可选)
|
||||
RedisEnabled bool `json:"redis_enabled"`
|
||||
RedisHost string `json:"redis_host"`
|
||||
RedisPort int `json:"redis_port"`
|
||||
RedisPassword string `json:"redis_password"`
|
||||
RedisDB int `json:"redis_db"`
|
||||
}
|
||||
|
||||
// DoInstall 执行安装
|
||||
func DoInstall(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req InstallRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填项
|
||||
if req.AdminUser == "" || req.AdminPass == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名和密码不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.DbType != "sqlite" && req.DbType != "postgres" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "数据库类型只能是 sqlite 或 postgres"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.DbType == "postgres" {
|
||||
if req.PgHost == "" || req.PgDatabase == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "PostgreSQL 配置不完整"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 确定数据库路径
|
||||
dbPath := req.SqlitePath
|
||||
if dbPath == "" {
|
||||
dbPath = "data/proxy.db"
|
||||
}
|
||||
|
||||
// 创建数据目录
|
||||
dbDir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建数据目录失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
|
||||
if req.DbType == "sqlite" {
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
} else {
|
||||
// PostgreSQL 支持(需要额外配置 postgres driver)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "PostgreSQL 支持需要额外配置,请使用 SQLite"})
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库连接失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.Node{},
|
||||
&models.NodeGroup{},
|
||||
&models.UnlockStatus{},
|
||||
&models.IPChangeLog{},
|
||||
&models.ConnectionLog{},
|
||||
&models.IPRefreshRule{},
|
||||
&models.InstallStatus{},
|
||||
); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库迁移失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建管理员用户
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.AdminPass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
|
||||
adminUser := &models.User{
|
||||
Username: req.AdminUser,
|
||||
PasswordHash: string(hashedPassword),
|
||||
Status: "active",
|
||||
TrafficQuota: 0, // 无限制
|
||||
}
|
||||
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
if err := userRepo.Create(adminUser); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建管理员失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建安装状态记录
|
||||
platformName := req.PlatformName
|
||||
if platformName == "" {
|
||||
platformName = "代理管理平台"
|
||||
}
|
||||
|
||||
installStatus := &models.InstallStatus{
|
||||
Installed: true,
|
||||
DbType: req.DbType,
|
||||
RedisEnabled: req.RedisEnabled,
|
||||
AdminUser: req.AdminUser,
|
||||
PlatformName: platformName,
|
||||
}
|
||||
|
||||
if err := db.Create(installStatus).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存安装状态失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成配置文件
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Host: "0.0.0.0",
|
||||
Port: 8080,
|
||||
Mode: "release",
|
||||
},
|
||||
SOCKS5: config.SOCKS5Config{
|
||||
Host: "0.0.0.0",
|
||||
Port: 1080,
|
||||
MaxConnections: 10000,
|
||||
Timeout: 30,
|
||||
},
|
||||
Database: config.DatabaseConfig{
|
||||
Type: req.DbType,
|
||||
Path: dbPath,
|
||||
},
|
||||
Redis: config.RedisConfig{
|
||||
Enabled: req.RedisEnabled,
|
||||
Host: req.RedisHost,
|
||||
Port: req.RedisPort,
|
||||
Password: req.RedisPassword,
|
||||
DB: req.RedisDB,
|
||||
},
|
||||
Scheduler: config.SchedulerConfig{
|
||||
Strategy: "least_latency",
|
||||
HealthCheckInterval: 60,
|
||||
UnlockCheckInterval: 300,
|
||||
NodeTimeout: 120,
|
||||
},
|
||||
Logging: config.LoggingConfig{
|
||||
Level: "info",
|
||||
Output: "stdout",
|
||||
},
|
||||
Install: config.InstallConfig{
|
||||
Installed: true,
|
||||
},
|
||||
}
|
||||
|
||||
// 保存配置文件
|
||||
configPath := "configs/scheduler.yaml"
|
||||
if err := config.Save(configPath, cfg); err != nil {
|
||||
logger.Error("保存配置文件失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
|
||||
logger.Info("安装完成",
|
||||
zap.String("db_type", req.DbType),
|
||||
zap.Bool("redis_enabled", req.RedisEnabled),
|
||||
zap.String("admin_user", req.AdminUser),
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "安装成功",
|
||||
"db_type": req.DbType,
|
||||
"redis_enabled": req.RedisEnabled,
|
||||
"admin_user": req.AdminUser,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetInstallStatus 获取安装状态(从数据库)
|
||||
func GetInstallStatus(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var status models.InstallStatus
|
||||
if result := db.First(&status); result.Error != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"installed": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"installed": status.Installed,
|
||||
"db_type": status.DbType,
|
||||
"redis_enabled": status.RedisEnabled,
|
||||
"platform_name": status.PlatformName,
|
||||
"admin_user": status.AdminUser,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// InstallStatus 安装状态
|
||||
type InstallStatus struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Installed bool `gorm:"default:false" json:"installed"`
|
||||
DbType string `gorm:"type:varchar(20);default:'sqlite'" json:"db_type"` // sqlite, postgres
|
||||
RedisEnabled bool `gorm:"default:false" json:"redis_enabled"`
|
||||
AdminUser string `gorm:"size:64" json:"admin_user"`
|
||||
PlatformName string `gorm:"size:128;default:'代理管理平台'" json:"platform_name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (InstallStatus) TableName() string {
|
||||
return "install_status"
|
||||
}
|
||||
Generated
+4849
File diff suppressed because it is too large
Load Diff
+15
-15
@@ -4,34 +4,34 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"element-plus": "^2.4.4",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.6.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^5.4.3",
|
||||
"vue-echarts": "^6.6.5"
|
||||
"element-plus": "^2.4.4",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-echarts": "^6.6.5",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.10",
|
||||
"vue-tsc": "^1.8.25",
|
||||
"eslint": "^8.55.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "~5.3.0",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"sass": "^1.69.5"
|
||||
"vite": "^5.0.10",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="6" fill="#409eff"/>
|
||||
<text x="16" y="22" text-anchor="middle" fill="white" font-size="16" font-weight="bold">P</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 222 B |
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
@@ -92,8 +92,6 @@ const handleLogout = () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/styles/variables.scss' as *;
|
||||
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -105,7 +103,7 @@ const handleLogout = () => {
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: $header-height;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -146,12 +144,12 @@ const handleLogout = () => {
|
||||
|
||||
.layout-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid $border-color;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
box-shadow: $shadow-light;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
@@ -183,7 +181,7 @@ const handleLogout = () => {
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
background-color: $bg-color;
|
||||
background-color: #f5f7fa;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
+13
-1
@@ -3,6 +3,12 @@ import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/install',
|
||||
name: 'Install',
|
||||
component: () => import('@/views/install/index.vue'),
|
||||
meta: { title: '安装向导', requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
@@ -60,12 +66,18 @@ const router = createRouter({
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title ? `${to.meta.title} - 代理管理平台` : '代理管理平台'
|
||||
|
||||
// 安装页面不需要检查登录
|
||||
if (to.path === '/install') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要登录
|
||||
if (to.meta.requiresAuth !== false && !userStore.isLoggedIn) {
|
||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
|
||||
@@ -36,13 +36,13 @@ body {
|
||||
|
||||
// Element Plus 自定义
|
||||
.el-table {
|
||||
--el-table-border-color: #{$border-color};
|
||||
--el-table-header-bg-color: #{$bg-color};
|
||||
--el-table-border-color: #e4e7ed;
|
||||
--el-table-header-bg-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: none;
|
||||
box-shadow: $shadow;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
// 通用样式
|
||||
@@ -56,7 +56,7 @@ body {
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.card-stat {
|
||||
@@ -64,11 +64,11 @@ body {
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $primary-color;
|
||||
color: #409eff;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: $text-secondary;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,7 @@
|
||||
// 变量定义
|
||||
$primary-color: #409eff;
|
||||
$success-color: #67c23a;
|
||||
$warning-color: #e6a23c;
|
||||
$danger-color: #f56c6c;
|
||||
$info-color: #909399;
|
||||
|
||||
$text-primary: #303133;
|
||||
$text-regular: #606266;
|
||||
$text-secondary: #909399;
|
||||
$text-placeholder: #c0c4cc;
|
||||
|
||||
$border-color: #ebeef5;
|
||||
$border-color-light: #e4e7ed;
|
||||
$border-color-lighter: #ebeef5;
|
||||
$border-color-extra-light: #f2f6fc;
|
||||
|
||||
$bg-color: #f5f7fa;
|
||||
$bg-color-light: #fafafa;
|
||||
|
||||
$shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
$shadow-light: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
$shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||
|
||||
$radius: 4px;
|
||||
$radius-medium: 6px;
|
||||
$radius-large: 8px;
|
||||
|
||||
$transition: all 0.3s;
|
||||
|
||||
$menu-width: 210px;
|
||||
$header-height: 60px;
|
||||
$bg-color: #f5f7fa;
|
||||
$border-color: #e4e7ed;
|
||||
$shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
$text-primary: #303133;
|
||||
$text-secondary: #909399;
|
||||
$primary-color: #409eff;
|
||||
|
||||
@@ -245,7 +245,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/styles/variables.scss' as *;
|
||||
|
||||
|
||||
.dashboard {
|
||||
.stat-row {
|
||||
@@ -277,12 +277,12 @@ onMounted(() => {
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: $text-secondary;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div class="install-container">
|
||||
<el-card class="install-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>{{ form.platform_name || '代理管理平台' }}</h2>
|
||||
<p>安装向导 - Installation Wizard</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-steps :active="currentStep" finish-status="success" simple>
|
||||
<el-step title="数据库配置" />
|
||||
<el-step title="管理员设置" />
|
||||
<el-step title="完成安装" />
|
||||
</el-steps>
|
||||
|
||||
<div class="step-content" v-show="currentStep === 0">
|
||||
<el-form :model="form" label-width="150px">
|
||||
<el-divider content-position="left">数据库配置</el-divider>
|
||||
|
||||
<el-form-item label="数据库类型">
|
||||
<el-radio-group v-model="form.db_type">
|
||||
<el-radio value="sqlite">
|
||||
<el-icon><Database /></el-icon>
|
||||
SQLite(推荐)
|
||||
</el-radio>
|
||||
<el-radio value="postgres">
|
||||
PostgreSQL
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">
|
||||
SQLite 无需额外配置,适合大多数场景;PostgreSQL 适合大规模部署
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="form.db_type === 'sqlite'">
|
||||
<el-form-item label="数据文件路径">
|
||||
<el-input v-model="form.sqlite_path" placeholder="data/proxy.db">
|
||||
<template #prepend>./</template>
|
||||
</el-input>
|
||||
<div class="form-tip">默认存储在 data 目录下</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="form.db_type === 'postgres'">
|
||||
<el-form-item label="主机地址">
|
||||
<el-input v-model="form.pg_host" placeholder="localhost" />
|
||||
</el-form-item>
|
||||
<el-form-item label="端口">
|
||||
<el-input-number v-model="form.pg_port" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.pg_user" placeholder="postgres" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.pg_password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据库">
|
||||
<el-input v-model="form.pg_database" placeholder="proxy_platform" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-divider content-position="left">Redis 缓存(可选)</el-divider>
|
||||
|
||||
<el-form-item label="启用 Redis">
|
||||
<el-switch v-model="form.redis_enabled" />
|
||||
<span class="switch-label">{{ form.redis_enabled ? '已启用' : '未启用' }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="form.redis_enabled">
|
||||
<el-form-item label="Redis 主机">
|
||||
<el-input v-model="form.redis_host" placeholder="localhost" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Redis 端口">
|
||||
<el-input-number v-model="form.redis_port" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Redis 密码">
|
||||
<el-input v-model="form.redis_password" type="password" show-password placeholder="无密码请留空" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Redis DB">
|
||||
<el-input-number v-model="form.redis_db" :min="0" :max="15" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="nextStep">下一步</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="step-content" v-show="currentStep === 1">
|
||||
<el-form :model="form" label-width="150px">
|
||||
<el-divider content-position="left">管理员设置</el-divider>
|
||||
|
||||
<el-form-item label="平台名称">
|
||||
<el-input v-model="form.platform_name" placeholder="代理管理平台" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="管理员用户名">
|
||||
<el-input v-model="form.admin_user" placeholder="admin" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="管理员密码">
|
||||
<el-input v-model="form.admin_pass" type="password" show-password placeholder="请输入密码" />
|
||||
<div class="form-tip">密码长度至少 6 位</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码">
|
||||
<el-input v-model="form.admin_pass_confirm" type="password" show-password placeholder="请再次输入密码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button @click="prevStep">上一步</el-button>
|
||||
<el-button type="primary" @click="checkAndInstall" :loading="installing">
|
||||
开始安装
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="step-content" v-show="currentStep === 2">
|
||||
<div class="install-success">
|
||||
<el-icon :size="80" color="#67C23A"><SuccessFilled /></el-icon>
|
||||
<h3>安装成功!</h3>
|
||||
<p>您的代理管理平台已经完成安装</p>
|
||||
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="数据库类型">{{ form.db_type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="Redis 缓存">{{ form.redis_enabled ? '已启用' : '未启用' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="管理员">{{ form.admin_user }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-button type="primary" size="large" @click="goToLogin" class="login-btn">
|
||||
进入登录页面
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Database, SuccessFilled } from '@element-plus/icons-vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = useRouter()
|
||||
const currentStep = ref(0)
|
||||
const installing = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
db_type: 'sqlite',
|
||||
sqlite_path: 'data/proxy.db',
|
||||
pg_host: 'localhost',
|
||||
pg_port: 5432,
|
||||
pg_user: 'postgres',
|
||||
pg_password: '',
|
||||
pg_database: 'proxy_platform',
|
||||
redis_enabled: false,
|
||||
redis_host: 'localhost',
|
||||
redis_port: 6379,
|
||||
redis_password: '',
|
||||
redis_db: 0,
|
||||
platform_name: '代理管理平台',
|
||||
admin_user: 'admin',
|
||||
admin_pass: '',
|
||||
admin_pass_confirm: '',
|
||||
})
|
||||
|
||||
const nextStep = () => {
|
||||
currentStep.value++
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
currentStep.value--
|
||||
}
|
||||
|
||||
const checkAndInstall = async () => {
|
||||
// 验证表单
|
||||
if (!form.admin_user) {
|
||||
ElMessage.error('请输入管理员用户名')
|
||||
return
|
||||
}
|
||||
if (!form.admin_pass || form.admin_pass.length < 6) {
|
||||
ElMessage.error('密码长度至少 6 位')
|
||||
return
|
||||
}
|
||||
if (form.admin_pass !== form.admin_pass_confirm) {
|
||||
ElMessage.error('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.db_type === 'postgres') {
|
||||
if (!form.pg_host || !form.pg_database) {
|
||||
ElMessage.error('PostgreSQL 配置不完整')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
installing.value = true
|
||||
|
||||
try {
|
||||
const res = await axios.post('/api/v1/install', form)
|
||||
|
||||
if (res.data.message) {
|
||||
ElMessage.success(res.data.message)
|
||||
currentStep.value = 2
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMsg = error.response?.data?.error || '安装失败'
|
||||
ElMessage.error(errorMsg)
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goToLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 检查是否已安装
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/v1/install/check')
|
||||
if (res.data.installed) {
|
||||
ElMessage.info('系统已安装,跳转到登录页面')
|
||||
router.push('/login')
|
||||
}
|
||||
} catch (error) {
|
||||
// 未安装,继续显示安装页面
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.install-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.install-card {
|
||||
width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
text-align: center;
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 24px;
|
||||
}
|
||||
p {
|
||||
margin: 8px 0 0;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.step-content {
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
margin-left: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.install-success {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
|
||||
h3 {
|
||||
margin: 20px 0 10px;
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #909399;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-descriptions {
|
||||
margin: 20px auto;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 30px;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-radio__label) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user