diff --git a/code/allapp/allapp-go-v3/internal/handle/data_list.go b/code/allapp/allapp-go-v3/internal/handle/data_list.go index ded853a1..a5de4356 100644 --- a/code/allapp/allapp-go-v3/internal/handle/data_list.go +++ b/code/allapp/allapp-go-v3/internal/handle/data_list.go @@ -5,6 +5,7 @@ import ( "allapp-go/internal/httpx" "allapp-go/internal/types" "allapp-go/pkg/db" + "allapp-go/pkg/uniqueid" "github.com/gofiber/fiber/v3" ) @@ -48,3 +49,22 @@ func LoadDataBySql(c fiber.Ctx) error { 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) +} diff --git a/code/allapp/allapp-go-v3/internal/handle/data_save.go b/code/allapp/allapp-go-v3/internal/handle/data_save.go index 9bcae627..0d9e20c5 100644 --- a/code/allapp/allapp-go-v3/internal/handle/data_save.go +++ b/code/allapp/allapp-go-v3/internal/handle/data_save.go @@ -21,6 +21,7 @@ func SaveData(c fiber.Ctx) error { } ctx := c.Context() + dbClient := db.New() err := dbClient.WithTx(ctx, func(tx *db.Client) error { diff --git a/code/allapp/allapp-go-v3/internal/httpx/request.go b/code/allapp/allapp-go-v3/internal/httpx/request.go index 5cb0f874..dc1b90c5 100644 --- a/code/allapp/allapp-go-v3/internal/httpx/request.go +++ b/code/allapp/allapp-go-v3/internal/httpx/request.go @@ -1,6 +1,8 @@ package httpx import ( + "reflect" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v3" ) @@ -8,10 +10,23 @@ import ( var validate = validator.New() func BindAndValidate(c fiber.Ctx, v any) error { + // 1️⃣ 绑定 JSON if err := c.Bind().Body(v); err != nil { 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 { return err } diff --git a/code/allapp/allapp-go-v3/internal/middleware/auth.go b/code/allapp/allapp-go-v3/internal/middleware/auth.go index c8e9a9f2..b453dbb9 100644 --- a/code/allapp/allapp-go-v3/internal/middleware/auth.go +++ b/code/allapp/allapp-go-v3/internal/middleware/auth.go @@ -1,9 +1,7 @@ package middleware import ( - "allapp-go/internal/httpx" - "allapp-go/pkg/jwtx" - "strings" + "context" "github.com/gofiber/fiber/v3" ) @@ -13,35 +11,30 @@ const CtxUserIDKey = "user_id" // Auth 登录校验中间件 func Auth() fiber.Handler { return func(c fiber.Ctx) error { - token := strings.TrimSpace(c.Get("Token")) - - if token == "" { - return httpx.Unauthorized(c, "未登录") - } + //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"]) - 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)) + c.SetContext(ctx) return c.Next() } } -func GetUserID(c fiber.Ctx) (int64, error) { - v := c.Locals(CtxUserIDKey) +func GetUserID(ctx context.Context) (int64, bool) { + v := ctx.Value(CtxUserIDKey) - if v == nil { - return 0, httpx.Unauthorized(c, "登录已过期") - } + id, ok := v.(int64) - userID, ok := v.(int64) - - if !ok { - return 0, httpx.Unauthorized(c, "登录已过期") - } - - return userID, nil + return id, ok } diff --git a/code/allapp/allapp-go-v3/internal/router/router.go b/code/allapp/allapp-go-v3/internal/router/router.go index 11fb63cf..5b9d9f8b 100644 --- a/code/allapp/allapp-go-v3/internal/router/router.go +++ b/code/allapp/allapp-go-v3/internal/router/router.go @@ -3,6 +3,7 @@ package router import ( "allapp-go/internal/config" "allapp-go/internal/handle" + "allapp-go/internal/middleware" "github.com/gofiber/fiber/v3" ) @@ -11,10 +12,14 @@ func SetupRouter(app *fiber.App, cfg *config.Config) { api := app.Group(cfg.App.BaseUrl) // ==================== data ==================== - api.Post("/data/loadData", handle.LoadData) - api.Post("/data/loadDataBySql", handle.LoadDataBySql) - api.Post("/data/saveData", handle.SaveData) + data := api.Group("/data", middleware.Auth()) + 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/wechat", handle.LoginWechat) } diff --git a/code/allapp/allapp-go-v3/internal/types/data_list.go b/code/allapp/allapp-go-v3/internal/types/data_list.go index 5d5bdf7e..a6939a6f 100644 --- a/code/allapp/allapp-go-v3/internal/types/data_list.go +++ b/code/allapp/allapp-go-v3/internal/types/data_list.go @@ -12,3 +12,7 @@ type LoadDataBySqlReq struct { Sql string `json:"sql" validate:"required"` Args []any `json:"args"` } + +type GetUniqueIdReq struct { + Count int `json:"count" binding:"required,min=1,max=100"` +} diff --git a/code/allapp/allapp-go-v3/pkg/db/common.go b/code/allapp/allapp-go-v3/pkg/db/common.go index 6816b11e..8672148d 100644 --- a/code/allapp/allapp-go-v3/pkg/db/common.go +++ b/code/allapp/allapp-go-v3/pkg/db/common.go @@ -6,6 +6,11 @@ import ( "strings" ) +var auditExcludeTables = map[string]bool{ + "user": true, + "user_oauth": true, +} + func validateData(table string, keyField string, data map[string]any) (any, error) { if table == "" { return nil, fmt.Errorf("表名不能为空") diff --git a/code/allapp/allapp-go-v3/pkg/db/insert.go b/code/allapp/allapp-go-v3/pkg/db/insert.go index c56b5d49..484488bd 100644 --- a/code/allapp/allapp-go-v3/pkg/db/insert.go +++ b/code/allapp/allapp-go-v3/pkg/db/insert.go @@ -1,10 +1,12 @@ package db import ( + "allapp-go/internal/middleware" "context" "fmt" "sort" "strings" + "time" ) // 构建 INSERT SQL(单条) @@ -43,6 +45,25 @@ func (c *Client) Insert( 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) _, err := c.exec().Exec(ctx, sql, args...) diff --git a/code/allapp/allapp-go-v3/pkg/db/select.go b/code/allapp/allapp-go-v3/pkg/db/select.go index 6ea8caaa..461c3d26 100644 --- a/code/allapp/allapp-go-v3/pkg/db/select.go +++ b/code/allapp/allapp-go-v3/pkg/db/select.go @@ -1,9 +1,12 @@ package db import ( + "allapp-go/pkg/logger" "context" "fmt" "strings" + + "go.uber.org/zap" ) func (c *Client) LoadData( @@ -15,33 +18,32 @@ func (c *Client) LoadData( args []any, ) ([]map[string]any, error) { - var where string - - // WHERE 构造 - 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 + "%" - } - } + selectCols := "*" + if len(searchColumns) > 0 { + selectCols = strings.Join(searchColumns, ", ") } - // ORDER BY - var order string + where := "" + if searchCondition != "" { + where = " WHERE " + searchCondition + } + + order := "" if 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...) } @@ -51,6 +53,10 @@ func (c *Client) LoadDataBySQL( sql string, args []any, ) ([]map[string]any, error) { + logger.FromCtx(ctx).Info("LoadDataBySQL", + zap.String("sql", sql), + zap.Any("args", args), + ) return c.query(ctx, sql, args...) } diff --git a/code/allapp/allapp-go-v3/pkg/db/update.go b/code/allapp/allapp-go-v3/pkg/db/update.go index 01b06a03..2d725646 100644 --- a/code/allapp/allapp-go-v3/pkg/db/update.go +++ b/code/allapp/allapp-go-v3/pkg/db/update.go @@ -1,10 +1,12 @@ package db import ( + "allapp-go/internal/middleware" "context" "fmt" "sort" "strings" + "time" ) func (c *Client) Update( @@ -18,6 +20,21 @@ func (c *Client) Update( 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 ( setClauses []string args []any diff --git a/code/app/app-rn4/package.json b/code/app/app-rn4/package.json index 67656900..0201a24b 100644 --- a/code/app/app-rn4/package.json +++ b/code/app/app-rn4/package.json @@ -46,10 +46,12 @@ "react-dom": "19.2.0", "react-native": "0.83.4", "react-native-gesture-handler": "~2.30.0", + "react-native-keyboard-controller": "1.20.7", "react-native-reanimated": "4.2.1", "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.23.0", "react-native-shadow-2": "^7.1.2", + "react-native-sortables": "^1.9.4", "react-native-svg": "15.15.3", "react-native-web": "~0.21.0", "react-native-worklets": "0.7.2", diff --git a/code/app/app-rn4/pnpm-lock.yaml b/code/app/app-rn4/pnpm-lock.yaml index 208c7039..dff31067 100644 --- a/code/app/app-rn4/pnpm-lock.yaml +++ b/code/app/app-rn4/pnpm-lock.yaml @@ -107,6 +107,9 @@ importers: react-native-gesture-handler: 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) + 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: 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) @@ -119,6 +122,9 @@ importers: react-native-shadow-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)) + 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: 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) @@ -1004,48 +1010,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.44.0': resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.44.0': resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.44.0': resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.44.0': resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.44.0': resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.44.0': resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.44.0': resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.44.0': resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==} @@ -1118,48 +1132,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.60.0': resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.60.0': resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.60.0': resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.60.0': resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.60.0': resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.60.0': resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.60.0': resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.60.0': resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==} @@ -1578,24 +1600,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -2513,7 +2539,7 @@ packages: glob@7.2.3: 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: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} @@ -2846,72 +2872,84 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.31.1: resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -3454,6 +3492,15 @@ packages: react: '*' 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: resolution: {integrity: sha512-qTjfHG2f/sc3/YKWZJvIJk/SwiTu8C9/SvtoLxRX+GM/6pvOJq0ge6rcV3vTh4s3QJDuNDOD6+a4hGD6vv2vSg==} hasBin: true @@ -3474,6 +3521,13 @@ packages: react: '*' 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: resolution: {integrity: sha512-/NcHnZMyOvsD/wYXug/YqSKw90P9edN0kEPL5lP4PFf1aQ4F1V7MKe/E0tvfkXKIajy3Qocp5EiEnlcrK/+BZg==} peerDependencies: @@ -3499,6 +3553,14 @@ packages: react-native: '*' 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: resolution: {integrity: sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==} peerDependencies: @@ -7868,6 +7930,13 @@ snapshots: 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): dependencies: colors: 1.4.0 @@ -7893,6 +7962,13 @@ snapshots: 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): dependencies: 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-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): dependencies: css-select: 5.2.2 diff --git a/code/app/app-rn4/src/app/(tabs)/_layout.tsx b/code/app/app-rn4/src/app/(tabs)/_layout.tsx index c933e9fd..6e3591a6 100644 --- a/code/app/app-rn4/src/app/(tabs)/_layout.tsx +++ b/code/app/app-rn4/src/app/(tabs)/_layout.tsx @@ -20,9 +20,9 @@ const tabs: TabbarItem[] = [ icon: HomeIcon, }, { - id: "/chat", - title: "聊天", - icon: MessageCircleIcon, + id: "/space", + title: "空间", + icon: EarthIcon, }, { id: "/tools", @@ -31,9 +31,9 @@ const tabs: TabbarItem[] = [ isCenter: true, }, { - id: "/space", - title: "空间", - icon: EarthIcon, + id: "/chat", + title: "聊天", + icon: MessageCircleIcon, }, { id: "/my", diff --git a/code/app/app-rn4/src/app/(tabs)/chat/index.tsx b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx index a3cd2db3..30eff9c6 100644 --- a/code/app/app-rn4/src/app/(tabs)/chat/index.tsx +++ b/code/app/app-rn4/src/app/(tabs)/chat/index.tsx @@ -1,36 +1,43 @@ -import { Button, Dialog } from "heroui-native"; +import { Button, Toast, useToast, useThemeColor } from "heroui-native"; import { View } from "react-native"; -import { useState } from "react"; -export default function DialogExample() { - const [isOpen, setIsOpen] = useState(false); +export default function ToastExample() { + const { toast } = useToast(); + const themeColorForeground = useThemeColor("foreground"); return ( - <> - + + - - - - - - - Confirm Action - - Are you sure you want to proceed with this action? This cannot be undone. - - - - - - - - - - + + ); } diff --git a/code/app/app-rn4/src/app/(tabs)/space/index.tsx b/code/app/app-rn4/src/app/(tabs)/space/index.tsx index b4b1ccf3..1fc6193a 100644 --- a/code/app/app-rn4/src/app/(tabs)/space/index.tsx +++ b/code/app/app-rn4/src/app/(tabs)/space/index.tsx @@ -1,13 +1,566 @@ -import React from "react"; +import React, { useEffect, useState, useRef } from "react"; 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() { + const router = useRouter(); + const userStore = useUserStore(); + const spaceStore = useSpaceStore(); + + const ITEM_HEIGHT = 64; + + const [spaces, setSpaces] = useState([]); + + const [loading, setLoading] = useState(false); + + // 加入 + const [showJoin, setShowJoin] = useState(false); + const [joinLoading, setJoinLoading] = useState(false); + const [joinForm, setJoinForm] = useState({ + invite_code: "", + }); + const joinInputRef = useRef(null); + + // 新建 + const [showCreate, setShowCreate] = useState(false); + const [createForm, setCreateForm] = useState({ + name: "", + }); + const [createLoading, setCreateLoading] = useState(false); + const createInputRef = useRef(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 }) => ( + { + 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" : "" + }`} + > + + + + {item.name} + + {item.owner_id === userStore.userInfo.id && ( + + 创建者 + + )} + + {item.v_is_default === 1 && ( + + 默认 + + )} + + {item.space_id === spaceStore?.currentSpace?.space_id && ( + + 当前空间 + + )} + + + + + + {item.v_member_name} + + + + {item.v_member_count} + + + + + + + + + + + ); + return ( - 我的空间 + + + + + + + + 我的空间 + + + + {loading ? ( + + + + ) : spaces.length === 0 ? ( + + ) : ( + { + 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); + }); + }} + /> + )} + + { + if (!val) { + setCreateForm({ + name: "", + }); + setCreateLoading(false); + } + + setShowCreate(val); + }} + > + + + + + + + + + setCreateForm({ + name: val, + }) + } + /> + + + + + + + + + { + if (!val) { + setJoinForm({ + invite_code: "", + }); + setJoinLoading(false); + } + + setShowJoin(val); + }} + > + + + + + + + + { + setJoinForm({ + invite_code: val, + }); + }} + > + + + + + + + + + + + + + + + + + + + ); } diff --git a/code/app/app-rn4/src/app/+not-found.tsx b/code/app/app-rn4/src/app/+not-found.tsx index c38ab23e..41be8c35 100644 --- a/code/app/app-rn4/src/app/+not-found.tsx +++ b/code/app/app-rn4/src/app/+not-found.tsx @@ -18,11 +18,11 @@ export default function NotFound() { className="mt-4" onPress={() => { requestAnimationFrame(() => { - router.replace("/home"); + router.back(); }); }} > - 返回首页 + 返回 } /> diff --git a/code/app/app-rn4/src/app/_layout.tsx b/code/app/app-rn4/src/app/_layout.tsx index 59720142..773666ec 100644 --- a/code/app/app-rn4/src/app/_layout.tsx +++ b/code/app/app-rn4/src/app/_layout.tsx @@ -8,10 +8,15 @@ import "@/global.css"; import { useEffect, useState } from "react"; import { Stack } from "expo-router"; import { Toaster } from "sonner-native"; +import { KeyboardProvider } from "react-native-keyboard-controller"; +import { useUserStore } from "@/store"; +import { useRouter } from "expo-router"; SplashScreen.preventAutoHideAsync(); export default function RootLayout() { + const router = useRouter(); + const userStore = useUserStore(); const isDark = useIsDark(); const isDev = __DEV__; @@ -29,31 +34,40 @@ export default function RootLayout() { init(); }, []); + useEffect(() => { + if (userStore.authExpired) { + requestAnimationFrame(() => { + router.replace("/auth/login"); + }); + } + }, [userStore.authExpired]); + if (!isReady) return null; return ( - - - + + - {!isDev && } - - - + > + + {!isDev && } + + + ); diff --git a/code/app/app-rn4/src/app/auth/login.tsx b/code/app/app-rn4/src/app/auth/login.tsx index 8cceab6b..200d9760 100644 --- a/code/app/app-rn4/src/app/auth/login.tsx +++ b/code/app/app-rn4/src/app/auth/login.tsx @@ -33,12 +33,14 @@ export default function Login() { setTimeout(() => { requestAnimationFrame(() => { userStore.setUserInfo({ - id: 1, + id: 779895419121733, token: "123", nickname: "Zhang", avatar: "", }); + userStore.setAuthExpired(false); + router.replace("/(tabs)/home"); }); diff --git a/code/app/app-rn4/src/app/tab-space/index.tsx b/code/app/app-rn4/src/app/tab-space/index.tsx new file mode 100644 index 00000000..185f314e --- /dev/null +++ b/code/app/app-rn4/src/app/tab-space/index.tsx @@ -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 ( + + + + + Space + + + + + + { + router.back(); + }} + /> + + + + ); +} diff --git a/code/app/app-rn4/src/request/api.ts b/code/app/app-rn4/src/request/api.ts index b0dba2cc..172f2562 100644 --- a/code/app/app-rn4/src/request/api.ts +++ b/code/app/app-rn4/src/request/api.ts @@ -9,6 +9,12 @@ export function loginWxApi(params: LoginWxParams) { return alovaInstance.Post>(`/auth/login/wechat`, params); } +export function getUniqueIdApi(count = 1) { + return alovaInstance.Post>(`/data/getUniqueId`, { + count: count, + }); +} + interface LoadDataBySqlParams { sql: string; args: any[]; @@ -23,10 +29,21 @@ interface LoadDataParams { search_condition: string; order_by: string; search_columns: string[]; - args: string[]; + args: any[]; } export function loadDataApi(params: LoadDataParams) { return alovaInstance.Post>(`/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>(`/data/saveData`, params); +} diff --git a/code/app/app-rn4/src/store/index.ts b/code/app/app-rn4/src/store/index.ts index 927d0c5c..e25a2d97 100644 --- a/code/app/app-rn4/src/store/index.ts +++ b/code/app/app-rn4/src/store/index.ts @@ -1,2 +1,3 @@ export * from "./app"; export * from "./user"; +export * from "./space"; diff --git a/code/app/app-rn4/src/store/space.ts b/code/app/app-rn4/src/store/space.ts new file mode 100644 index 00000000..a038d169 --- /dev/null +++ b/code/app/app-rn4/src/store/space.ts @@ -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()( + 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), + }, + ), +); diff --git a/code/app/app-rn4/src/utils/common.ts b/code/app/app-rn4/src/utils/common.ts new file mode 100644 index 00000000..4b05e60d --- /dev/null +++ b/code/app/app-rn4/src/utils/common.ts @@ -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; +} diff --git a/code/test.html b/code/test.html new file mode 100644 index 00000000..711e3beb --- /dev/null +++ b/code/test.html @@ -0,0 +1,143 @@ + + + + + + Tree + + + + + + +
+ + +
+ + +
+ ▶ + 系统管理 +
+ +
+ +
+ 用户管理 +
+ +
+ 角色管理 +
+ + +
+ +
+ ▶ + 权限管理 +
+ +
+ +
+ 菜单权限 +
+ +
+ 按钮权限 +
+ +
+
+ +
+
+ +
+ + + + + + \ No newline at end of file diff --git a/code/test2.html b/code/test2.html new file mode 100644 index 00000000..b83324ac --- /dev/null +++ b/code/test2.html @@ -0,0 +1,384 @@ + + + + + + Dialog Final + + + + + + + + + + + + + + + + \ No newline at end of file