添加前端页面以及相关打包脚本和内置 web 的逻辑

This commit is contained in:
2026-01-14 23:09:16 +08:00
parent e456d3a823
commit 9b646321e1
45 changed files with 583 additions and 10 deletions

40
build.bat Normal file
View File

@@ -0,0 +1,40 @@
@echo off
set APP_NAME=FileRelay.exe
set DIST_DIR=dist
set CONFIG_SRC=config\config.yaml
set CONFIG_DEST=%DIST_DIR%\config\config.yaml
echo 开始构建 %APP_NAME%...
:: 清理 dist 目录
if exist "%DIST_DIR%" (
echo 正在清理 %DIST_DIR% 目录...
rd /s /q "%DIST_DIR%"
)
:: 创建 dist 目录
if not exist "%DIST_DIR%\config" (
mkdir "%DIST_DIR%\config"
)
:: 编译 Go 二进制文件
echo 正在编译...
go build -o "%DIST_DIR%\%APP_NAME%" main.go
if %ERRORLEVEL% equ 0 (
echo 编译成功!
) else (
echo 编译失败!
exit /b 1
)
:: 复制配置文件
if exist "%CONFIG_SRC%" (
echo 正在复制配置文件...
copy "%CONFIG_SRC%" "%CONFIG_DEST%" /Y
) else (
echo 警告: 未找到源配置文件 %CONFIG_SRC%,跳过复制。
)
echo 打包完成!输出目录: %DIST_DIR%
echo 你可以运行 .\%DIST_DIR%\%APP_NAME% 来启动服务。

40
build.ps1 Normal file
View File

@@ -0,0 +1,40 @@
# 设置变量
$APP_NAME = "FileRelay.exe"
$DIST_DIR = "dist"
$CONFIG_SRC = "config\config.yaml"
$CONFIG_DEST = "$DIST_DIR\config\config.yaml"
Write-Host "开始构建 $APP_NAME..." -ForegroundColor Cyan
# 清理 dist 目录
if (Test-Path $DIST_DIR) {
Write-Host "正在清理 $DIST_DIR 目录..."
Remove-Item -Path $DIST_DIR -Recurse -Force
}
# 创建 dist 目录
if (-not (Test-Path "$DIST_DIR\config")) {
New-Item -Path "$DIST_DIR\config" -ItemType Directory -Force | Out-Null
}
# 编译 Go 二进制文件
Write-Host "正在编译..."
go build -o "$DIST_DIR\$APP_NAME" main.go
if ($LASTEXITCODE -eq 0) {
Write-Host "编译成功!" -ForegroundColor Green
} else {
Write-Host "编译失败!" -ForegroundColor Red
exit 1
}
# 复制配置文件
if (Test-Path $CONFIG_SRC) {
Write-Host "正在复制配置文件..."
Copy-Item -Path $CONFIG_SRC -Destination $CONFIG_DEST -Force
} else {
Write-Host "警告: 未找到源配置文件 $CONFIG_SRC,跳过复制。" -ForegroundColor Yellow
}
Write-Host "打包完成!输出目录: $DIST_DIR" -ForegroundColor Green
Write-Host "你可以运行 .\$DIST_DIR\$APP_NAME 来启动服务。"

40
build.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# 设置变量
APP_NAME="FileRelay"
DIST_DIR="dist"
CONFIG_SRC="config/config.yaml"
CONFIG_DEST="$DIST_DIR/config/config.yaml"
echo "开始构建 $APP_NAME..."
# 清理 dist 目录
if [ -d "$DIST_DIR" ]; then
echo "正在清理 $DIST_DIR 目录..."
rm -rf "$DIST_DIR"
fi
# 创建 dist 目录
mkdir -p "$DIST_DIR/config"
# 编译 Go 二进制文件
echo "正在编译..."
go build -o "$DIST_DIR/$APP_NAME" main.go
if [ $? -eq 0 ]; then
echo "编译成功!"
else
echo "编译失败!"
exit 1
fi
# 复制配置文件
if [ -f "$CONFIG_SRC" ]; then
echo "正在复制配置文件..."
cp "$CONFIG_SRC" "$CONFIG_DEST"
else
echo "警告: 未找到源配置文件 $CONFIG_SRC,跳过复制。"
fi
echo "打包完成!输出目录: $DIST_DIR"
echo "你可以运行 ./$DIST_DIR/$APP_NAME 来启动服务。"

View File

@@ -34,3 +34,5 @@ api_token:
max_tokens: 20 max_tokens: 20
database: database:
path: file_relay.db path: file_relay.db
web:
path: web

View File

@@ -855,7 +855,7 @@ const docTemplate = `{
"APITokenAuth": [] "APITokenAuth": []
} }
], ],
"description": "根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。", "description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
"produces": [ "produces": [
"application/octet-stream" "application/octet-stream"
], ],
@@ -893,6 +893,58 @@ const docTemplate = `{
} }
} }
} }
},
"/api/files/{file_id}/{filename}": {
"get": {
"security": [
{
"APITokenAuth": []
}
],
"description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
"produces": [
"application/octet-stream"
],
"tags": [
"Public"
],
"summary": "下载单个文件",
"parameters": [
{
"type": "string",
"description": "文件 ID (UUID)",
"name": "file_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "文件名",
"name": "filename",
"in": "path"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/model.Response"
}
},
"410": {
"description": "Gone",
"schema": {
"$ref": "#/definitions/model.Response"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -969,6 +1021,9 @@ const docTemplate = `{
"admin.UpdateBatchRequest": { "admin.UpdateBatchRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"download_count": {
"type": "integer"
},
"expire_at": { "expire_at": {
"type": "string" "type": "string"
}, },
@@ -1068,6 +1123,10 @@ const docTemplate = `{
"config.SecurityConfig": { "config.SecurityConfig": {
"type": "object", "type": "object",
"properties": { "properties": {
"admin_password": {
"description": "管理员密码明文 (仅用于更新请求,不保存到文件)",
"type": "string"
},
"admin_password_hash": { "admin_password_hash": {
"description": "管理员密码哈希 (bcrypt)", "description": "管理员密码哈希 (bcrypt)",
"type": "string" "type": "string"
@@ -1277,6 +1336,9 @@ const docTemplate = `{
"created_at": { "created_at": {
"type": "string" "type": "string"
}, },
"download_url": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },

View File

@@ -848,7 +848,7 @@
"APITokenAuth": [] "APITokenAuth": []
} }
], ],
"description": "根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。", "description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
"produces": [ "produces": [
"application/octet-stream" "application/octet-stream"
], ],
@@ -886,6 +886,58 @@
} }
} }
} }
},
"/api/files/{file_id}/{filename}": {
"get": {
"security": [
{
"APITokenAuth": []
}
],
"description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
"produces": [
"application/octet-stream"
],
"tags": [
"Public"
],
"summary": "下载单个文件",
"parameters": [
{
"type": "string",
"description": "文件 ID (UUID)",
"name": "file_id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "文件名",
"name": "filename",
"in": "path"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "file"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/model.Response"
}
},
"410": {
"description": "Gone",
"schema": {
"$ref": "#/definitions/model.Response"
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@@ -962,6 +1014,9 @@
"admin.UpdateBatchRequest": { "admin.UpdateBatchRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
"download_count": {
"type": "integer"
},
"expire_at": { "expire_at": {
"type": "string" "type": "string"
}, },
@@ -1061,6 +1116,10 @@
"config.SecurityConfig": { "config.SecurityConfig": {
"type": "object", "type": "object",
"properties": { "properties": {
"admin_password": {
"description": "管理员密码明文 (仅用于更新请求,不保存到文件)",
"type": "string"
},
"admin_password_hash": { "admin_password_hash": {
"description": "管理员密码哈希 (bcrypt)", "description": "管理员密码哈希 (bcrypt)",
"type": "string" "type": "string"
@@ -1270,6 +1329,9 @@
"created_at": { "created_at": {
"type": "string" "type": "string"
}, },
"download_url": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },

View File

@@ -48,6 +48,8 @@ definitions:
type: object type: object
admin.UpdateBatchRequest: admin.UpdateBatchRequest:
properties: properties:
download_count:
type: integer
expire_at: expire_at:
type: string type: string
expire_type: expire_type:
@@ -106,6 +108,9 @@ definitions:
type: object type: object
config.SecurityConfig: config.SecurityConfig:
properties: properties:
admin_password:
description: 管理员密码明文 (仅用于更新请求,不保存到文件)
type: string
admin_password_hash: admin_password_hash:
description: 管理员密码哈希 (bcrypt) description: 管理员密码哈希 (bcrypt)
type: string type: string
@@ -251,6 +256,8 @@ definitions:
type: string type: string
created_at: created_at:
type: string type: string
download_url:
type: string
id: id:
type: string type: string
mime_type: mime_type:
@@ -854,9 +861,42 @@ paths:
summary: 获取公共配置 summary: 获取公共配置
tags: tags:
- Public - Public
/api/files/{file_id}/{filename}:
get:
description: 根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。
parameters:
- description: 文件 ID (UUID)
in: path
name: file_id
required: true
type: string
- description: 文件名
in: path
name: filename
type: string
produces:
- application/octet-stream
responses:
"200":
description: OK
schema:
type: file
"404":
description: Not Found
schema:
$ref: '#/definitions/model.Response'
"410":
description: Gone
schema:
$ref: '#/definitions/model.Response'
security:
- APITokenAuth: []
summary: 下载单个文件
tags:
- Public
/api/files/{file_id}/download: /api/files/{file_id}/download:
get: get:
description: 根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。 description: 根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。
parameters: parameters:
- description: 文件 ID (UUID) - description: 文件 ID (UUID)
in: path in: path

View File

@@ -115,6 +115,20 @@ func (h *PickupHandler) Pickup(c *gin.Context) {
} }
} }
// 生成文件下载绝对路径直链
scheme := "http"
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
scheme = "https"
}
host := c.Request.Host
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
}
for i := range batch.FileItems {
batch.FileItems[i].DownloadURL = fmt.Sprintf("%s://%s/api/files/%s/%s", scheme, host, batch.FileItems[i].ID, batch.FileItems[i].OriginalName)
}
c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{ c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{
Remark: batch.Remark, Remark: batch.Remark,
ExpireAt: batch.ExpireAt, ExpireAt: batch.ExpireAt,
@@ -129,14 +143,16 @@ func (h *PickupHandler) Pickup(c *gin.Context) {
// DownloadFile 下载单个文件 // DownloadFile 下载单个文件
// @Summary 下载单个文件 // @Summary 下载单个文件
// @Description 根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。 // @Description 根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。
// @Tags Public // @Tags Public
// @Security APITokenAuth // @Security APITokenAuth
// @Param file_id path string true "文件 ID (UUID)" // @Param file_id path string true "文件 ID (UUID)"
// @Param filename path string false "文件名"
// @Produce application/octet-stream // @Produce application/octet-stream
// @Success 200 {file} file // @Success 200 {file} file
// @Failure 404 {object} model.Response // @Failure 404 {object} model.Response
// @Failure 410 {object} model.Response // @Failure 410 {object} model.Response
// @Router /api/files/{file_id}/{filename} [get]
// @Router /api/files/{file_id}/download [get] // @Router /api/files/{file_id}/download [get]
func (h *PickupHandler) DownloadFile(c *gin.Context) { func (h *PickupHandler) DownloadFile(c *gin.Context) {
fileID := c.Param("file_id") fileID := c.Param("file_id")
@@ -155,7 +171,12 @@ func (h *PickupHandler) DownloadFile(c *gin.Context) {
if h.batchService.IsExpired(&batch) { if h.batchService.IsExpired(&batch) {
h.batchService.MarkAsExpired(&batch) h.batchService.MarkAsExpired(&batch)
c.JSON(http.StatusGone, model.ErrorResponse(model.CodeGone, "batch expired")) // 按照需求,如果不存在(已在上面处理)或达到上限,返回 404
if batch.ExpireType == "download" && batch.DownloadCount >= batch.MaxDownloads {
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "file not found or download limit reached"))
} else {
c.JSON(http.StatusGone, model.ErrorResponse(model.CodeGone, "batch expired"))
}
return return
} }

View File

@@ -2,6 +2,7 @@ package config
import ( import (
"os" "os"
"path/filepath"
"sync" "sync"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -14,6 +15,11 @@ type Config struct {
Storage StorageConfig `yaml:"storage" json:"storage"` // 存储设置 Storage StorageConfig `yaml:"storage" json:"storage"` // 存储设置
APIToken APITokenConfig `yaml:"api_token" json:"api_token"` // API Token 设置 APIToken APITokenConfig `yaml:"api_token" json:"api_token"` // API Token 设置
Database DatabaseConfig `yaml:"database" json:"database"` // 数据库设置 Database DatabaseConfig `yaml:"database" json:"database"` // 数据库设置
Web WebConfig `yaml:"web" json:"web"` // Web 前端设置
}
type WebConfig struct {
Path string `yaml:"path" json:"path"` // Web 前端资源路径
} }
type SiteConfig struct { type SiteConfig struct {
@@ -78,6 +84,26 @@ func LoadConfig(path string) error {
configLock.Lock() configLock.Lock()
defer configLock.Unlock() defer configLock.Unlock()
// 检查文件是否存在
if _, err := os.Stat(path); os.IsNotExist(err) {
// 创建默认配置
cfg := GetDefaultConfig()
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
// 确保目录存在
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
if err := os.WriteFile(path, data, 0644); err != nil {
return err
}
GlobalConfig = cfg
ConfigPath = path
return nil
}
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
@@ -93,6 +119,47 @@ func LoadConfig(path string) error {
return nil return nil
} }
func GetDefaultConfig() *Config {
return &Config{
Site: SiteConfig{
Name: "文件暂存柜",
Description: "临时文件中转服务",
Logo: "https://www.hxuanyu.com/upload/favicon.png",
},
Security: SecurityConfig{
AdminPasswordHash: "$2a$10$Bm0TEmU4uj.bVHYiIPFBheUkcdg6XHpsanLvmpoAtgU1UnKbo9.vy", // 默认密码: admin123
PickupCodeLength: 6,
PickupFailLimit: 5,
JWTSecret: "file-relay-secret",
},
Upload: UploadConfig{
MaxFileSizeMB: 100,
MaxBatchFiles: 20,
MaxRetentionDays: 30,
RequireToken: false,
},
Storage: StorageConfig{
Type: "local",
Local: struct {
Path string `yaml:"path" json:"path"`
}{
Path: "storage_data",
},
},
APIToken: APITokenConfig{
Enabled: true,
AllowAdminAPI: true,
MaxTokens: 20,
},
Database: DatabaseConfig{
Path: "file_relay.db",
},
Web: WebConfig{
Path: "web",
},
}
}
func SaveConfig() error { func SaveConfig() error {
configLock.RLock() configLock.RLock()
defer configLock.RUnlock() defer configLock.RUnlock()

View File

@@ -11,5 +11,6 @@ type FileItem struct {
StoragePath string `json:"storage_path"` StoragePath string `json:"storage_path"`
Size int64 `json:"size"` Size int64 `json:"size"`
MimeType string `json:"mime_type"` MimeType string `json:"mime_type"`
DownloadURL string `gorm:"-" json:"download_url,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }

127
main.go
View File

@@ -10,8 +10,17 @@ import (
"FileRelay/internal/model" "FileRelay/internal/model"
"FileRelay/internal/task" "FileRelay/internal/task"
"context" "context"
"embed"
"flag"
"fmt" "fmt"
"io"
"io/fs"
"log" "log"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -19,6 +28,9 @@ import (
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
) )
//go:embed all:web
var webFS embed.FS
// @title 文件暂存柜 API // @title 文件暂存柜 API
// @version 1.0 // @version 1.0
// @description 自托管的文件暂存柜后端系统 API 文档 // @description 自托管的文件暂存柜后端系统 API 文档
@@ -44,11 +56,50 @@ import (
// @description Type "Bearer <API-Token>" to authenticate. Required scope depends on the endpoint. // @description Type "Bearer <API-Token>" to authenticate. Required scope depends on the endpoint.
func main() { func main() {
// 注册常用 MIME 类型
mime.AddExtensionType(".js", "application/javascript")
mime.AddExtensionType(".css", "text/css")
mime.AddExtensionType(".woff", "font/woff")
mime.AddExtensionType(".woff2", "font/woff2")
mime.AddExtensionType(".svg", "image/svg+xml")
// 解析命令行参数
configPath := flag.String("config", "config/config.yaml", "path to config file")
flag.Parse()
// 1. 加载配置 // 1. 加载配置
if err := config.LoadConfig("config/config.yaml"); err != nil { if err := config.LoadConfig(*configPath); err != nil {
log.Fatalf("Failed to load config: %v", err) log.Fatalf("Failed to load config: %v", err)
} }
port := 8080
// 打印配置信息
fmt.Println("========================================")
fmt.Println("FileRelay 服务启动中...")
fmt.Printf("配置文件路径: %s\n", *configPath)
fmt.Printf("监听端口: %d\n", port)
fmt.Printf("数据库文件: %s\n", config.GlobalConfig.Database.Path)
fmt.Printf("存储模式: %s\n", config.GlobalConfig.Storage.Type)
webPath := config.GlobalConfig.Web.Path
useExternalWeb := false
if webPath != "" {
if info, err := os.Stat(webPath); err == nil && info.IsDir() {
useExternalWeb = true
}
}
if useExternalWeb {
fmt.Printf("前端资源来源: 外部目录 (%s)\n", webPath)
} else {
fmt.Printf("前端资源来源: 内置嵌入 (嵌入 fs)\n")
if webPath != "" {
fmt.Printf("提示: 配置的外部前端路径 %s 不存在,已回退到内置资源\n", webPath)
}
}
fmt.Println("========================================")
// 2. 初始化 // 2. 初始化
bootstrap.InitDB() bootstrap.InitDB()
@@ -82,7 +133,9 @@ func main() {
api.POST("/batches/text", middleware.APITokenAuth(model.ScopeUpload, !config.GlobalConfig.Upload.RequireToken), uploadHandler.UploadText) api.POST("/batches/text", middleware.APITokenAuth(model.ScopeUpload, !config.GlobalConfig.Upload.RequireToken), uploadHandler.UploadText)
api.GET("/batches/:pickup_code", middleware.PickupRateLimit(), middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.Pickup) api.GET("/batches/:pickup_code", middleware.PickupRateLimit(), middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.Pickup)
api.GET("/batches/:pickup_code/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadBatch) api.GET("/batches/:pickup_code/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadBatch)
// 文件下载保持 /files/:id/download 风格 // 文件下载路由,支持直观的文件名结尾
api.GET("/files/:file_id/:filename", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile)
// 保持旧路由兼容性
api.GET("/files/:file_id/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile) api.GET("/files/:file_id/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile)
// 保持旧路由兼容性 (可选,但为了平滑过渡通常建议保留一段时间或直接更新) // 保持旧路由兼容性 (可选,但为了平滑过渡通常建议保留一段时间或直接更新)
@@ -114,8 +167,74 @@ func main() {
adm.POST("/api-tokens/:id/revoke", tokenHandler.RevokeToken) adm.POST("/api-tokens/:id/revoke", tokenHandler.RevokeToken)
} }
// 静态资源服务 (放在最后,确保 API 路由优先)
webSub, _ := fs.Sub(webFS, "web")
r.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
// 如果请求的是 API 或 Swagger则不处理静态资源 (让其返回 404)
// 注意:此处不排除 /admin因为 /admin 通常是前端 SPA 的路由地址
if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/swagger") {
return
}
// 辅助函数:尝试从外部或嵌入服务文件
serveFile := func(relPath string, allowExternal bool) bool {
// 1. 优先尝试外部路径
if allowExternal && config.GlobalConfig.Web.Path != "" {
fullPath := filepath.Join(config.GlobalConfig.Web.Path, relPath)
if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
c.File(fullPath)
return true
}
}
// 2. 尝试嵌入式文件
f, err := webSub.Open(relPath)
if err == nil {
defer f.Close()
stat, err := f.Stat()
if err == nil && !stat.IsDir() {
// 使用 http.ServeContent 避免 c.FileFromFS 重定向问题
if rs, ok := f.(io.ReadSeeker); ok {
// 显式设置 Content-Type防止某些环境下识别失败
ext := filepath.Ext(relPath)
ctype := mime.TypeByExtension(ext)
if ctype != "" {
c.Header("Content-Type", ctype)
}
http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), rs)
return true
}
}
}
return false
}
// 1. 尝试直接请求的文件 (如果是 / 则尝试 index.html)
requestedPath := strings.TrimPrefix(path, "/")
if requestedPath == "" {
requestedPath = "index.html"
}
if serveFile(requestedPath, true) {
return
}
// 2. SPA 支持:对于非文件请求(没有后缀或不包含点),尝试返回 index.html
// 如果请求的是 assets 目录下的文件(包含点)却没找到,不应该回退到 index.html
isAsset := strings.Contains(requestedPath, ".")
if !isAsset || requestedPath == "index.html" {
if serveFile("index.html", true) {
return
}
}
// 最终找不到则返回 404
c.Status(http.StatusNotFound)
})
// 5. 运行 // 5. 运行
port := 8080
fmt.Printf("Server is running on port %d\n", port)
r.Run(fmt.Sprintf(":%d", port)) r.Run(fmt.Sprintf(":%d", port))
} }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.border-b-2[data-v-3adbfb80]{border-bottom-width:2px}.hover\:border-gray-300[data-v-3adbfb80]:hover{border-color:#d1d5db}.hover\:text-gray-700[data-v-3adbfb80]:hover{color:#374151}.hover\:shadow-lg[data-v-3adbfb80]:hover{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d;transition:box-shadow .3s ease}.flex-col[data-v-3adbfb80]:hover{transform:translateY(-2px);transition:transform .2s ease}.hover\:bg-gray-50[data-v-3adbfb80]:hover{background-color:#f9fafb;transition:background-color .2s ease}@keyframes slideInUp-3adbfb80{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.grid[data-v-3adbfb80]>*{animation:slideInUp-3adbfb80 .3s ease forwards}.grid[data-v-3adbfb80]>*:nth-child(1){animation-delay:.1s}.grid[data-v-3adbfb80]>*:nth-child(2){animation-delay:.2s}.grid[data-v-3adbfb80]>*:nth-child(3){animation-delay:.3s}.grid[data-v-3adbfb80]>*:nth-child(4){animation-delay:.4s}.overflow-x-auto[data-v-3adbfb80]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar{height:6px}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar-track{background:transparent}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:3px}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar-thumb:hover{background:#9ca3afb3}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.animate-spin[data-v-266683e4]{animation:spin-266683e4 1s linear infinite}@keyframes spin-266683e4{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.group:hover .group-hover\:text-indigo-400[data-v-266683e4]{color:#818cf8}input[type=password][data-v-266683e4]::-ms-reveal,input[type=password][data-v-266683e4]::-ms-clear{display:none}.shadow-lg[data-v-266683e4]{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d}.hover\:bg-indigo-700[data-v-266683e4]:hover{background-color:#4338ca;transition:background-color .2s ease}.bg-red-50[data-v-266683e4]{animation:fadeIn-266683e4 .3s ease-in-out}@keyframes fadeIn-266683e4{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.focus\:ring-2[data-v-266683e4]:focus{outline:none;box-shadow:0 0 0 2px #6366f133}.focus\:ring-offset-2[data-v-266683e4]:focus{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px #6366f133}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.border-b-2[data-v-18610c62]{border-bottom-width:2px}.hover\:border-gray-300[data-v-18610c62]:hover{border-color:#d1d5db}.hover\:text-gray-700[data-v-18610c62]:hover{color:#374151}.hover\:bg-gray-50[data-v-18610c62]:hover{background-color:#f9fafb;transition:background-color .2s ease}.overflow-x-auto[data-v-18610c62],.overflow-y-auto[data-v-18610c62]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar{width:6px;height:6px}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar-track,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar-track{background:transparent}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar-thumb,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:3px}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar-thumb:hover,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar-thumb:hover{background:#9ca3afb3}.animate-spin[data-v-18610c62]{animation:spin-18610c62 1s linear infinite}@keyframes spin-18610c62{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.space-y-4[data-v-18610c62]>*{animation:fadeInUp-18610c62 .3s ease forwards}@keyframes fadeInUp-18610c62{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.hover\:scale-105[data-v-18610c62]:hover{transform:scale(1.05);transition:transform .2s ease}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{c as r}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as e,f as o,n as c,u as n,k as d,g as l}from"./index-BhaIiTEj.js";const m=e({__name:"CardDescription",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),o("p",{"data-slot":"card-description",class:c(n(r)("text-muted-foreground text-sm",a.class))},[d(t.$slots,"default")],2))}}),_=e({__name:"CardHeader",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),o("div",{"data-slot":"card-header",class:c(n(r)("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",a.class))},[d(t.$slots,"default")],2))}}),f=e({__name:"CardTitle",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),o("h3",{"data-slot":"card-title",class:c(n(r)("leading-none font-semibold",a.class))},[d(t.$slots,"default")],2))}});export{_,f as a,m as b};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.container[data-v-72019359]{max-width:1200px}.tracking-widest[data-v-72019359]{letter-spacing:.1em}.animate-spin[data-v-72019359]{animation:spin-72019359 1s linear infinite}@keyframes spin-72019359{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.shadow-lg[data-v-72019359]{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d}.hover\:bg-gray-100[data-v-72019359]:hover{transition:background-color .2s ease}.overflow-y-auto[data-v-72019359]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-y-auto[data-v-72019359]::-webkit-scrollbar{width:8px}.overflow-y-auto[data-v-72019359]::-webkit-scrollbar-track{background:transparent}.overflow-y-auto[data-v-72019359]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:4px}

View File

@@ -0,0 +1 @@
import{d as l,N as n,_ as d,u as r,f as u,n as m,X as p,g as f}from"./index-BhaIiTEj.js";import{u as c}from"./Label.vue_vue_type_script_setup_true_lang-D-_Evs0_.js";import{c as b}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";const _=l({__name:"Input",props:{defaultValue:{},modelValue:{},class:{}},emits:["update:modelValue"],setup(a,{emit:o}){const e=a,t=c(e,"modelValue",o,{passive:!0,defaultValue:e.defaultValue});return(v,i)=>n((f(),u("input",{"onUpdate:modelValue":i[0]||(i[0]=s=>p(t)?t.value=s:null),"data-slot":"input",class:m(r(b)("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm","focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]","aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",e.class))},null,2)),[[d,r(t)]])}});export{_};

View File

@@ -0,0 +1 @@
import{P as B,m as E,i as F,j as L,r as N,c as q}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as v,h as y,g as C,z as S,k as O,l as P,u as m,a7 as D,v as J,w as x,c as M,s as T,M as z}from"./index-BhaIiTEj.js";import{u as R}from"./useForwardExpose-CRFbVhil.js";var U=v({__name:"Label",props:{for:{type:String,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:"label"}},setup(t){const e=t;return R(),(s,l)=>(C(),y(m(B),P(e,{onMousedown:l[0]||(l[0]=n=>{!n.defaultPrevented&&n.detail>1&&n.preventDefault()})}),{default:S(()=>[O(s.$slots,"default")]),_:3},16))}}),j=U;function H(t={}){const{inheritAttrs:e=!0}=t,s=T(),l=v({setup(o,{slots:u}){return()=>{s.value=u.default}}}),n=v({inheritAttrs:e,props:t.props,setup(o,{attrs:u,slots:_}){return()=>{var d;if(!s.value)throw new Error("[VueUse] Failed to find the definition of reusable template");const i=(d=s.value)===null||d===void 0?void 0:d.call(s,{...t.props==null?k(u):o,$slots:_});return e&&i?.length===1?i[0]:i}}});return E({define:l,reuse:n},[l,n])}function k(t){const e={};for(const s in t)e[L(s)]=t[s];return e}function A(t){return JSON.parse(JSON.stringify(t))}function Q(t,e,s,l={}){var n,o;const{clone:u=!1,passive:_=!1,eventName:d,deep:i=!1,defaultValue:V,shouldEmit:g}=l,a=D(),b=s||a?.emit||(a==null||(n=a.$emit)===null||n===void 0?void 0:n.bind(a))||(a==null||(o=a.proxy)===null||o===void 0||(o=o.$emit)===null||o===void 0?void 0:o.bind(a?.proxy));let c=d;e||(e="modelValue"),c=c||`update:${e.toString()}`;const h=r=>u?typeof u=="function"?u(r):A(r):r,$=()=>F(t[e])?h(t[e]):V,w=r=>{g?g(r)&&b(c,r):b(c,r)};if(_){const r=J($());let f=!1;return x(()=>t[e],p=>{f||(f=!0,r.value=h(p),z(()=>f=!1))}),x(r,p=>{!f&&(p!==t[e]||i)&&w(p)},{deep:i}),r}else return M({get(){return $()},set(r){w(r)}})}const W=v({__name:"Label",props:{for:{},asChild:{type:Boolean},as:{},class:{}},setup(t){const e=t,s=N(e,"class");return(l,n)=>(C(),y(m(j),P({"data-slot":"label"},m(s),{class:m(q)("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",e.class)}),{default:S(()=>[O(l.$slots,"default")]),_:3},16,["class"]))}});export{W as _,H as c,Q as u};

View File

@@ -0,0 +1 @@
import{d as f,f as r,j as e,I as n,R as _,z as a,E as g,g as i,u as t,i as k,D as d,n as u,C as h}from"./index-BhaIiTEj.js";import{d as m}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";const x={class:"bg-white border-b border-gray-200 sticky top-0 z-10"},v={class:"container mx-auto px-4"},w={class:"flex justify-between items-center h-16"},y={class:"flex items-center space-x-4"},b={key:0,class:"w-8 h-8 rounded-lg overflow-hidden flex items-center justify-center"},C=["src","alt"],j={key:1,class:"w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center"},B={class:"text-lg font-semibold text-gray-900"},z={key:0,class:"text-xs text-gray-500"},V={class:"flex items-center space-x-1"},D=f({__name:"NavBar",props:{showDescription:{type:Boolean}},setup(p){const{config:o}=g();return(c,s)=>{const l=_("router-link");return i(),r("nav",x,[e("div",v,[e("div",w,[e("div",y,[n(l,{to:"/",class:"flex items-center space-x-3"},{default:a(()=>[t(o).site?.logo?(i(),r("div",b,[e("img",{src:t(o).site.logo,alt:t(o).site?.name||"文件中转站",class:"w-full h-full object-contain"},null,8,C)])):(i(),r("div",j,[...s[0]||(s[0]=[e("svg",{class:"w-5 h-5 text-white",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"})],-1)])])),e("div",null,[e("h1",B,d(t(o).site?.name||"文件中转站"),1),p.showDescription?(i(),r("p",z,d(t(o).site?.description||"安全、便捷的文件暂存服务"),1)):k("",!0)])]),_:1})]),e("div",V,[n(l,{to:"/"},{default:a(()=>[n(t(m),{variant:"ghost",size:"sm",class:u(c.$route.path==="/"?"bg-gray-100":"")},{default:a(()=>[...s[1]||(s[1]=[e("svg",{class:"w-4 h-4 mr-2",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"}),e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M8 5l4-4 4 4"})],-1),h(" 取件 ",-1)])]),_:1},8,["class"])]),_:1}),n(l,{to:"/upload"},{default:a(()=>[n(t(m),{variant:"ghost",size:"sm",class:u(c.$route.path==="/upload"?"bg-gray-100":"")},{default:a(()=>[...s[2]||(s[2]=[e("svg",{class:"w-4 h-4 mr-2",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"})],-1),h(" 发送 ",-1)])]),_:1},8,["class"])]),_:1})])])])])}}});export{D as _};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{c as t}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as l,f as o,n as r,u as c,g as n,j as _,k as d}from"./index-BhaIiTEj.js";const i=l({__name:"Skeleton",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("div",{"data-slot":"skeleton",class:r(c(t)("animate-pulse rounded-md bg-primary/10",a.class))},null,2))}}),u={"data-slot":"table-container",class:"relative w-full overflow-auto"},f=l({__name:"Table",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("div",u,[_("table",{"data-slot":"table",class:r(c(t)("w-full caption-bottom text-sm",a.class))},[d(e.$slots,"default")],2)]))}}),h=l({__name:"TableBody",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("tbody",{"data-slot":"table-body",class:r(c(t)("[&_tr:last-child]:border-0",a.class))},[d(e.$slots,"default")],2))}}),x=l({__name:"TableCell",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("td",{"data-slot":"table-cell",class:r(c(t)("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",a.class))},[d(e.$slots,"default")],2))}}),$=l({__name:"TableRow",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("tr",{"data-slot":"table-row",class:r(c(t)("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",a.class))},[d(e.$slots,"default")],2))}}),k=l({__name:"TableHead",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("th",{"data-slot":"table-head",class:r(c(t)("text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",a.class))},[d(e.$slots,"default")],2))}}),w=l({__name:"TableHeader",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("thead",{"data-slot":"table-header",class:r(c(t)("[&_tr]:border-b",a.class))},[d(e.$slots,"default")],2))}});export{i as _,f as a,w as b,$ as c,k as d,h as e,x as f};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{d as l,N as n,f as d,n as u,u as s,X as m,_ as c,g as p}from"./index-BhaIiTEj.js";import{u as f}from"./Label.vue_vue_type_script_setup_true_lang-D-_Evs0_.js";import{c as x}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";const w=l({__name:"Textarea",props:{class:{},defaultValue:{},modelValue:{}},emits:["update:modelValue"],setup(r,{emit:o}){const e=r,a=f(e,"modelValue",o,{passive:!0,defaultValue:e.defaultValue});return(v,t)=>n((p(),d("textarea",{"onUpdate:modelValue":t[0]||(t[0]=i=>m(a)?a.value=i:null),"data-slot":"textarea",class:u(s(x)("border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",e.class))},null,2)),[[c,s(a)]])}});export{w as _};

View File

@@ -0,0 +1 @@
.border-b-2[data-v-c7a1641d]{border-bottom-width:2px}.hover\:border-gray-300[data-v-c7a1641d]:hover{border-color:#d1d5db}.hover\:text-gray-700[data-v-c7a1641d]:hover{color:#374151}.hover\:bg-gray-50[data-v-c7a1641d]:hover{background-color:#f9fafb;transition:background-color .2s ease}code[data-v-c7a1641d]{word-break:break-all;white-space:pre-wrap}.overflow-x-auto[data-v-c7a1641d]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar{width:6px;height:6px}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar-track{background:transparent}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:3px}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar-thumb:hover{background:#9ca3afb3}.animate-spin[data-v-c7a1641d]{animation:spin-c7a1641d 1s linear infinite}@keyframes spin-c7a1641d{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.space-y-4[data-v-c7a1641d]>*{animation:fadeInUp-c7a1641d .3s ease forwards}@keyframes fadeInUp-c7a1641d{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.bg-yellow-50[data-v-c7a1641d]{animation:slideInDown-c7a1641d .3s ease-out}@keyframes slideInDown-c7a1641d{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.bg-blue-50[data-v-c7a1641d]{animation:fadeIn-c7a1641d .3s ease-in-out}@keyframes fadeIn-c7a1641d{0%{opacity:0}to{opacity:1}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.container[data-v-4101ad79]{max-width:1200px}.border-blue-400[data-v-4101ad79]{border-color:#60a5fa}.bg-blue-50[data-v-4101ad79]{background-color:#eff6ff}.animate-spin[data-v-4101ad79]{animation:spin-4101ad79 1s linear infinite}@keyframes spin-4101ad79{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.shadow-lg[data-v-4101ad79]{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d}.overflow-y-auto[data-v-4101ad79]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-y-auto[data-v-4101ad79]::-webkit-scrollbar{width:8px}.overflow-y-auto[data-v-4101ad79]::-webkit-scrollbar-track{background:transparent}.overflow-y-auto[data-v-4101ad79]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:4px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{r as i,P as n,c as o,g as d}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as c,h as v,u as e,l,z as u,k as g,g as f}from"./index-BhaIiTEj.js";const x=c({__name:"Badge",props:{asChild:{type:Boolean},as:{},variant:{},class:{}},setup(r){const a=r,t=i(a,"class");return(s,b)=>(f(),v(e(n),l({"data-slot":"badge",class:e(o)(e(p)({variant:r.variant}),a.class)},e(t)),{default:u(()=>[g(s.$slots,"default")]),_:3},16,["class"]))}}),p=d("inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",secondary:"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",destructive:"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",outline:"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"}},defaultVariants:{variant:"default"}});export{x as _};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{h as o}from"./Teleport-DV_BGdH0.js";function f(t){return t?"open":"closed"}function c(t){const e=o();for(const n of t)if(n===e||(n.focus(),o()!==e))return}export{c as f,f as g};

14
web/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue-vite-shadcn-template</title>
<script type="module" crossorigin src="/assets/index-BhaIiTEj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-o9d4BZZI.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

1
web/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB