This commit is contained in:
oneao committed 2026-04-19 21:30:51 +08:00
1 parent b6415c0d65
commit 89aaef1d6b
29 files changed
+1118 -277

No files matched your search

@@ -5,9 +5,15 @@ postgres:
port: 5432 port: 5432
user: postgres user: postgres
password: zhang520.. password: zhang520..
dbname: app dbname: allapp
timezone: Asia/Shanghai timezone: Asia/Shanghai
max_open_conns: 10 max_open_conns: 10
max_idle_conns: 5 max_idle_conns: 5
conn_max_lifetime: 30m conn_max_lifetime: 30m
sll_mode: false sll_mode: false
aws_s3:
region: us-east-1
access_key: 2c0sZEpPQyCVhoLGNzHS
secret_key: fRbcQPEhU8B0C3On4qIoJ19w5vXxTFD26LlAiae7
endpoint: http://118.89.70.199:9000
bucket: test
+18
View File
@@ -15,6 +15,24 @@ require (
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.15 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
+36
View File
@@ -1,5 +1,41 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.15 h1:i7rHbaySnBXGvCkDndaBU8f3EAlRVgViwNfkwFUrXgE=
github.com/aws/aws-sdk-go-v2/config v1.32.15/go.mod h1:yLJzL0IkI9+4BwjPSOueyHzppJj3t0dhK5tbmmcFk5Q=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -5,6 +5,8 @@ import (
"allapp-go/internal/config" "allapp-go/internal/config"
"allapp-go/pkg/db" "allapp-go/pkg/db"
"allapp-go/pkg/logger" "allapp-go/pkg/logger"
"allapp-go/pkg/s3store"
"allapp-go/pkg/wechat"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
@@ -27,6 +29,9 @@ func NewApp() (*App, error) {
// 日志 // 日志
boots.InitLogger(cfg) boots.InitLogger(cfg)
// S3
s3store.NewS3Client(cfg)
// 数据库 // 数据库
pgsql, err := boots.InitPgsql(cfg) pgsql, err := boots.InitPgsql(cfg)
if err != nil { if err != nil {
@@ -38,6 +43,7 @@ func NewApp() (*App, error) {
// 其他组件 // 其他组件
boots.InitUniqueId(cfg) boots.InitUniqueId(cfg)
boots.InitJwt(cfg) boots.InitJwt(cfg)
wechat.Init(cfg.Wechat.AppId, cfg.Wechat.AppSecret)
return &App{ return &App{
Config: cfg, Config: cfg,
@@ -8,6 +8,7 @@ type Config struct {
UniqueID UniqueIDConfig `mapstructure:"unique_id"` UniqueID UniqueIDConfig `mapstructure:"unique_id"`
Postgres PostgresConfig `mapstructure:"postgres"` Postgres PostgresConfig `mapstructure:"postgres"`
Wechat WechatConfig `mapstructure:"wechat"` Wechat WechatConfig `mapstructure:"wechat"`
AwsS3 AwsS3Config `mapstructure:"aws_s3"`
} }
type AppConfig struct { type AppConfig struct {
@@ -43,3 +44,11 @@ type WechatConfig struct {
AppId string `mapstructure:"app_id"` AppId string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"` AppSecret string `mapstructure:"app_secret"`
} }
type AwsS3Config struct {
Region string `mapstructure:"region"`
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Bucket string `mapstructure:"bucket"`
Endpoint string `mapstructure:"endpoint"`
}
@@ -94,8 +94,8 @@ func handleThirdLogin(
} }
// ========= token ========= // ========= token =========
token, err := jwtx.CreateToken(c.Context(), map[string]any{ token, err := jwtx.CreateToken(c.Context(), jwtx.TokenData{
"id": id, UserID: id,
}) })
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@@ -129,25 +129,24 @@ func registerUser(
err := dbClient.WithTx(c.Context(), func(tx *db.Client) error { err := dbClient.WithTx(c.Context(), func(tx *db.Client) error {
if err := tx.Insert(c.Context(), "user", "id", map[string]any{ if err := tx.Insert(c.Context(), "b_user", "id", map[string]any{
"id": userId, "id": userId,
"nickname": nickname, "nickname": nickname,
"avatar": avatar, "avatar": avatar,
"status": 1, "created_at": "NOW()",
"create_time": "NOW()", "updated_at": "NOW()",
"update_time": "NOW()",
"last_login_time": "NOW()", "last_login_time": "NOW()",
}); err != nil { }); err != nil {
return err return err
} }
if err := tx.Insert(c.Context(), "user_oauth", "id", map[string]any{ if err := tx.Insert(c.Context(), "b_user_oauth", "id", map[string]any{
"id": uniqueid.NextId(), "id": uniqueid.NextId(),
"user_id": userId, "user_id": userId,
"type": loginType, "type": loginType,
"openid": openid, "openid": openid,
"create_time": "NOW()", "created_at": "NOW()",
"update_time": "NOW()", "updated_at": "NOW()",
}); err != nil { }); err != nil {
return err return err
} }
@@ -159,8 +158,8 @@ func registerUser(
return errors.WithStack(err) return errors.WithStack(err)
} }
token, err := jwtx.CreateToken(c.Context(), map[string]any{ token, err := jwtx.CreateToken(c.Context(), jwtx.TokenData{
"id": userId, UserID: userId,
}) })
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@@ -179,7 +178,10 @@ func getUserByOpenID(dbClient *db.Client, c fiber.Ctx, openid string, loginType
users, err := dbClient.LoadDataBySQL( users, err := dbClient.LoadDataBySQL(
c.Context(), c.Context(),
"SELECT u.* FROM user_oauth o JOIN user u ON u.id = o.user_id WHERE o.openid = ? AND o.type = ?", `SELECT u.*
FROM b_user_oauth o
JOIN b_user u ON u.id = o.user_id
WHERE o.openid = $1 AND o.type = $2`,
[]any{openid, loginType}, []any{openid, loginType},
) )
@@ -196,6 +198,19 @@ func getUserByOpenID(dbClient *db.Client, c fiber.Ctx, openid string, loginType
// ======================== 类型工具(极简版) ======================== // ======================== 类型工具(极简版) ========================
func toInt64(v any) int64 { func toInt64(v any) int64 {
i, _ := v.(int64) switch val := v.(type) {
return i case int64:
return val
case int32:
return int64(val)
case int16:
return int64(val)
case int:
return int64(val)
case float64:
return int64(val)
default:
fmt.Printf("unknown type: %T, value=%v\n", v, v)
return 0
}
} }
@@ -0,0 +1,79 @@
package handle
import (
"allapp-go/internal/errors"
"allapp-go/internal/httpx"
"allapp-go/pkg/s3store"
"fmt"
"mime/multipart"
"path/filepath"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/gofiber/fiber/v3"
"github.com/google/uuid"
)
func S3ListBuckets(c fiber.Ctx) error {
result, err := s3store.S3Client.ListBuckets(c.Context(), &s3.ListBucketsInput{})
if err != nil {
return errors.WithStack(err)
}
buckets := make([]string, 0, len(result.Buckets))
for _, b := range result.Buckets {
if b.Name != nil {
buckets = append(buckets, *b.Name)
}
}
return httpx.OK(c, buckets)
}
func S3UploadFile(c fiber.Ctx) error {
fileHeader, err := c.FormFile("file")
if err != nil {
return errors.WithStack(err)
}
file, err := fileHeader.Open()
if err != nil {
return errors.WithStack(err)
}
defer func(file multipart.File) {
_ = file.Close()
}(file)
// 日期目录
now := time.Now()
folderPath := fmt.Sprintf("%d/%02d/%02d",
now.Year(),
now.Month(),
now.Day(),
)
// UUID 文件名(保留后缀)
ext := filepath.Ext(fileHeader.Filename)
newName := fmt.Sprintf("%s%s", uuid.NewString(), ext)
// S3 object key
key := fmt.Sprintf("%s/%s", folderPath, newName)
// ContentType(防止前端乱传)
contentType := fileHeader.Header.Get("Content-Type")
// 调用存储层
err = s3store.UploadToRustFS(
c.Context(),
key,
file,
fileHeader.Size,
contentType,
)
if err != nil {
return errors.WithStack(err)
}
return httpx.OK(c, nil)
}
@@ -1,7 +1,10 @@
package middleware package middleware
import ( import (
"allapp-go/internal/httpx"
"allapp-go/pkg/jwtx"
"context" "context"
"strings"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -11,20 +14,18 @@ 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 == "" {
// return httpx.Unauthorized(c, "未登录")
//}
//
//verifyToken, ok := jwtx.VerifyToken(c.Context(), token)
//if !ok {
// return httpx.Unauthorized(c, "登录已过期")
//}
//
//c.Locals(CtxUserIDKey, verifyToken.Data["user_id"])
ctx := context.WithValue(c.Context(), CtxUserIDKey, int64(779895419121733)) if token == "" {
return httpx.Unauthorized(c, "未登录")
}
verifyToken, ok := jwtx.VerifyToken(c.Context(), token)
if !ok {
return httpx.Unauthorized(c, "登录已过期")
}
ctx := context.WithValue(c.Context(), CtxUserIDKey, verifyToken.Data.UserID)
c.SetContext(ctx) c.SetContext(ctx)
return c.Next() return c.Next()
@@ -19,6 +19,10 @@ func SetupRouter(app *fiber.App, cfg *config.Config) {
data.Post("/saveData", handle.SaveData) data.Post("/saveData", handle.SaveData)
data.Post("/getUniqueId", handle.GetUniqueId) data.Post("/getUniqueId", handle.GetUniqueId)
s3 := api.Group("/s3", middleware.Auth())
s3.Post("/listBuckets", handle.S3ListBuckets)
s3.Post("/upload", handle.S3UploadFile)
// ==================== auth ==================== // ==================== 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)
+2 -2
View File
@@ -7,8 +7,8 @@ import (
) )
var auditExcludeTables = map[string]bool{ var auditExcludeTables = map[string]bool{
"user": true, "b_user": true,
"user_oauth": true, "b_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) {
+23 -3
View File
@@ -16,11 +16,21 @@ func (c *Client) Delete(
return fmt.Errorf("keyField 不能为空") return fmt.Errorf("keyField 不能为空")
} }
keyVal, err := validateData(table, keyField, data) rawTable := table
keyVal, err := validateData(rawTable, keyField, data)
if err != nil { if err != nil {
return err return err
} }
tableParts := strings.Split(table, ".")
for i, p := range tableParts {
tableParts[i] = `"` + p + `"`
}
table = strings.Join(tableParts, ".")
keyField = `"` + keyField + `"`
sql := fmt.Sprintf( sql := fmt.Sprintf(
"DELETE FROM %s WHERE %s = $1", "DELETE FROM %s WHERE %s = $1",
table, table,
@@ -50,6 +60,16 @@ func (c *Client) BatchDelete(
return fmt.Errorf("数据不能为空") return fmt.Errorf("数据不能为空")
} }
rawTable := table
tableParts := strings.Split(table, ".")
for i, p := range tableParts {
tableParts[i] = `"` + p + `"`
}
table = strings.Join(tableParts, ".")
keyFieldQuoted := `"` + keyField + `"`
var ( var (
placeholders []string placeholders []string
args []any args []any
@@ -57,7 +77,7 @@ func (c *Client) BatchDelete(
) )
for _, data := range dataList { for _, data := range dataList {
keyVal, err := validateData(table, keyField, data) keyVal, err := validateData(rawTable, keyField, data)
if err != nil { if err != nil {
return err return err
} }
@@ -74,7 +94,7 @@ func (c *Client) BatchDelete(
sql := fmt.Sprintf( sql := fmt.Sprintf(
"DELETE FROM %s WHERE %s IN (%s)", "DELETE FROM %s WHERE %s IN (%s)",
table, table,
keyField, keyFieldQuoted,
strings.Join(placeholders, ", "), strings.Join(placeholders, ", "),
) )
+24 -5
View File
@@ -17,9 +17,18 @@ func buildInsertSQL(table string, data map[string]any) (string, []any) {
args []any args []any
) )
// 处理 schema.table 的情况
tableParts := strings.Split(table, ".")
for i, p := range tableParts {
tableParts[i] = `"` + p + `"`
}
table = strings.Join(tableParts, ".")
i := 1 i := 1
for col, val := range data { for col, val := range data {
columns = append(columns, col) // 列名加引号
columns = append(columns, `"`+col+`"`)
placeholders = append(placeholders, fmt.Sprintf("$%d", i)) placeholders = append(placeholders, fmt.Sprintf("$%d", i))
args = append(args, val) args = append(args, val)
i++ i++
@@ -80,12 +89,18 @@ func (c *Client) BatchInsert(
return fmt.Errorf("数据不能为空") return fmt.Errorf("数据不能为空")
} }
// 用第一条确定列顺序
first := dataList[0] first := dataList[0]
if _, err := validateData(table, keyField, first); err != nil { if _, err := validateData(table, keyField, first); err != nil {
return err return err
} }
// 👉 表名加引号(支持 schema)
tableParts := strings.Split(table, ".")
for i, p := range tableParts {
tableParts[i] = `"` + p + `"`
}
table = strings.Join(tableParts, ".")
var columns []string var columns []string
for col := range first { for col := range first {
columns = append(columns, col) columns = append(columns, col)
@@ -93,6 +108,12 @@ func (c *Client) BatchInsert(
sort.Strings(columns) sort.Strings(columns)
// 👉 列名加引号
var quotedColumns []string
for _, col := range columns {
quotedColumns = append(quotedColumns, `"`+col+`"`)
}
var ( var (
valueStrings []string valueStrings []string
args []any args []any
@@ -100,12 +121,10 @@ func (c *Client) BatchInsert(
) )
for _, data := range dataList { for _, data := range dataList {
// 统一校验
if _, err := validateData(table, keyField, data); err != nil { if _, err := validateData(table, keyField, data); err != nil {
return err return err
} }
// 字段数量检查
if len(data) != len(columns) { if len(data) != len(columns) {
return fmt.Errorf("批量插入失败:数据字段不一致") return fmt.Errorf("批量插入失败:数据字段不一致")
} }
@@ -132,7 +151,7 @@ func (c *Client) BatchInsert(
sql := fmt.Sprintf( sql := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES %s", "INSERT INTO %s (%s) VALUES %s",
table, table,
strings.Join(columns, ", "), strings.Join(quotedColumns, ", "),
strings.Join(valueStrings, ", "), strings.Join(valueStrings, ", "),
) )
+29 -11
View File
@@ -29,12 +29,21 @@ func (c *Client) Update(
data["update_time"] = now data["update_time"] = now
userID, flag := middleware.GetUserID(ctx) userID, flag := middleware.GetUserID(ctx)
if flag { if flag {
data["update_by"] = userID data["update_by"] = userID
} }
} }
// 👉 表名加引号(支持 schema)
tableParts := strings.Split(table, ".")
for i, p := range tableParts {
tableParts[i] = `"` + p + `"`
}
table = strings.Join(tableParts, ".")
// 👉 keyField 加引号
keyField = `"` + keyField + `"`
var ( var (
setClauses []string setClauses []string
args []any args []any
@@ -45,12 +54,15 @@ func (c *Client) Update(
if col == keyField { if col == keyField {
continue continue
} }
setClauses = append(setClauses, fmt.Sprintf("%s=$%d", col, i))
// 👉 列名加引号
colQuoted := `"` + col + `"`
setClauses = append(setClauses, fmt.Sprintf("%s=$%d", colQuoted, i))
args = append(args, val) args = append(args, val)
i++ i++
} }
// WHERE 条件
where := fmt.Sprintf("%s=$%d", keyField, i) where := fmt.Sprintf("%s=$%d", keyField, i)
args = append(args, keyVal) args = append(args, keyVal)
@@ -76,13 +88,21 @@ func (c *Client) BatchUpdate(
return fmt.Errorf("数据不能为空") return fmt.Errorf("数据不能为空")
} }
// 用第一条数据确定字段
first := dataList[0] first := dataList[0]
if _, err := validateData(table, keyField, first); err != nil { if _, err := validateData(table, keyField, first); err != nil {
return err return err
} }
// 提取字段(排除主键)+ 排序(关键) // 👉 表名加引号(支持 schema)
tableParts := strings.Split(table, ".")
for i, p := range tableParts {
tableParts[i] = `"` + p + `"`
}
table = strings.Join(tableParts, ".")
// 👉 keyField 加引号
keyFieldQuoted := `"` + keyField + `"`
var columns []string var columns []string
for col := range first { for col := range first {
if col != keyField { if col != keyField {
@@ -96,16 +116,15 @@ func (c *Client) BatchUpdate(
argIndex = 1 argIndex = 1
) )
// CASE 语句
var setClauses []string var setClauses []string
for _, col := range columns { for _, col := range columns {
var caseBuilder strings.Builder colQuoted := `"` + col + `"`
caseBuilder.WriteString(fmt.Sprintf("%s = CASE %s ", col, keyField)) var caseBuilder strings.Builder
caseBuilder.WriteString(fmt.Sprintf("%s = CASE %s ", colQuoted, keyFieldQuoted))
for _, data := range dataList { for _, data := range dataList {
// 校验 key
keyVal, err := validateData(table, keyField, data) keyVal, err := validateData(table, keyField, data)
if err != nil { if err != nil {
return err return err
@@ -130,7 +149,6 @@ func (c *Client) BatchUpdate(
setClauses = append(setClauses, caseBuilder.String()) setClauses = append(setClauses, caseBuilder.String())
} }
// WHERE IN
var wherePlaceholders []string var wherePlaceholders []string
for _, data := range dataList { for _, data := range dataList {
@@ -148,7 +166,7 @@ func (c *Client) BatchUpdate(
"UPDATE %s SET %s WHERE %s IN (%s)", "UPDATE %s SET %s WHERE %s IN (%s)",
table, table,
strings.Join(setClauses, ", "), strings.Join(setClauses, ", "),
keyField, keyFieldQuoted,
strings.Join(wherePlaceholders, ", "), strings.Join(wherePlaceholders, ", "),
) )
+7 -5
View File
@@ -48,8 +48,12 @@ func getManager() *jwtManager {
// Claims(通用结构) // Claims(通用结构)
// ================== // ==================
type TokenData struct {
UserID int64 `json:"user_id"`
}
type CustomClaims struct { type CustomClaims struct {
Data map[string]any `json:"data"` Data TokenData `json:"data"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@@ -58,7 +62,7 @@ type CustomClaims struct {
// ================== // ==================
// CreateToken 生成 JWT // CreateToken 生成 JWT
func CreateToken(ctx context.Context, data map[string]any) (string, error) { func CreateToken(ctx context.Context, data TokenData) (string, error) {
m := getManager() m := getManager()
return m.createToken(data) return m.createToken(data)
} }
@@ -91,7 +95,7 @@ func VerifyToken(ctx context.Context, tokenString string) (*CustomClaims, bool)
// 内部实现 // 内部实现
// ================== // ==================
func (j *jwtManager) createToken(data map[string]any) (string, error) { func (j *jwtManager) createToken(data TokenData) (string, error) {
now := time.Now() now := time.Now()
claims := &CustomClaims{ claims := &CustomClaims{
@@ -101,8 +105,6 @@ func (j *jwtManager) createToken(data map[string]any) (string, error) {
}, },
} }
// ✅ 只有 >0 才设置过期时间
// -1 / 0 都表示不过期
if j.tokenExpiry > 0 { if j.tokenExpiry > 0 {
claims.ExpiresAt = jwt.NewNumericDate(now.Add(j.tokenExpiry)) claims.ExpiresAt = jwt.NewNumericDate(now.Add(j.tokenExpiry))
} }
@@ -0,0 +1,52 @@
package s3store
import (
"allapp-go/internal/config"
"context"
"fmt"
"mime/multipart"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
var S3Client *s3.Client
var presignClient *s3.PresignClient
var bucket string
func NewS3Client(cfg *config.Config) {
credProvider := credentials.NewStaticCredentialsProvider(
cfg.AwsS3.AccessKey,
cfg.AwsS3.SecretKey,
"",
)
awsCfg := aws.Config{
Region: cfg.AwsS3.Region,
Credentials: credProvider,
}
S3Client = s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(cfg.AwsS3.Endpoint)
o.UsePathStyle = true
})
presignClient = s3.NewPresignClient(S3Client)
bucket = cfg.AwsS3.Bucket
fmt.Println("S3 client and Presign client initialized successfully!")
}
func UploadToRustFS(ctx context.Context, key string, file multipart.File, size int64, contentType string) error {
_, err := S3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: file,
ContentLength: aws.Int64(size),
ContentType: aws.String(contentType),
})
return err
}
+25 -6
View File
@@ -6,11 +6,10 @@
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"scheme": [ "scheme": ["oneao", "tencent102826474", "wxdab3e21a1f7e392f"],
"oneao", "extra": {
"tencent102826474", "linkingScheme": "oneao"
"wxdab3e21a1f7e392f" },
],
"ios": { "ios": {
"icon": "./assets/expo.icon" "icon": "./assets/expo.icon"
}, },
@@ -58,7 +57,27 @@
} }
} }
], ],
"expo-wechat" "expo-wechat",
[
"expo-document-picker",
{
"iCloudContainerEnvironment": "Production"
}
],
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let you share them with your friends.",
"colors": {
"cropToolbarColor": "#000000"
},
"dark": {
"colors": {
"cropToolbarColor": "#000000"
}
}
}
]
], ],
"experiments": { "experiments": {
"typedRoutes": true, "typedRoutes": true,
+2
View File
@@ -27,9 +27,11 @@
"expo-build-properties": "~55.0.13", "expo-build-properties": "~55.0.13",
"expo-constants": "~55.0.14", "expo-constants": "~55.0.14",
"expo-device": "~55.0.15", "expo-device": "~55.0.15",
"expo-document-picker": "~55.0.13",
"expo-font": "~55.0.6", "expo-font": "~55.0.6",
"expo-glass-effect": "~55.0.10", "expo-glass-effect": "~55.0.10",
"expo-image": "~55.0.8", "expo-image": "~55.0.8",
"expo-image-picker": "~55.0.18",
"expo-linear-gradient": "~55.0.13", "expo-linear-gradient": "~55.0.13",
"expo-linking": "~55.0.13", "expo-linking": "~55.0.13",
"expo-qq": "^0.1.8", "expo-qq": "^0.1.8",
@@ -1,95 +1,47 @@
const fs = require("node:fs"); // plugins/withAndroidSignature.js
const path = require("node:path");
const { withAppBuildGradle } = require("expo/config-plugins");
const CREDENTIAL_PATH = path.resolve( const fs = require('node:fs')
__dirname, const path = require('node:path')
"../credentials/credentials.json" const { withAppBuildGradle } = require('expo/config-plugins')
);
const KEYSTORE_PATH = path.resolve(
__dirname,
"../credentials/oneao-android.keystore"
);
module.exports = function withAndroidSignature(config) { module.exports = function withAndroidSignature(config) {
return withAppBuildGradle(config, (config) => { return withAppBuildGradle(config, (config) => {
if (config.modResults.language !== "groovy") { if (config.modResults.language === 'groovy') {
throw new Error("only groovy build.gradle is supported"); config.modResults.contents = setAndroidSignature(config.modResults.contents)
} }
else {
throw new Error('如果不是 groovy,则无法在 app/build.gradle 中设置 signingConfigs')
}
return config
})
}
config.modResults.contents = setAndroidSignature(
config.modResults.contents
);
return config;
});
};
// ======================== 主入口 ========================
function setAndroidSignature(appBuildGradle) { function setAndroidSignature(appBuildGradle) {
if (!fs.existsSync(CREDENTIAL_PATH)) { if (!fs.existsSync(path.resolve(__dirname, '../credentials/credentials.json'))) {
console.warn("⚠️ credentials.json 不存在,跳过签名配置"); console.warn('警告:没有设置正式版本的 Android Keystore 文件,因为 credentials.json 不存在。')
return appBuildGradle; return appBuildGradle
} }
const info = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../credentials/credentials.json'), { encoding: 'utf8' }))
const info = JSON.parse( // 使用正则表达式插入签名信息
fs.readFileSync(CREDENTIAL_PATH, "utf8") let output = appBuildGradle.replace(
); /(signingConfigs\s*\{)/,
`$1
let output = appBuildGradle;
// ⭐ 1. 重写 signingConfigs(关键)
output = replaceSigningConfigs(output, info);
// ⭐ 2. 确保所有 debug/release 使用统一 keystore
output = unifySigningUsage(output);
return output;
}
// ======================== ⭐核心:重写 signingConfigs ========================
function replaceSigningConfigs(appBuildGradle, info) {
const block = `
signingConfigs {
release { release {
storeFile file(${JSON.stringify(KEYSTORE_PATH)}) storeFile file(${JSON.stringify(path.resolve(__dirname, '../credentials/oneao-android.keystore'))})
storePassword ${JSON.stringify(info.android.keystore.keystorePassword)} storePassword ${JSON.stringify(info.android.keystore.keystorePassword)}
keyAlias ${JSON.stringify(info.android.keystore.keyAlias)} keyAlias ${JSON.stringify(info.android.keystore.keyAlias)}
keyPassword ${JSON.stringify(info.android.keystore.keyPassword)} keyPassword ${JSON.stringify(info.android.keystore.keyPassword)}
} }`,
)
debug { // 使用正则表达式替换 signingConfig
storeFile file(${JSON.stringify(KEYSTORE_PATH)})
storePassword ${JSON.stringify(info.android.keystore.keystorePassword)}
keyAlias ${JSON.stringify(info.android.keystore.keyAlias)}
keyPassword ${JSON.stringify(info.android.keystore.keyPassword)}
}
}
`;
const regex = /signingConfigs\s*\{[\s\S]*?\n\s*\}/m;
if (regex.test(appBuildGradle)) {
return appBuildGradle.replace(regex, block.trim());
}
// 没有就插入
return appBuildGradle.replace(
/(android\s*\{)/,
`$1\n${block.trim()}`
);
}
// ======================== ⭐统一使用 signingConfig ========================
function unifySigningUsage(appBuildGradle) {
let output = appBuildGradle;
// debug -> release(你如果想 debug 也用 release,可以改这里)
output = output.replace( output = output.replace(
/signingConfig\s+signingConfigs\.debug/g, /(release\s*\{)[^}]*?signingConfig\s+signingConfigs\.debug/,
"signingConfig signingConfigs.release" `$1
); signingConfig signingConfigs.release
`,
)
return output; return output
} }
+35 -33
View File
@@ -50,6 +50,9 @@ importers:
expo-device: expo-device:
specifier: ~55.0.15 specifier: ~55.0.15
version: 55.0.15(expo@55.0.15) version: 55.0.15(expo@55.0.15)
expo-document-picker:
specifier: ~55.0.13
version: 55.0.13(expo@55.0.15)
expo-font: expo-font:
specifier: ~55.0.6 specifier: ~55.0.6
version: 55.0.6(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) version: 55.0.6(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -59,6 +62,9 @@ importers:
expo-image: expo-image:
specifier: ~55.0.8 specifier: ~55.0.8
version: 55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(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: 55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(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)
expo-image-picker:
specifier: ~55.0.18
version: 55.0.18(expo@55.0.15)
expo-linear-gradient: expo-linear-gradient:
specifier: ~55.0.13 specifier: ~55.0.13
version: 55.0.13(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) version: 55.0.13(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -1010,56 +1016,48 @@ 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==}
@@ -1132,56 +1130,48 @@ 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==}
@@ -1600,28 +1590,24 @@ 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==}
@@ -2256,6 +2242,11 @@ packages:
peerDependencies: peerDependencies:
expo: '*' expo: '*'
expo-document-picker@55.0.13:
resolution: {integrity: sha512-IhswJElhdzs3fKDEKW8KXYRoFkWGEsXRMYAZT46Yo56zqqy8yQXrczo33RSwD2hFzNQBdLT97SJL9N311UyS3g==}
peerDependencies:
expo: '*'
expo-file-system@55.0.16: expo-file-system@55.0.16:
resolution: {integrity: sha512-EetQ/zVFK07Vmz4Yke0fvoES4xVwScTdd0PMoLekuMX7puE4op75pNnEdh1M0AeWzkqLrBoZIaU2ynSrKN5VZg==} resolution: {integrity: sha512-EetQ/zVFK07Vmz4Yke0fvoES4xVwScTdd0PMoLekuMX7puE4op75pNnEdh1M0AeWzkqLrBoZIaU2ynSrKN5VZg==}
peerDependencies: peerDependencies:
@@ -2276,6 +2267,16 @@ packages:
react: '*' react: '*'
react-native: '*' react-native: '*'
expo-image-loader@55.0.0:
resolution: {integrity: sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==}
peerDependencies:
expo: '*'
expo-image-picker@55.0.18:
resolution: {integrity: sha512-lGpPGRu+7mE8qN0ma2boRsCmfOGbdHZ2bXTpWVeWly0JCZdogGlTrYFnhTqgS8+lmiRb/UCOs7iTm2P5Rra6kw==}
peerDependencies:
expo: '*'
expo-image@55.0.8: expo-image@55.0.8:
resolution: {integrity: sha512-fNdvdYVcGn3g1x6o5AXHKzk4xX8U6rg2W9vFdE1pQO80kWCNReh003ypqSrGy4dD+zA8FtZjrNF3oMDGnPpIGQ==} resolution: {integrity: sha512-fNdvdYVcGn3g1x6o5AXHKzk4xX8U6rg2W9vFdE1pQO80kWCNReh003ypqSrGy4dD+zA8FtZjrNF3oMDGnPpIGQ==}
peerDependencies: peerDependencies:
@@ -2539,7 +2540,7 @@ packages:
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported 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
gopd@1.2.0: gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
@@ -2872,84 +2873,72 @@ 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==}
@@ -6497,6 +6486,10 @@ snapshots:
expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3) expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3)
ua-parser-js: 0.7.41 ua-parser-js: 0.7.41
expo-document-picker@55.0.13(expo@55.0.15):
dependencies:
expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3)
expo-file-system@55.0.16(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)): expo-file-system@55.0.16(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)):
dependencies: dependencies:
expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3) expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3)
@@ -6515,6 +6508,15 @@ 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)
expo-image-loader@55.0.0(expo@55.0.15):
dependencies:
expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3)
expo-image-picker@55.0.18(expo@55.0.15):
dependencies:
expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3)
expo-image-loader: 55.0.0(expo@55.0.15)
expo-image@55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(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): expo-image@55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(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:
expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3) expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(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)(typescript@5.9.3)
@@ -0,0 +1,25 @@
import { memo } from "react";
import {
KeyboardAwareScrollView,
KeyboardAwareScrollViewProps,
} from "react-native-keyboard-controller";
import {
SCROLLABLE_TYPE,
createBottomSheetScrollableComponent,
type BottomSheetScrollViewMethods,
} from "@gorhom/bottom-sheet";
import type { BottomSheetScrollViewProps } from "@gorhom/bottom-sheet/src/components/bottomSheetScrollable/types";
import Reanimated from "react-native-reanimated";
const AnimatedScrollView = Reanimated.createAnimatedComponent(KeyboardAwareScrollView);
const BottomSheetScrollViewComponent = createBottomSheetScrollableComponent<
BottomSheetScrollViewMethods,
BottomSheetScrollViewProps
>(SCROLLABLE_TYPE.SCROLLVIEW, AnimatedScrollView);
const BottomSheetKeyboardAwareScrollView = memo(BottomSheetScrollViewComponent);
BottomSheetKeyboardAwareScrollView.displayName = "BottomSheetKeyboardAwareScrollView";
export default BottomSheetKeyboardAwareScrollView as (
props: BottomSheetScrollViewProps & KeyboardAwareScrollViewProps,
) => ReturnType<typeof BottomSheetKeyboardAwareScrollView>;
+47 -36
View File
@@ -1,43 +1,54 @@
import { Button, Toast, useToast, useThemeColor } from "heroui-native"; import { BottomSheet, Button, TextField, Input, useBottomSheetAwareHandlers } from "heroui-native";
import { View } from "react-native"; import { useState, useRef, useEffect } from "react";
import { Keyboard } from "react-native";
export default function ToastExample() { const BottomSheetTextInput = ({ isOpen }: { isOpen: boolean }) => {
const { toast } = useToast(); const { onFocus, onBlur } = useBottomSheetAwareHandlers();
const themeColorForeground = useThemeColor("foreground"); const inputRef = useRef<any>(null);
useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
} else {
inputRef.current?.blur();
}
}, [isOpen]);
return ( return (
<View className="gap-4 p-4"> <TextField>
<Button <Input ref={inputRef} placeholder="Type here..." onFocus={onFocus} onBlur={onBlur} />
onPress={() => </TextField>
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>
<Button export default function BottomSheetExample() {
onPress={() => const [isOpen, setIsOpen] = useState(false);
toast.show({
duration: 0, return (
component: (props) => ( <BottomSheet
<Toast variant="accent" {...props}> isOpen={isOpen}
<Toast.Title>Custom Toast</Toast.Title> onOpenChange={(open) => {
<Toast.Description>This uses a custom component</Toast.Description> setIsOpen(open);
<Toast.Action onPress={() => props.hide()}>Undo</Toast.Action>
<Toast.Close className="absolute top-0 right-0" /> if (!open) {
</Toast> Keyboard.dismiss(); // ✅ 关闭时收键盘
),
})
} }
> }}
Show Custom Toast animation="disable-all"
</Button> >
</View> <BottomSheet.Trigger asChild>
<Button variant="secondary" className="mt-30">
Open Bottom Sheet
</Button>
</BottomSheet.Trigger>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content enableBlurKeyboardOnGesture keyboardBlurBehavior="restore">
<BottomSheetTextInput isOpen={isOpen} />
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
); );
} }
@@ -0,0 +1,39 @@
import { BottomSheet, Button, TextField, Input, useBottomSheetAwareHandlers } from "heroui-native";
import { useState } from "react";
const BottomSheetTextInput = () => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
return (
<TextField>
<Input placeholder="Type here..." onFocus={onFocus} onBlur={onBlur} />
</TextField>
);
};
export default function BottomSheetExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<BottomSheet
isOpen={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
}}
>
<BottomSheet.Trigger asChild>
<Button variant="secondary" className="mt-30">
Open Bottom Sheet
</Button>
</BottomSheet.Trigger>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content keyboardBlurBehavior="restore">
<BottomSheetTextInput />
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
);
}
@@ -1,6 +1,8 @@
import React from "react"; import React, { useEffect } from "react";
import LayoutView from "@/components/layout/view"; import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus"; import EmptyState from "@/components/EmptyStatus";
import { s3ListBucketsApi } from "@/request/api";
export default function Home() { export default function Home() {
// const [dailyData, setDailyData] = useState<any>(null); // const [dailyData, setDailyData] = useState<any>(null);
@@ -15,6 +17,12 @@ export default function Home() {
// .catch(() => {}); // .catch(() => {});
// }, []); // }, []);
useEffect(() => {
s3ListBucketsApi().then((res) => {
console.log(res);
});
}, []);
return ( return (
<LayoutView> <LayoutView>
<LayoutView.ScrollView> <LayoutView.ScrollView>
+5 -3
View File
@@ -30,12 +30,14 @@ export default function My() {
<LayoutView.ScrollView> <LayoutView.ScrollView>
<View className="flex-row items-center"> <View className="flex-row items-center">
<Avatar alt=""> <Avatar alt="">
<Avatar.Image source={{ uri: "https://example.com/avatar.jpg" }} /> <Avatar.Image source={{ uri: userStore.userInfo.avatar }} />
<Avatar.Fallback className="text-sm">JD</Avatar.Fallback> <Avatar.Fallback className="text-sm">{userStore.userInfo.nickname}</Avatar.Fallback>
</Avatar> </Avatar>
<View className="ml-3 mr-1"> <View className="ml-3 mr-1">
<Text className="text-xl font-semibold text-foreground">JD</Text> <Text className="text-xl font-semibold text-foreground">
{userStore.userInfo.nickname}
</Text>
</View> </View>
<ChevronRightIcon size={18} color="#c7c7cc" /> <ChevronRightIcon size={18} color="#c7c7cc" />
+75 -35
View File
@@ -26,6 +26,48 @@ import { UserRoundIcon, UsersRoundIcon } from "lucide-react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { FadeIn } from "react-native-reanimated"; import { FadeIn } from "react-native-reanimated";
import { generateCode } from "@/utils/common"; import { generateCode } from "@/utils/common";
import { Keyboard } from "react-native";
import { forwardRef } from "react";
const BottomSheetTextInput = forwardRef<any, React.ComponentProps<typeof Input>>((props, ref) => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
return (
<Input
ref={ref}
{...props}
onFocus={(e) => {
onFocus(e);
props.onFocus?.(e);
}}
onBlur={(e) => {
onBlur(e);
props.onBlur?.(e);
}}
/>
);
});
const BottomSheetTextInputOTP = forwardRef<any, React.ComponentProps<typeof InputOTP>>(
(props, ref) => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
return (
<InputOTP
ref={ref}
{...props}
onFocus={(e) => {
onFocus(e);
props.onFocus?.(e);
}}
onBlur={(e) => {
onBlur(e);
props.onBlur?.(e);
}}
/>
);
},
);
export default function Space() { export default function Space() {
const router = useRouter(); const router = useRouter();
@@ -54,28 +96,38 @@ export default function Space() {
const [createLoading, setCreateLoading] = useState(false); const [createLoading, setCreateLoading] = useState(false);
const createInputRef = useRef<any>(null); const createInputRef = useRef<any>(null);
const { onFocus, onBlur } = useBottomSheetAwareHandlers(); useEffect(() => {
listUserSpaces();
}, []);
useEffect(() => { useEffect(() => {
if (showCreate) { if (showCreate) {
setTimeout(() => { requestAnimationFrame(() => {
createInputRef.current?.focus(); createInputRef.current?.focus();
}, 0); });
} else {
setCreateForm({
name: "",
});
setCreateLoading(false);
Keyboard.dismiss();
} }
}, [showCreate]); }, [showCreate]);
useEffect(() => { useEffect(() => {
if (showJoin) { if (showJoin) {
setTimeout(() => { requestAnimationFrame(() => {
joinInputRef.current?.focus(); joinInputRef.current?.focus();
}, 0); });
} else {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
Keyboard.dismiss();
} }
}, [showJoin]); }, [showJoin]);
useEffect(() => {
listUserSpaces();
}, []);
function listUserSpaces(loading = true) { function listUserSpaces(loading = true) {
if (loading) { if (loading) {
setLoading(true); setLoading(true);
@@ -396,7 +448,7 @@ export default function Space() {
className="flex-1" className="flex-1"
onPress={() => { onPress={() => {
setShowCreate(true); setShowCreate(true);
createInputRef.current?.focus(); // createInputRef.current?.focus();
}} }}
> >
<PlusIcon color="#fff" size={20} /> <PlusIcon color="#fff" size={20} />
@@ -463,32 +515,25 @@ export default function Space() {
/> />
)} )}
</LayoutView.ScrollView> </LayoutView.ScrollView>
<BottomSheet <BottomSheet
isOpen={showCreate} isOpen={showCreate}
onOpenChange={(val) => { onOpenChange={(val) => {
if (!val) {
setCreateForm({
name: "",
});
setCreateLoading(false);
}
setShowCreate(val); setShowCreate(val);
}} }}
> >
<BottomSheet.Portal> <BottomSheet.Portal>
<BottomSheet.Overlay /> <BottomSheet.Overlay />
<BottomSheet.Content
<BottomSheet.Content> animation="disabled"
enableBlurKeyboardOnGesture
keyboardBlurBehavior="restore"
>
<View> <View>
<TextField> <TextField>
<Label>空间名称</Label> <Label>空间名称</Label>
<Input <BottomSheetTextInput
ref={createInputRef} ref={createInputRef}
placeholder="请输入空间名称" placeholder="请输入空间名称"
onFocus={onFocus}
onBlur={onBlur}
value={createForm.name} value={createForm.name}
onChangeText={(val) => onChangeText={(val) =>
setCreateForm({ setCreateForm({
@@ -510,28 +555,23 @@ export default function Space() {
<BottomSheet <BottomSheet
isOpen={showJoin} isOpen={showJoin}
onOpenChange={(val) => { onOpenChange={(val) => {
if (!val) {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
}
setShowJoin(val); setShowJoin(val);
}} }}
> >
<BottomSheet.Portal> <BottomSheet.Portal>
<BottomSheet.Overlay /> <BottomSheet.Overlay />
<BottomSheet.Content> <BottomSheet.Content
animation={"disabled"}
enableBlurKeyboardOnGesture
keyboardBlurBehavior="restore"
>
<View> <View>
<TextField> <TextField>
<Label>邀请码</Label> <Label>邀请码</Label>
<InputOTP <BottomSheetTextInputOTP
ref={joinInputRef} ref={joinInputRef}
maxLength={6} maxLength={6}
onFocus={onFocus}
onBlur={onBlur}
value={joinForm.invite_code} value={joinForm.invite_code}
onChange={(val) => { onChange={(val) => {
setJoinForm({ setJoinForm({
@@ -550,7 +590,7 @@ export default function Space() {
<InputOTP.Slot index={4} /> <InputOTP.Slot index={4} />
<InputOTP.Slot index={5} /> <InputOTP.Slot index={5} />
</InputOTP.Group> </InputOTP.Group>
</InputOTP> </BottomSheetTextInputOTP>
</TextField> </TextField>
<Button className="mt-3" onPress={handleJoin}> <Button className="mt-3" onPress={handleJoin}>
+12 -11
View File
@@ -11,9 +11,16 @@ import { Toaster } from "sonner-native";
import { KeyboardProvider } from "react-native-keyboard-controller"; import { KeyboardProvider } from "react-native-keyboard-controller";
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { HeroUINativeConfig } from "heroui-native";
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();
const config: HeroUINativeConfig = {
devInfo: {
stylingPrinciples: false,
},
};
export default function RootLayout() { export default function RootLayout() {
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
@@ -46,15 +53,9 @@ export default function RootLayout() {
return ( return (
<GestureHandlerRootView className="flex-1"> <GestureHandlerRootView className="flex-1">
<KeyboardProvider> <ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}> <KeyboardProvider>
<HeroUINativeProvider <HeroUINativeProvider config={config}>
config={{
devInfo: {
stylingPrinciples: false,
},
}}
>
<Stack <Stack
screenOptions={{ screenOptions={{
animation: "slide_from_right", animation: "slide_from_right",
@@ -66,8 +67,8 @@ export default function RootLayout() {
/> />
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />} {!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
</HeroUINativeProvider> </HeroUINativeProvider>
</ThemeProvider> </KeyboardProvider>
</KeyboardProvider> </ThemeProvider>
<Toaster duration={1000} /> <Toaster duration={1000} />
</GestureHandlerRootView> </GestureHandlerRootView>
); );
+179 -14
View File
@@ -1,4 +1,4 @@
import React from "react"; import { useEffect } from "react";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { Image, Text, View } from "react-native"; import { Image, Text, View } from "react-native";
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native"; import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
@@ -7,19 +7,55 @@ import { useState } from "react";
import { FadeIn } from "react-native-reanimated"; import { FadeIn } from "react-native-reanimated";
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import ExpoQQ from "expo-qq";
import ExpoWeChat from "expo-wechat";
import { useEvent } from "expo";
import { toast } from "sonner-native";
import { loginQqApi, loginWxApi } from "@/request/api";
export default function Login() { export default function Login() {
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
// 登录
const onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
const authResult = useEvent(ExpoWeChat, "onAuthResult");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isAgreed, setIsAgreed] = useState(true); const [isAgreed, setIsAgreed] = useState(true);
const [form, setForm] = useState({
type: 0,
openid: "",
code: "",
});
useEffect(() => {
ExpoQQ.init("102826474", "");
ExpoWeChat.registerApp("wxdab3e21a1f7e392f", "");
}, []);
// 微信登录 // 微信登录
async function handleLoginWechat() { async function handleLoginWechat() {
if (loading) { if (loading) {
return; return;
} }
setLoading(true);
// 检查微信是否安装
const isInstalled = await ExpoWeChat.isWXAppInstalled();
if (!isInstalled) {
toast.warning(onLoginFinished?.errorMessage || "请先安装微信");
setLoading(false);
return;
}
setForm((val) => ({
...val,
type: 0,
}));
ExpoWeChat.sendAuthRequest("snsapi_userinfo", "state123");
} }
// QQ 登录 // QQ 登录
@@ -30,23 +66,152 @@ export default function Login() {
setLoading(true); setLoading(true);
setTimeout(() => { setForm((val) => ({
requestAnimationFrame(() => { ...val,
userStore.setUserInfo({ type: 1,
id: 779895419121733, }));
token: "123", await ExpoQQ.login(["get_user_info"]);
nickname: "Zhang", }
avatar: "",
/**
* {"code": 1000, "data": {"avatar": "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100", "nickname": "Zhang", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo3OTY0NTY3MTI3ODU5ODl9LCJpYXQiOjE3NzY1ODU2MDR9.ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY", "userId": 796456712785989}, "isSuccess": true, "message": "success", "track_id": "djIRo23SZlA160zBTH2DX-kxeuLApKTNyZdxqqauSMk"}
*/
// function handleTest() {
// if (loading) {
// return;
// }
// setLoading(true);
// setTimeout(() => {
// requestAnimationFrame(() => {
// userStore.setUserInfo({
// id: 779895419121733,
// token: "ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY",
// nickname: "Zhang",
// avatar: "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
// });
// userStore.setAuthExpired(false);
// router.replace("/(tabs)/home");
// });
// setLoading(false);
// }, 2000);
// }
useEffect(() => {
if (!onLoginFinished) {
return;
}
if (!onLoginFinished.success) {
toast.warning(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
setLoading(false);
return;
}
const fetchUserInfo = async () => {
try {
const tokenInfo = await ExpoQQ.getLoginTokenInfo();
if (!tokenInfo.openId) {
toast.warning(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
setLoading(false);
return;
}
setForm((val) => ({
...val,
openid: String(tokenInfo.openId),
}));
await ExpoQQ.sendGetUserInfoReq();
} catch (e) {
console.log(e);
setLoading(false);
}
};
fetchUserInfo();
}, [onLoginFinished]);
// QQ登录
useEffect(() => {
const handleLogin = async () => {
if (!onGetUserInfo) {
setLoading(false);
return;
}
try {
const loginRes = await loginQqApi({
openid: form.openid,
nickname: onGetUserInfo.nickname || "",
avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
}); });
userStore.setAuthExpired(false); console.log(loginRes);
router.replace("/(tabs)/home"); if (loginRes.isSuccess) {
}); requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
setLoading(false); userStore.setAuthExpired(false);
}, 2000); router.replace("/(tabs)/home");
} });
}
} catch (err) {
console.error("登录接口出错", err);
} finally {
setLoading(false);
}
};
handleLogin();
}, [onGetUserInfo]);
// 微信登录
useEffect(() => {
if (!authResult) return;
const handleLogin = async () => {
if (authResult.errorCode === 0) {
try {
const loginRes = await loginWxApi({
code: authResult.code,
});
if (loginRes.isSuccess) {
requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
userStore.setAuthExpired(false);
router.replace("/(tabs)/home");
});
}
} catch (err) {
console.error("登录接口出错", err);
} finally {
setLoading(false);
}
} else {
console.log("授权失败:", authResult.errorMessage);
setLoading(false);
}
};
handleLogin();
}, [authResult]);
return ( return (
<> <>
+18
View File
@@ -47,3 +47,21 @@ interface SaveDataParams {
export function saveDataApi(params: SaveDataParams[]) { export function saveDataApi(params: SaveDataParams[]) {
return alovaInstance.Post<ResponseData<null>>(`/data/saveData`, params); return alovaInstance.Post<ResponseData<null>>(`/data/saveData`, params);
} }
export function s3ListBucketsApi() {
return alovaInstance.Post<ResponseData<string[]>>(`/s3/listBuckets`);
}
export function s3UploadFileApi(files: { uri: string; name: string; type: string }[]) {
const form = new FormData();
files.forEach((file) => {
form.append("files", file as any);
});
return alovaInstance.Post<ResponseData<string[]>>("/s3/upload", form, {
headers: {
"Content-Type": "multipart/form-data",
},
});
}
+272
View File
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>家庭系统 Pro Demo</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.hidden {
display: none;
}
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 14px;
border-radius: 10px;
font-size: 12px;
}
</style>
</head>
<body class="bg-gray-100">
<div id="app" class="max-w-md mx-auto min-h-screen flex flex-col">
<!-- ================= LOGIN ================= -->
<div id="login" class="flex-1 flex items-center justify-center p-6">
<div class="bg-white p-6 rounded-2xl w-full shadow">
<h1 class="text-xl font-bold mb-1">欢迎回来</h1>
<p class="text-sm text-gray-400 mb-4">家庭系统登录</p>
<input id="username" class="w-full border p-2 rounded mb-3" placeholder="请输入名字">
<button onclick="login()" class="w-full bg-blue-500 text-white py-2 rounded">
登录
</button>
</div>
</div>
<!-- ================= FAMILY ONBOARD ================= -->
<div id="onboard" class="hidden flex-1 p-6">
<div class="bg-white p-6 rounded-2xl shadow">
<h1 class="text-lg font-bold">创建你的家庭</h1>
<p class="text-sm text-gray-400 mb-4">你还没有加入任何家庭</p>
<input id="familyName" class="w-full border p-2 rounded mb-3" placeholder="家庭名称">
<button onclick="createFamily()" class="w-full bg-green-500 text-white py-2 rounded mb-2">
创建家庭
</button>
<button onclick="joinFamilyMock()" class="w-full bg-gray-200 py-2 rounded">
加入示例家庭
</button>
</div>
</div>
<!-- ================= MAIN ================= -->
<div id="main" class="hidden flex-1 flex flex-col">
<!-- CONTENT -->
<div class="flex-1 p-4 space-y-3">
<!-- 家庭卡 -->
<div class="bg-white p-4 rounded-2xl shadow">
<div class="flex justify-between">
<div>
<h2 class="font-bold text-lg">🏠 <span id="familyTitle"></span></h2>
<p class="text-xs text-gray-400">邀请码:ABC123 · 3人</p>
</div>
<button onclick="openSettings()" class="text-xs bg-gray-100 px-3 py-1 rounded-lg">
设置
</button>
</div>
</div>
<!-- 成员 -->
<div class="bg-white p-4 rounded-2xl shadow">
<div class="flex justify-between mb-3">
<div class="font-semibold">家庭成员</div>
<button class="text-blue-500 text-sm">邀请</button>
</div>
<div class="flex gap-4">
<div class="text-center">
<div class="w-10 h-10 bg-blue-200 rounded-full"></div>
<div class="text-xs mt-1">我</div>
</div>
<div class="text-center">
<div class="w-10 h-10 bg-pink-200 rounded-full"></div>
<div class="text-xs mt-1">妈妈</div>
</div>
<div class="text-center">
<div class="w-10 h-10 bg-yellow-200 rounded-full"></div>
<div class="text-xs mt-1">爸爸</div>
</div>
</div>
</div>
<!-- 子应用 -->
<div class="bg-white p-4 rounded-2xl shadow">
<div class="font-semibold mb-3">家庭工具</div>
<div class="grid grid-cols-3 gap-3">
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('记账')">
📒<div class="text-xs mt-1">记账</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('清单')">
🛒<div class="text-xs mt-1">清单</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('日程')">
📅<div class="text-xs mt-1">日程</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('物品')">
📦<div class="text-xs mt-1">物品</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('待办')">
📝<div class="text-xs mt-1">待办</div>
</div>
<div class="bg-gray-100 p-3 rounded-xl text-center text-gray-400">
➕<div class="text-xs mt-1">添加</div>
</div>
</div>
</div>
</div>
<!-- TABBAR -->
<div class="flex border-t bg-white">
<button class="flex-1 py-2" onclick="switchTab('home')">🏠</button>
<button class="flex-1 py-2" onclick="switchTab('apps')">🧩</button>
<button class="flex-1 py-2" onclick="switchTab('me')">👤</button>
</div>
</div>
<!-- ================= SETTINGS ================= -->
<div id="settings" class="hidden flex-1 p-4">
<div class="bg-white p-4 rounded-2xl shadow">
<h2 class="font-bold mb-3">家庭设置</h2>
<div class="space-y-2 text-sm">
<div class="p-2 bg-gray-100 rounded">修改家庭名称</div>
<div class="p-2 bg-gray-100 rounded">成员管理</div>
<div class="p-2 bg-gray-100 rounded text-red-500">退出家庭</div>
</div>
<button onclick="backHome()" class="mt-4 w-full bg-blue-500 text-white py-2 rounded">
返回
</button>
</div>
</div>
</div>
<!-- toast -->
<div id="toast" class="toast hidden"></div>
<script>
let state = {
user: null,
family: null
}
function show(id) {
document.querySelectorAll('#app > div').forEach(v => v.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');
}
function toast(msg) {
const t = document.getElementById('toast');
t.innerText = msg;
t.classList.remove('hidden');
setTimeout(() => {
t.classList.add('hidden');
}, 1500);
}
/* ========== login ========== */
function login() {
const name = document.getElementById('username').value;
if (!name) return toast('请输入用户名');
state.user = name;
show('onboard');
}
/* ========== family ========== */
function createFamily() {
const name = document.getElementById('familyName').value;
if (!name) return toast('请输入家庭名称');
state.family = name;
enterApp();
}
function joinFamilyMock() {
state.family = "幸福家庭";
enterApp();
}
function enterApp() {
document.getElementById('familyTitle').innerText = state.family;
show('main');
switchTab('home');
}
/* ========== tab ========== */
function switchTab(tab) {
document.querySelectorAll('#main > div:nth-child(1) > div').forEach(v => {
v.style.display = 'none';
});
toast("切换:" + tab);
}
function openApp(name) {
toast("进入:" + name);
}
/* ========== settings ========== */
function openSettings() {
show('settings');
}
function backHome() {
show('main');
}
</script>
</body>
</html>