This commit is contained in:
oneao committed 2026-04-17 17:25:15 +08:00
1 parent e893ef2e48
commit b6415c0d65
25 files changed
+1495 -112

No files matched your search

@@ -5,6 +5,7 @@ import (
"allapp-go/internal/httpx" "allapp-go/internal/httpx"
"allapp-go/internal/types" "allapp-go/internal/types"
"allapp-go/pkg/db" "allapp-go/pkg/db"
"allapp-go/pkg/uniqueid"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -48,3 +49,22 @@ func LoadDataBySql(c fiber.Ctx) error {
return httpx.OK(c, data) return httpx.OK(c, data)
} }
func GetUniqueId(c fiber.Ctx) error {
var req types.GetUniqueIdReq
if err := httpx.BindAndValidate(c, &req); err != nil {
return errors.WithStack(err)
}
count := req.Count
// 预分配,避免扩容
ids := make([]int64, 0, count)
for i := 0; i < count; i++ {
ids = append(ids, uniqueid.NextId())
}
return httpx.OK(c, ids)
}
@@ -21,6 +21,7 @@ func SaveData(c fiber.Ctx) error {
} }
ctx := c.Context() ctx := c.Context()
dbClient := db.New() dbClient := db.New()
err := dbClient.WithTx(ctx, func(tx *db.Client) error { err := dbClient.WithTx(ctx, func(tx *db.Client) error {
@@ -1,6 +1,8 @@
package httpx package httpx
import ( import (
"reflect"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -8,10 +10,23 @@ import (
var validate = validator.New() var validate = validator.New()
func BindAndValidate(c fiber.Ctx, v any) error { func BindAndValidate(c fiber.Ctx, v any) error {
// 1️⃣ 绑定 JSON
if err := c.Bind().Body(v); err != nil { if err := c.Bind().Body(v); err != nil {
return err return err
} }
// 2️⃣ 只允许 struct 校验(先不支持 slice)
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
// 如果是 slice,直接跳过校验
if rv.Kind() == reflect.Slice {
return nil
}
// 3️⃣ struct 校验
if err := validate.Struct(v); err != nil { if err := validate.Struct(v); err != nil {
return err return err
} }
@@ -1,9 +1,7 @@
package middleware package middleware
import ( import (
"allapp-go/internal/httpx" "context"
"allapp-go/pkg/jwtx"
"strings"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -13,35 +11,30 @@ const CtxUserIDKey = "user_id"
// Auth 登录校验中间件 // Auth 登录校验中间件
func Auth() fiber.Handler { func Auth() fiber.Handler {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
token := strings.TrimSpace(c.Get("Token")) //token := strings.TrimSpace(c.Get("Token"))
//
if token == "" { //if token == "" {
return httpx.Unauthorized(c, "未登录") // return httpx.Unauthorized(c, "未登录")
} //}
//
//verifyToken, ok := jwtx.VerifyToken(c.Context(), token)
//if !ok {
// return httpx.Unauthorized(c, "登录已过期")
//}
//
//c.Locals(CtxUserIDKey, verifyToken.Data["user_id"])
verifyToken, ok := jwtx.VerifyToken(c.Context(), token) ctx := context.WithValue(c.Context(), CtxUserIDKey, int64(779895419121733))
if !ok { c.SetContext(ctx)
return httpx.Unauthorized(c, "登录已过期")
}
c.Locals(CtxUserIDKey, verifyToken.Data["user_id"])
return c.Next() return c.Next()
} }
} }
func GetUserID(c fiber.Ctx) (int64, error) { func GetUserID(ctx context.Context) (int64, bool) {
v := c.Locals(CtxUserIDKey) v := ctx.Value(CtxUserIDKey)
if v == nil { id, ok := v.(int64)
return 0, httpx.Unauthorized(c, "登录已过期")
}
userID, ok := v.(int64) return id, ok
if !ok {
return 0, httpx.Unauthorized(c, "登录已过期")
}
return userID, nil
} }
@@ -3,6 +3,7 @@ package router
import ( import (
"allapp-go/internal/config" "allapp-go/internal/config"
"allapp-go/internal/handle" "allapp-go/internal/handle"
"allapp-go/internal/middleware"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -11,10 +12,14 @@ func SetupRouter(app *fiber.App, cfg *config.Config) {
api := app.Group(cfg.App.BaseUrl) api := app.Group(cfg.App.BaseUrl)
// ==================== data ==================== // ==================== data ====================
api.Post("/data/loadData", handle.LoadData) data := api.Group("/data", middleware.Auth())
api.Post("/data/loadDataBySql", handle.LoadDataBySql)
api.Post("/data/saveData", handle.SaveData)
data.Post("/loadData", handle.LoadData)
data.Post("/loadDataBySql", handle.LoadDataBySql)
data.Post("/saveData", handle.SaveData)
data.Post("/getUniqueId", handle.GetUniqueId)
// ==================== auth ====================
api.Post("/auth/login/qq", handle.LoginQq) api.Post("/auth/login/qq", handle.LoginQq)
api.Post("/auth/login/wechat", handle.LoginWechat) api.Post("/auth/login/wechat", handle.LoginWechat)
} }
@@ -12,3 +12,7 @@ type LoadDataBySqlReq struct {
Sql string `json:"sql" validate:"required"` Sql string `json:"sql" validate:"required"`
Args []any `json:"args"` Args []any `json:"args"`
} }
type GetUniqueIdReq struct {
Count int `json:"count" binding:"required,min=1,max=100"`
}
@@ -6,6 +6,11 @@ import (
"strings" "strings"
) )
var auditExcludeTables = map[string]bool{
"user": true,
"user_oauth": true,
}
func validateData(table string, keyField string, data map[string]any) (any, error) { func validateData(table string, keyField string, data map[string]any) (any, error) {
if table == "" { if table == "" {
return nil, fmt.Errorf("表名不能为空") return nil, fmt.Errorf("表名不能为空")
+21
View File
@@ -1,10 +1,12 @@
package db package db
import ( import (
"allapp-go/internal/middleware"
"context" "context"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"time"
) )
// 构建 INSERT SQL(单条) // 构建 INSERT SQL(单条)
@@ -43,6 +45,25 @@ func (c *Client) Insert(
return err return err
} }
if !auditExcludeTables[table] {
now := time.Now()
delete(data, "create_time")
delete(data, "update_time")
delete(data, "create_by")
delete(data, "update_by")
data["create_time"] = now
data["update_time"] = now
userID, flag := middleware.GetUserID(ctx)
if flag {
data["create_by"] = userID
data["update_by"] = userID
}
}
sql, args := buildInsertSQL(table, data) sql, args := buildInsertSQL(table, data)
_, err := c.exec().Exec(ctx, sql, args...) _, err := c.exec().Exec(ctx, sql, args...)
+27 -21
View File
@@ -1,9 +1,12 @@
package db package db
import ( import (
"allapp-go/pkg/logger"
"context" "context"
"fmt" "fmt"
"strings" "strings"
"go.uber.org/zap"
) )
func (c *Client) LoadData( func (c *Client) LoadData(
@@ -15,33 +18,32 @@ func (c *Client) LoadData(
args []any, args []any,
) ([]map[string]any, error) { ) ([]map[string]any, error) {
var where string selectCols := "*"
if len(searchColumns) > 0 {
// WHERE 构造 selectCols = strings.Join(searchColumns, ", ")
if searchCondition != "" && len(searchColumns) > 0 {
var conditions []string
for _, col := range searchColumns {
conditions = append(conditions, fmt.Sprintf("%s LIKE ?", col))
}
where = " WHERE (" + strings.Join(conditions, " OR ") + ")"
// 自动加 %
for i := range args {
if s, ok := args[i].(string); ok {
args[i] = "%" + s + "%"
}
}
} }
// ORDER BY where := ""
var order string if searchCondition != "" {
where = " WHERE " + searchCondition
}
order := ""
if orderBy != "" { if orderBy != "" {
order = " ORDER BY " + orderBy order = " ORDER BY " + orderBy
} }
sql := fmt.Sprintf("SELECT * FROM %s%s%s", viewName, where, order) sql := fmt.Sprintf("SELECT %s FROM %s%s%s",
selectCols,
viewName,
where,
order,
)
logger.FromCtx(ctx).Info("LoadData",
zap.String("sql", sql),
zap.Any("args", args),
)
return c.query(ctx, sql, args...) return c.query(ctx, sql, args...)
} }
@@ -51,6 +53,10 @@ func (c *Client) LoadDataBySQL(
sql string, sql string,
args []any, args []any,
) ([]map[string]any, error) { ) ([]map[string]any, error) {
logger.FromCtx(ctx).Info("LoadDataBySQL",
zap.String("sql", sql),
zap.Any("args", args),
)
return c.query(ctx, sql, args...) return c.query(ctx, sql, args...)
} }
+17
View File
@@ -1,10 +1,12 @@
package db package db
import ( import (
"allapp-go/internal/middleware"
"context" "context"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"time"
) )
func (c *Client) Update( func (c *Client) Update(
@@ -18,6 +20,21 @@ func (c *Client) Update(
return err return err
} }
if !auditExcludeTables[table] {
now := time.Now()
delete(data, "update_time")
delete(data, "update_by")
data["update_time"] = now
userID, flag := middleware.GetUserID(ctx)
if flag {
data["update_by"] = userID
}
}
var ( var (
setClauses []string setClauses []string
args []any args []any
+2
View File
@@ -46,10 +46,12 @@
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-native": "0.83.4", "react-native": "0.83.4",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.30.0",
"react-native-keyboard-controller": "1.20.7",
"react-native-reanimated": "4.2.1", "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2", "react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0", "react-native-screens": "~4.23.0",
"react-native-shadow-2": "^7.1.2", "react-native-shadow-2": "^7.1.2",
"react-native-sortables": "^1.9.4",
"react-native-svg": "15.15.3", "react-native-svg": "15.15.3",
"react-native-web": "~0.21.0", "react-native-web": "~0.21.0",
"react-native-worklets": "0.7.2", "react-native-worklets": "0.7.2",
+86 -1
View File
@@ -107,6 +107,9 @@ importers:
react-native-gesture-handler: react-native-gesture-handler:
specifier: ~2.30.0 specifier: ~2.30.0
version: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) version: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-keyboard-controller:
specifier: 1.20.7
version: 1.20.7(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-reanimated: react-native-reanimated:
specifier: 4.2.1 specifier: 4.2.1
version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -119,6 +122,9 @@ importers:
react-native-shadow-2: react-native-shadow-2:
specifier: ^7.1.2 specifier: ^7.1.2
version: 7.1.2(react-native-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)) version: 7.1.2(react-native-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))
react-native-sortables:
specifier: ^1.9.4
version: 1.9.4(react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-svg: react-native-svg:
specifier: 15.15.3 specifier: 15.15.3
version: 15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) version: 15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -1004,48 +1010,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.44.0': '@oxfmt/binding-linux-arm64-musl@0.44.0':
resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==} resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.44.0': '@oxfmt/binding-linux-ppc64-gnu@0.44.0':
resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==} resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.44.0': '@oxfmt/binding-linux-riscv64-gnu@0.44.0':
resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==} resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.44.0': '@oxfmt/binding-linux-riscv64-musl@0.44.0':
resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==} resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.44.0': '@oxfmt/binding-linux-s390x-gnu@0.44.0':
resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==} resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.44.0': '@oxfmt/binding-linux-x64-gnu@0.44.0':
resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==} resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.44.0': '@oxfmt/binding-linux-x64-musl@0.44.0':
resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==} resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.44.0': '@oxfmt/binding-openharmony-arm64@0.44.0':
resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==} resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==}
@@ -1118,48 +1132,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.60.0': '@oxlint/binding-linux-arm64-musl@1.60.0':
resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==} resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.60.0': '@oxlint/binding-linux-ppc64-gnu@1.60.0':
resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==} resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.60.0': '@oxlint/binding-linux-riscv64-gnu@1.60.0':
resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==} resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.60.0': '@oxlint/binding-linux-riscv64-musl@1.60.0':
resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==} resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.60.0': '@oxlint/binding-linux-s390x-gnu@1.60.0':
resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==} resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.60.0': '@oxlint/binding-linux-x64-gnu@1.60.0':
resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==} resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.60.0': '@oxlint/binding-linux-x64-musl@1.60.0':
resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==} resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.60.0': '@oxlint/binding-openharmony-arm64@1.60.0':
resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==} resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==}
@@ -1578,24 +1600,28 @@ packages:
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.1': '@tailwindcss/oxide-linux-arm64-musl@4.2.1':
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.1': '@tailwindcss/oxide-linux-x64-gnu@4.2.1':
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.1': '@tailwindcss/oxide-linux-x64-musl@4.2.1':
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.1': '@tailwindcss/oxide-wasm32-wasi@4.2.1':
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
@@ -2513,7 +2539,7 @@ packages:
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me deprecated: Glob versions prior to v9 are no longer supported
gopd@1.2.0: gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
@@ -2846,72 +2872,84 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.31.1: lightningcss-linux-arm64-gnu@1.31.1:
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.32.0: lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.30.1: lightningcss-linux-arm64-musl@1.30.1:
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.31.1: lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.32.0: lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.30.1: lightningcss-linux-x64-gnu@1.30.1:
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.31.1: lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.32.0: lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.30.1: lightningcss-linux-x64-musl@1.30.1:
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.31.1: lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.32.0: lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.30.1: lightningcss-win32-arm64-msvc@1.30.1:
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
@@ -3454,6 +3492,15 @@ packages:
react: '*' react: '*'
react-native: '*' react-native: '*'
react-native-haptic-feedback@3.0.0:
resolution: {integrity: sha512-yxBQFHhXU8S0xupGtzDRToGw5yMWezUdLzxtlx2Oi9YpjPwprWZWCw8foPt0S2t84B0ZikzZYyOHiMSXIelUxg==}
peerDependencies:
react: '>=18.0.0'
react-native: '>=0.71.0'
peerDependenciesMeta:
react:
optional: true
react-native-iconfont-cli@2.2.4: react-native-iconfont-cli@2.2.4:
resolution: {integrity: sha512-qTjfHG2f/sc3/YKWZJvIJk/SwiTu8C9/SvtoLxRX+GM/6pvOJq0ge6rcV3vTh4s3QJDuNDOD6+a4hGD6vv2vSg==} resolution: {integrity: sha512-qTjfHG2f/sc3/YKWZJvIJk/SwiTu8C9/SvtoLxRX+GM/6pvOJq0ge6rcV3vTh4s3QJDuNDOD6+a4hGD6vv2vSg==}
hasBin: true hasBin: true
@@ -3474,6 +3521,13 @@ packages:
react: '*' react: '*'
react-native: '*' react-native: '*'
react-native-keyboard-controller@1.20.7:
resolution: {integrity: sha512-G8S5jz1FufPrcL1vPtReATx+jJhT/j+sTqxMIb30b1z7cYEfMlkIzOCyaHgf6IMB2KA9uBmnA5M6ve2A9Ou4kw==}
peerDependencies:
react: '*'
react-native: '*'
react-native-reanimated: '>=3.0.0'
react-native-reanimated@4.2.1: react-native-reanimated@4.2.1:
resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==} resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==}
peerDependencies: peerDependencies:
@@ -3499,6 +3553,14 @@ packages:
react-native: '*' react-native: '*'
react-native-svg: '>=12.1.0' react-native-svg: '>=12.1.0'
react-native-sortables@1.9.4:
resolution: {integrity: sha512-a6hxT+gl14HA5Sm8UiLXJqF8KMEQVa+mUJd75OnzoVsmrxUDtjAatlMdV0kI9qTQDT/ZSFLPRmdUhOR762IA4g==}
peerDependencies:
react: '*'
react-native: '*'
react-native-gesture-handler: '>=2.0.0'
react-native-reanimated: '>=3.0.0'
react-native-svg@15.15.3: react-native-svg@15.15.3:
resolution: {integrity: sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==} resolution: {integrity: sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==}
peerDependencies: peerDependencies:
@@ -7868,6 +7930,13 @@ snapshots:
react: 19.2.0 react: 19.2.0
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
react-native-haptic-feedback@3.0.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
optionalDependencies:
react: 19.2.0
optional: true
react-native-iconfont-cli@2.2.4(react-native-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): react-native-iconfont-cli@2.2.4(react-native-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
colors: 1.4.0 colors: 1.4.0
@@ -7893,6 +7962,13 @@ snapshots:
react: 19.2.0 react: 19.2.0
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
react-native-keyboard-controller@1.20.7(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
react-native-is-edge-to-edge: 1.3.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-reanimated: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
react: 19.2.0 react: 19.2.0
@@ -7919,6 +7995,15 @@ snapshots:
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
react-native-svg: 15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) react-native-svg: 15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-sortables@1.9.4(react-native-gesture-handler@2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
react: 19.2.0
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
react-native-gesture-handler: 2.30.1(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-reanimated: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
optionalDependencies:
react-native-haptic-feedback: 3.0.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
react-native-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): react-native-svg@15.15.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
css-select: 5.2.2 css-select: 5.2.2
+6 -6
View File
@@ -20,9 +20,9 @@ const tabs: TabbarItem[] = [
icon: HomeIcon, icon: HomeIcon,
}, },
{ {
id: "/chat", id: "/space",
title: "聊天", title: "空间",
icon: MessageCircleIcon, icon: EarthIcon,
}, },
{ {
id: "/tools", id: "/tools",
@@ -31,9 +31,9 @@ const tabs: TabbarItem[] = [
isCenter: true, isCenter: true,
}, },
{ {
id: "/space", id: "/chat",
title: "空间", title: "聊天",
icon: EarthIcon, icon: MessageCircleIcon,
}, },
{ {
id: "/my", id: "/my",
+36 -29
View File
@@ -1,36 +1,43 @@
import { Button, Dialog } from "heroui-native"; import { Button, Toast, useToast, useThemeColor } from "heroui-native";
import { View } from "react-native"; import { View } from "react-native";
import { useState } from "react";
export default function DialogExample() { export default function ToastExample() {
const [isOpen, setIsOpen] = useState(false); const { toast } = useToast();
const themeColorForeground = useThemeColor("foreground");
return ( return (
<> <View className="gap-4 p-4">
<Button variant="primary" onPress={() => { <Button
setIsOpen(true) onPress={() =>
}}>Open Dialog</Button> toast.show({
variant: "success",
label: "You have upgraded your plan",
description: "You can continue using HeroUI Chat",
actionLabel: "Close",
onActionPress: ({ hide }) => hide(),
})
}
>
Show Success Toast
</Button>
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}> <Button
<Dialog.Portal> onPress={() =>
<Dialog.Overlay /> toast.show({
<Dialog.Content> duration: 0,
<Dialog.Close variant="ghost" /> component: (props) => (
<View className="mb-5 gap-1.5"> <Toast variant="accent" {...props}>
<Dialog.Title>Confirm Action</Dialog.Title> <Toast.Title>Custom Toast</Toast.Title>
<Dialog.Description> <Toast.Description>This uses a custom component</Toast.Description>
Are you sure you want to proceed with this action? This cannot be undone. <Toast.Action onPress={() => props.hide()}>Undo</Toast.Action>
</Dialog.Description> <Toast.Close className="absolute top-0 right-0" />
</View> </Toast>
<View className="flex-row justify-end gap-3"> ),
<Button variant="ghost" size="sm" onPress={() => setIsOpen(false)}> })
Cancel }
</Button> >
<Button size="sm">Confirm</Button> Show Custom Toast
</View> </Button>
</Dialog.Content> </View>
</Dialog.Portal>
</Dialog>
</>
); );
} }
+556 -3
View File
@@ -1,13 +1,566 @@
import React from "react"; import React, { useEffect, useState, useRef } from "react";
import LayoutView from "@/components/layout/view"; import LayoutView from "@/components/layout/view";
import { Text } from "react-native"; import { View, Text } from "react-native";
import {
Button,
Separator,
Spinner,
Chip,
BottomSheet,
Label,
TextField,
Input,
InputOTP,
type InputOTPRef,
PressableFeedback,
useBottomSheetAwareHandlers,
} from "heroui-native";
import { PlusIcon, HousePlusIcon, GripIcon } from "lucide-react-native";
import { useUserStore } from "@/store";
import { getUniqueIdApi, loadDataApi, loadDataBySqlApi, saveDataApi } from "@/request/api";
import EmptyState from "@/components/EmptyStatus";
import Sortable from "react-native-sortables";
import { useSpaceStore } from "@/store";
import { useRouter } from "expo-router";
import { UserRoundIcon, UsersRoundIcon } from "lucide-react-native";
import { toast } from "sonner-native";
import { FadeIn } from "react-native-reanimated";
import { generateCode } from "@/utils/common";
export default function Space() { export default function Space() {
const router = useRouter();
const userStore = useUserStore();
const spaceStore = useSpaceStore();
const ITEM_HEIGHT = 64;
const [spaces, setSpaces] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 加入
const [showJoin, setShowJoin] = useState(false);
const [joinLoading, setJoinLoading] = useState(false);
const [joinForm, setJoinForm] = useState({
invite_code: "",
});
const joinInputRef = useRef<InputOTPRef>(null);
// 新建
const [showCreate, setShowCreate] = useState(false);
const [createForm, setCreateForm] = useState({
name: "",
});
const [createLoading, setCreateLoading] = useState(false);
const createInputRef = useRef<any>(null);
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
useEffect(() => {
if (showCreate) {
setTimeout(() => {
createInputRef.current?.focus();
}, 0);
}
}, [showCreate]);
useEffect(() => {
if (showJoin) {
setTimeout(() => {
joinInputRef.current?.focus();
}, 0);
}
}, [showJoin]);
useEffect(() => {
listUserSpaces();
}, []);
function listUserSpaces(loading = true) {
if (loading) {
setLoading(true);
}
loadDataApi({
view_name: "v_user_spaces",
search_condition: `v_user_id = ?`,
search_columns: [],
order_by: "",
args: [userStore.userInfo.id],
}).then((res) => {
if (res.isSuccess) {
setSpaces(res.data);
if (loading) {
setLoading(false);
}
}
});
}
async function handleCreate() {
if (createLoading) return;
if (createForm.name === "") {
toast.warning("空间名称不能为空");
return;
}
setCreateLoading(true);
try {
let inviteCode = "";
let isExist = true;
// 循环生成直到不存在
while (isExist) {
inviteCode = generateCode(6);
const res = await loadDataBySqlApi({
sql: `SELECT 1 FROM space WHERE invite_code = ? LIMIT 1`,
args: [inviteCode],
});
if (!res.isSuccess) {
throw new Error("校验邀请码失败");
}
isExist = res.data.length > 0;
}
// 设置是否是默认空间
let isDefault = false;
const defaultRes = await loadDataApi({
view_name: "v_user_spaces",
search_condition: `v_user_id = ?`,
search_columns: ["v_is_default"],
order_by: "",
args: [userStore.userInfo.id],
});
if (!defaultRes.isSuccess) {
return;
}
if (defaultRes.data.length == 0) {
isDefault = true;
}
// 创建
const autoRes = await getUniqueIdApi(2);
if (!autoRes.isSuccess) {
return;
}
const spaceId = autoRes.data[0];
const spaceMemberId = autoRes.data[1];
// 插入空间
const requestData = [];
requestData.push({
table_name: "space",
key_field: "id",
inserts: [
{
id: spaceId,
name: createForm.name,
invite_code: inviteCode,
owner_id: userStore.userInfo.id,
status: 0,
},
],
updates: [],
deletes: [],
});
// 插入空间用户
requestData.push({
table_name: "space_member",
key_field: "id",
inserts: [
{
id: spaceMemberId,
space_id: spaceId,
user_id: userStore.userInfo.id,
role: 0,
is_default: isDefault ? 1 : 0,
sort_number: defaultRes.data.length,
member_name: userStore.userInfo.nickname,
},
],
updates: [],
deletes: [],
});
const saveRes = await saveDataApi(requestData);
if (saveRes.isSuccess) {
toast.success("创建成功");
setShowCreate(false);
setCreateForm({
name: "",
});
listUserSpaces();
}
} catch (e) {
console.log(e);
} finally {
setCreateLoading(false);
}
}
async function handleJoin() {
if (joinLoading) return;
const code = joinForm.invite_code.trim();
if (!code) {
toast.warning("邀请码不能为空");
return;
}
if (!/^[A-Za-z0-9]{6}$/.test(code)) {
toast.warning("邀请码必须为6位字母或数字");
return;
}
setJoinLoading(true);
try {
const res = await loadDataBySqlApi({
sql: ` SELECT
s.id,
s.name,
EXISTS (
SELECT 1
FROM space_member sm
WHERE sm.space_id = s.id
AND sm.user_id = ?
) AS is_joined
FROM space s
WHERE s.invite_code = ?
LIMIT 1;
`,
args: [userStore.userInfo.id, code],
});
if (!res.isSuccess) return;
const row = res.data?.[0];
if (!row) {
toast.warning("邀请码无效");
return;
}
if (row.is_joined) {
toast.warning("你已经在该空间了");
return;
}
const autoRes = await getUniqueIdApi(1);
if (!autoRes.isSuccess) {
return;
}
// 设置是否是默认空间
let isDefault = false;
const defaultRes = await loadDataApi({
view_name: "v_user_spaces",
search_condition: `v_user_id = ?`,
search_columns: ["v_is_default"],
order_by: "",
args: [userStore.userInfo.id],
});
if (!defaultRes.isSuccess) {
return;
}
if (defaultRes.data.length == 0) {
isDefault = true;
}
// 加入
const requestData = [];
requestData.push({
table_name: "space_member",
key_field: "id",
inserts: [
{
id: autoRes.data[0],
space_id: row.id,
user_id: userStore.userInfo.id,
role: 1,
is_default: isDefault ? 1 : 0,
sort_number: defaultRes.data.length,
member_name: userStore.userInfo.nickname,
},
],
updates: [],
deletes: [],
});
const saveRes = await saveDataApi(requestData);
if (saveRes.isSuccess) {
toast.success("加入成功");
setShowJoin(false);
setJoinForm({
invite_code: "",
});
listUserSpaces();
}
} catch (e) {
console.error(e);
toast.error("加入失败,请稍后重试");
} finally {
setJoinLoading(false);
}
}
const renderItem = ({ item, index }: { item: any; index: number }) => (
<PressableFeedback
key={item.space_id}
onPress={() => {
router.navigate({
pathname: "/tab-space" as any,
params: {
spaceId: item.space_id,
},
});
}}
style={{
height: ITEM_HEIGHT,
}}
className={`rounded-xl flex-row items-center px-4 py-3 bg-white ${
index === spaces.length - 1 ? "mb-4" : ""
}`}
>
<View className="w-full flex-row items-center justify-between">
<View>
<View className="flex-row items-center gap-2">
<Text className="font-semibold text-gray-900">{item.name}</Text>
{item.owner_id === userStore.userInfo.id && (
<Chip size="sm" variant="soft" color="success">
创建者
</Chip>
)}
{item.v_is_default === 1 && (
<Chip size="sm" variant="secondary" color="default">
默认
</Chip>
)}
{item.space_id === spaceStore?.currentSpace?.space_id && (
<Chip size="sm" variant="primary" color="accent">
当前空间
</Chip>
)}
</View>
<View className="flex-row items-center gap-3 mt-3">
<View className="flex-row gap-1 items-center">
<UserRoundIcon size={12} />
<Text className="text-xs">{item.v_member_name}</Text>
</View>
<View className="flex-row gap-1 items-center">
<UsersRoundIcon size={12} />
<Text className={`text-xs `}>{item.v_member_count}</Text>
</View>
</View>
</View>
<Sortable.Handle>
<GripIcon size={20} />
</Sortable.Handle>
</View>
<PressableFeedback.Ripple />
</PressableFeedback>
);
return ( return (
<LayoutView> <LayoutView>
<LayoutView.ScrollView> <LayoutView.ScrollView>
<Text className="text-base text-black">我的空间</Text> <View className="flex-row gap-4">
<Button
className="flex-1"
onPress={() => {
setShowCreate(true);
createInputRef.current?.focus();
}}
>
<PlusIcon color="#fff" size={20} />
<Button.Label>创建新空间</Button.Label>
</Button>
<Button
className="flex-1"
variant="outline"
onPress={() => {
setShowJoin(true);
}}
>
<HousePlusIcon size={20} />
<Button.Label>加入其它空间</Button.Label>
</Button>
</View>
<View className="flex-row my-3 items-center gap-3">
<Separator className="flex-1" />
<Text className="text-xs text-gray-400">我的空间</Text>
<Separator className="flex-1" />
</View>
{loading ? (
<View className="flex-1 items-center justify-center">
<Spinner size="lg" />
</View>
) : spaces.length === 0 ? (
<EmptyState />
) : (
<Sortable.Grid
keyExtractor={(item: any) => {
return String(item.space_id);
}}
activeItemScale={1}
columns={1}
data={spaces}
overDrag="vertical"
renderItem={renderItem}
rowGap={10}
highlight-next-line
customHandle
autoScrollActivationOffset={200}
onDragEnd={(dragData: any) => {
const list = dragData.data;
const updates = list.map((item: any, index: number) => ({
id: item.v_space_member_id,
sort_number: index,
}));
saveDataApi([
{
table_name: "space_member",
key_field: "id",
inserts: [],
updates,
deletes: [],
},
]).then((_res) => {
// console.log(res);
});
}}
/>
)}
</LayoutView.ScrollView> </LayoutView.ScrollView>
<BottomSheet
isOpen={showCreate}
onOpenChange={(val) => {
if (!val) {
setCreateForm({
name: "",
});
setCreateLoading(false);
}
setShowCreate(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<View>
<TextField>
<Label>空间名称</Label>
<Input
ref={createInputRef}
placeholder="请输入空间名称"
onFocus={onFocus}
onBlur={onBlur}
value={createForm.name}
onChangeText={(val) =>
setCreateForm({
name: val,
})
}
/>
</TextField>
<Button className="mt-3" onPress={handleCreate}>
{createLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
<Button.Label>创建</Button.Label>
</Button>
</View>
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
<BottomSheet
isOpen={showJoin}
onOpenChange={(val) => {
if (!val) {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
}
setShowJoin(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<View>
<TextField>
<Label>邀请码</Label>
<InputOTP
ref={joinInputRef}
maxLength={6}
onFocus={onFocus}
onBlur={onBlur}
value={joinForm.invite_code}
onChange={(val) => {
setJoinForm({
invite_code: val,
});
}}
>
<InputOTP.Group>
<InputOTP.Slot index={0} />
<InputOTP.Slot index={1} />
<InputOTP.Slot index={2} />
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
<InputOTP.Slot index={3} />
<InputOTP.Slot index={4} />
<InputOTP.Slot index={5} />
</InputOTP.Group>
</InputOTP>
</TextField>
<Button className="mt-3" onPress={handleJoin}>
{joinLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
<Button.Label>加入</Button.Label>
</Button>
</View>
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
</LayoutView> </LayoutView>
); );
} }
+2 -2
View File
@@ -18,11 +18,11 @@ export default function NotFound() {
className="mt-4" className="mt-4"
onPress={() => { onPress={() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
router.replace("/home"); router.back();
}); });
}} }}
> >
返回首页 返回
</Button> </Button>
} }
/> />
+33 -19
View File
@@ -8,10 +8,15 @@ import "@/global.css";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { Toaster } from "sonner-native"; import { Toaster } from "sonner-native";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { useUserStore } from "@/store";
import { useRouter } from "expo-router";
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();
export default function RootLayout() { export default function RootLayout() {
const router = useRouter();
const userStore = useUserStore();
const isDark = useIsDark(); const isDark = useIsDark();
const isDev = __DEV__; const isDev = __DEV__;
@@ -29,31 +34,40 @@ export default function RootLayout() {
init(); init();
}, []); }, []);
useEffect(() => {
if (userStore.authExpired) {
requestAnimationFrame(() => {
router.replace("/auth/login");
});
}
}, [userStore.authExpired]);
if (!isReady) return null; if (!isReady) return null;
return ( return (
<GestureHandlerRootView className="flex-1"> <GestureHandlerRootView className="flex-1">
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}> <KeyboardProvider>
<HeroUINativeProvider <ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
config={{ <HeroUINativeProvider
devInfo: { config={{
stylingPrinciples: false, devInfo: {
}, stylingPrinciples: false,
}}
>
<Stack
screenOptions={{
animation: "slide_from_right",
contentStyle: {
backgroundColor: "#fff",
}, },
headerShown: false,
}} }}
/> >
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />} <Stack
</HeroUINativeProvider> screenOptions={{
</ThemeProvider> animation: "slide_from_right",
contentStyle: {
backgroundColor: "#fff",
},
headerShown: false,
}}
/>
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
</HeroUINativeProvider>
</ThemeProvider>
</KeyboardProvider>
<Toaster duration={1000} /> <Toaster duration={1000} />
</GestureHandlerRootView> </GestureHandlerRootView>
); );
+3 -1
View File
@@ -33,12 +33,14 @@ export default function Login() {
setTimeout(() => { setTimeout(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
userStore.setUserInfo({ userStore.setUserInfo({
id: 1, id: 779895419121733,
token: "123", token: "123",
nickname: "Zhang", nickname: "Zhang",
avatar: "", avatar: "",
}); });
userStore.setAuthExpired(false);
router.replace("/(tabs)/home"); router.replace("/(tabs)/home");
}); });
@@ -0,0 +1,43 @@
import React from "react";
import LayoutView from "@/components/layout/view";
import { View, Text } from "react-native";
import Navbar from "@/components/layout/navbar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { EarthIcon } from "lucide-react-native";
import { useRouter } from "expo-router";
export default function Space() {
const insets = useSafeAreaInsets();
const router = useRouter();
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1, zIndex: 0 }}>
<LayoutView>
<LayoutView.ScrollView>
<Text className="text-base text-black">Space</Text>
</LayoutView.ScrollView>
</LayoutView>
</View>
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
zIndex: 10,
}}
>
<View style={{ paddingTop: insets.top }}>
<Navbar
title={"空间"}
titleIcon={EarthIcon}
onBack={() => {
router.back();
}}
/>
</View>
</View>
</View>
);
}
+18 -1
View File
@@ -9,6 +9,12 @@ export function loginWxApi(params: LoginWxParams) {
return alovaInstance.Post<ResponseData<LoginResult>>(`/auth/login/wechat`, params); return alovaInstance.Post<ResponseData<LoginResult>>(`/auth/login/wechat`, params);
} }
export function getUniqueIdApi(count = 1) {
return alovaInstance.Post<ResponseData<number[]>>(`/data/getUniqueId`, {
count: count,
});
}
interface LoadDataBySqlParams { interface LoadDataBySqlParams {
sql: string; sql: string;
args: any[]; args: any[];
@@ -23,10 +29,21 @@ interface LoadDataParams {
search_condition: string; search_condition: string;
order_by: string; order_by: string;
search_columns: string[]; search_columns: string[];
args: string[]; args: any[];
} }
export function loadDataApi(params: LoadDataParams) { export function loadDataApi(params: LoadDataParams) {
return alovaInstance.Post<ResponseData<any[]>>(`/data/loadData`, params); return alovaInstance.Post<ResponseData<any[]>>(`/data/loadData`, params);
} }
interface SaveDataParams {
table_name: string;
key_field: string;
inserts: any[];
updates: any[];
deletes: any[];
}
export function saveDataApi(params: SaveDataParams[]) {
return alovaInstance.Post<ResponseData<null>>(`/data/saveData`, params);
}
+1
View File
@@ -1,2 +1,3 @@
export * from "./app"; export * from "./app";
export * from "./user"; export * from "./user";
export * from "./space";
+32
View File
@@ -0,0 +1,32 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
interface Space {
space_id: number;
}
interface SpaceState {
currentSpace: Space | null;
setCurrentSpace: (space: Space | null) => void;
clear: () => void;
inSpace: () => boolean;
}
export const useSpaceStore = create<SpaceState>()(
persist(
(set, get) => ({
currentSpace: null,
setCurrentSpace: (space) => set({ currentSpace: space }),
clear: () => set({ currentSpace: null }),
inSpace: () => {
const currentSpace = get().currentSpace;
return currentSpace !== null && currentSpace.space_id !== 0;
},
}),
{
name: "space-storage",
storage: createJSONStorage(() => AsyncStorage),
},
),
);
+13
View File
@@ -0,0 +1,13 @@
export function generateCode(
length: number = 6,
chars: string = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789",
): string {
let result = "";
const max = chars.length;
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * max));
}
return result;
}
+143
View File
@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>Tree</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI";
background: #f5f7fa;
padding: 30px;
}
.tree {
width: 320px;
background: #fff;
padding: 10px;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
}
.node {
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: 0.2s;
}
.node:hover {
background: #f0f7ff;
color: #1677ff;
}
.node.active {
background: #e6f4ff;
color: #1677ff;
}
.arrow {
width: 14px;
font-size: 12px;
cursor: pointer;
transition: 0.2s;
}
.arrow.open {
transform: rotate(90deg);
}
.children {
margin-left: 18px;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: all 0.25s ease;
}
.children.open {
max-height: 500px;
opacity: 1;
}
</style>
</head>
<body>
<div class="tree">
<!-- 根 -->
<div class="tree-node">
<!-- ❗关键:整个 node 都可以选中 -->
<div class="node" onclick="selectNode(this)">
<span class="arrow" onclick="toggle(event, this)">▶</span>
系统管理
</div>
<div class="children open">
<div class="node" onclick="selectNode(this)">
用户管理
</div>
<div class="node" onclick="selectNode(this)">
角色管理
</div>
<!-- 父节点 -->
<div class="tree-node">
<div class="node" onclick="selectNode(this)">
<span class="arrow" onclick="toggle(event, this)">▶</span>
权限管理
</div>
<div class="children">
<div class="node" onclick="selectNode(this)">
菜单权限
</div>
<div class="node" onclick="selectNode(this)">
按钮权限
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// ✅ 单选:保证父子都能选
function selectNode(el) {
document.querySelectorAll(".node.active")
.forEach(n => n.classList.remove("active"));
el.classList.add("active");
}
// ✅ 只控制展开(不影响选中)
function toggle(e, el) {
e.stopPropagation(); // ❗关键:阻止触发选中
const wrapper = el.closest(".tree-node");
const children = wrapper.querySelector(".children");
if (!children) return;
children.classList.toggle("open");
el.classList.toggle("open");
}
</script>
</body>
</html>
+384
View File
@@ -0,0 +1,384 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>Dialog Final</title>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
.mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.35);
z-index: 999;
display: none;
}
.win-dialog {
position: fixed;
background: #fff;
box-shadow: 0 12px 40px rgba(0, 0, 0, .25);
display: none;
overflow: hidden;
z-index: 1000;
}
.win-header {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
cursor: move;
background: #fafafa;
}
.win-title {
padding-left: 10px;
}
.win-btn {
display: flex;
height: 100%;
}
.btn-item {
padding: 0 10px;
display: flex;
align-items: center;
cursor: pointer;
}
.btn-item:hover {
background: #ddd;
}
.win-body {
height: calc(100% - 40px);
padding: 10px;
}
.resizer {
position: absolute;
}
.br {
right: 0;
bottom: 0;
width: 12px;
height: 12px;
cursor: nwse-resize;
}
.r {
right: 0;
top: 0;
width: 5px;
height: 100%;
cursor: ew-resize;
}
.b {
bottom: 0;
left: 0;
width: 100%;
height: 5px;
cursor: ns-resize;
}
.l {
left: 0;
top: 0;
width: 5px;
height: 100%;
cursor: ew-resize;
}
.t {
top: 0;
left: 0;
width: 100%;
height: 5px;
cursor: ns-resize;
}
</style>
</head>
<body>
<template id="file-template">
<div class="layout">
<div class="left">分类树</div>
<div class="middle">列表</div>
<div class="right">预览</div>
</div>
</template>
<button onclick="handleShowDialog()">
打开窗口
</button>
<script>
function handleShowDialog() {
Dialog.open({
title: 'Test Window',
template: '#file-template',
width: '80%',
height: '80%'
})
}
// Dialog
const Dialog = (() => {
let $mask = $('<div class="mask"></div>').appendTo('body');
let $el = null;
let isMax = false;
let old = {};
let opt = null;
// ====== 工具函数 ======
function parseSize(val, max) {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
if (val.includes('%')) return max * parseFloat(val) / 100;
if (val.includes('vw')) return window.innerWidth * parseFloat(val) / 100;
if (val.includes('vh')) return window.innerHeight * parseFloat(val) / 100;
return parseFloat(val);
}
return max;
}
// ====== 核心方法 ======
function applySize() {
const maxW = window.innerWidth;
const maxH = window.innerHeight;
let w = parseSize(opt.width || 600, maxW);
let h = parseSize(opt.height || 400, maxH);
w = Math.min(w, maxW);
h = Math.min(h, maxH);
$el.css({
width: w,
height: h,
left: (maxW - w) / 2,
top: (maxH - h) / 2
});
}
function bindResize() {
$(window).on('resize.dialog', () => {
if (isMax) {
$el.css({
width: window.innerWidth,
height: window.innerHeight
});
return;
}
const w = opt.width;
const h = opt.height;
if (
(typeof w === 'string' && (w.includes('%') || w.includes('vw'))) ||
(typeof h === 'string' && (h.includes('%') || h.includes('vh')))
) {
applySize();
}
});
}
function bindEvents() {
const self = {
get el() { return $el; }
};
$el.find('.close').on('click', close);
// drag
let move = false, ox = 0, oy = 0;
$el.find('.win-header').on('mousedown', function (e) {
if (isMax) return;
move = true;
const pos = $el.position();
ox = e.clientX - pos.left;
oy = e.clientY - pos.top;
$(document).on('mousemove.drag', function (e) {
if (!move) return;
$el.css({
left: e.clientX - ox,
top: e.clientY - oy
});
});
$(document).on('mouseup.drag', function () {
move = false;
$(document).off('.drag');
});
});
// resize
const MIN_W = 300;
const MIN_H = 200;
$el.find('.resizer').on('mousedown', function (e) {
if (isMax) return;
let type = $(this).attr('class').split(' ')[1];
const startX = e.clientX;
const startY = e.clientY;
const startW = $el.width();
const startH = $el.height();
const startL = parseFloat($el.css('left'));
const startT = parseFloat($el.css('top'));
const MAX_W = window.innerWidth;
const MAX_H = window.innerHeight;
function onMove(e) {
if (e.buttons !== 1) return;
let dx = e.clientX - startX;
let dy = e.clientY - startY;
let newW = startW;
let newH = startH;
let newL = startL;
let newT = startT;
if (type === 'br') { newW += dx; newH += dy; }
if (type === 'r') newW += dx;
if (type === 'b') newH += dy;
if (type === 'l') { newW -= dx; newL += dx; }
if (type === 't') { newH -= dy; newT += dy; }
if (newW < MIN_W) newW = MIN_W;
if (newH < MIN_H) newH = MIN_H;
if (newW > MAX_W) newW = MAX_W;
if (newH > MAX_H) newH = MAX_H;
$el.css({
width: newW,
height: newH,
left: newL,
top: newT
});
}
function onUp() {
$(document).off('.resize');
}
$(document)
.on('mousemove.resize', onMove)
.on('mouseup.resize', onUp);
});
// max
$el.find('.max').on('click', function () {
if (!isMax) {
old = {
width: $el.width(),
height: $el.height(),
left: $el.position().left,
top: $el.position().top
};
$el.css({
left: 0,
top: 0,
width: window.innerWidth,
height: window.innerHeight
});
isMax = true;
} else {
$el.css(old);
isMax = false;
}
});
}
function destroy() {
if ($el) {
$el.remove();
$el = null;
}
isMax = false;
$mask.off().hide();
$(window).off('resize.dialog');
}
// ====== API ======
function open(o) {
destroy();
opt = o;
$el = $(`
<div class="win-dialog">
<div class="win-header">
<div class="win-title"></div>
<div class="win-btn">
<div class="btn-item max">MAX</div>
<div class="btn-item close">X</div>
</div>
</div>
<div class="win-body"></div>
</div>
`).appendTo('body');
$el.find('.win-title').text(opt.title || 'Dialog');
const $body = $el.find('.win-body');
if (opt.template) {
const tpl = document.querySelector(opt.template);
const node = tpl.content.cloneNode(true);
$body.empty().append(node);
}
else if (opt.content instanceof HTMLElement) {
$body.empty().append(opt.content);
}
else {
$body.html(opt.content || '');
}
applySize();
$mask.fadeIn(120);
$el.fadeIn(180);
bindEvents();
bindResize();
}
function close() {
$el?.fadeOut(120);
$mask.fadeOut(120);
setTimeout(() => destroy(), 120);
}
// ====== 单例导出 ======
return {
open,
close
};
})();
</script>
</body>
</html>