diff --git a/code/allapp/allapp-go/api/handler/space/index.go b/code/allapp/allapp-go/api/handler/space/index.go index 8cbcd1fd..b4effdf4 100644 --- a/code/allapp/allapp-go/api/handler/space/index.go +++ b/code/allapp/allapp-go/api/handler/space/index.go @@ -7,6 +7,8 @@ import ( "allapp/db/repo" "allapp/utils" "allapp/utils/errorx" + "allapp/utils/pgtypex" + "allapp/utils/redisx" "allapp/utils/requestx" "allapp/utils/responsex" "allapp/utils/uniqueid" @@ -180,6 +182,12 @@ func DissolveSpace(c fiber.Ctx) error { return err } + err = redisx.SpaceDelete(spaceId) + + if err != nil { + return err + } + return responsex.OK(c, "空间已成功解散") } @@ -212,6 +220,11 @@ func ExitSpace(c fiber.Ctx) error { return err } + err = redisx.SpaceRemoveMember(spaceId, userId) + if err != nil { + return err + } + return responsex.OK(c, "空间已成功退出") } @@ -409,6 +422,395 @@ func UpdateSpaceSortNumber(c fiber.Ctx) error { return responsex.OK(c, "更新成功") } +func ListSpaceMembers(c fiber.Ctx) error { + dto := new(types.ListSpaceMembersDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + members, err := db.Queries.ListSpaceMembers(c.Context(), dto.SpaceId) + + if err != nil { + return err + } + + spaceMembers := make([]types.SpaceMember, 0, len(members)) + + for _, member := range members { + spaceMembers = append(spaceMembers, types.SpaceMember{ + SpaceId: member.SpaceID, + UserId: member.UserID, + Role: member.Role, + MemberName: member.MemberName, + MemberStatus: member.MemberStatus, + MemberUpdateTime: pgtypex.TimestamptzToMillis(member.MemberUpdateTime), + JoinTime: pgtypex.TimestamptzToMillis(member.JoinTime), + Avatar: pgtypex.TextToString(member.Avatar), + UserStatus: pgtypex.Int2ToInt16(member.UserStatus), + LastLoginTime: pgtypex.TimestamptzToMillis(member.LastLoginTime), + }) + } + + vo := new(types.ListSpaceMembersVO) + vo.Members = spaceMembers + + return responsex.OK(c, vo) +} + +// KickoutSpaceMember 踢出空间 +func KickoutSpaceMember(c fiber.Ctx) error { + dto := new(types.KickoutSpaceMemberDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + spaceId := dto.SpaceId + + space, err := db.Queries.GetSpaceById(c.Context(), spaceId) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "空间不存在或已解散") + } + return err + } + + if userId != space.OwnerID { + return responsex.Error(c, "您不是该空间的拥有者") + } + + _, err = db.Queries.GetSpaceByUserIdAndSpaceId(c.Context(), repo.GetSpaceByUserIdAndSpaceIdParams{ + ID: spaceId, + UserID: dto.MemberId, + }) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "该用户已不再该空间内") + } + return err + } + + if err := db.Queries.UpdateSpaceMemberExit(c.Context(), repo.UpdateSpaceMemberExitParams{ + SpaceID: spaceId, + UserID: dto.MemberId, + }); err != nil { + return err + } + + err = redisx.SpaceRemoveMember(spaceId, dto.MemberId) + + if err != nil { + return err + } + + return responsex.OK(c, "已踢出该空间") +} + +// TransferSpace 转让空间 +func TransferSpace(c fiber.Ctx) error { + dto := new(types.TransferSpaceDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + spaceId := dto.SpaceId + + space, err := db.Queries.GetSpaceById(c.Context(), spaceId) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "空间不存在或已解散") + } + return err + } + + if userId != space.OwnerID { + return responsex.Error(c, "您不是该空间的拥有者") + } + + _, err = db.Queries.GetSpaceByUserIdAndSpaceId(c.Context(), repo.GetSpaceByUserIdAndSpaceIdParams{ + ID: spaceId, + UserID: dto.MemberId, + }) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "用户已不再该空间内") + } + return err + } + + // 转让 + err = db.WithTx(c.Context(), func(q *repo.Queries) error { + err2 := q.UpdateSpaceOwnerId(c.Context(), repo.UpdateSpaceOwnerIdParams{ + OwnerID: dto.MemberId, + ID: spaceId, + }) + + if err2 != nil { + return err2 + } + + err2 = q.UpdateSpaceMemberRole(c.Context(), repo.UpdateSpaceMemberRoleParams{ + SpaceID: spaceId, + Role: 0, + UserID: dto.MemberId, + }) + + if err2 != nil { + return err2 + } + + err2 = q.UpdateSpaceMemberRole(c.Context(), repo.UpdateSpaceMemberRoleParams{ + SpaceID: spaceId, + Role: 2, + UserID: userId, + }) + + if err2 != nil { + return err2 + } + + return nil + }) + + if err != nil { + return err + } + + err = redisx.SpaceAddOrUpdateMember(spaceId, dto.MemberId, 0) + if err != nil { + return err + } + + err = redisx.SpaceAddOrUpdateMember(spaceId, userId, 2) + if err != nil { + return err + } + + return responsex.OK(c, nil) +} + +func UpdateSpaceInviteCode(c fiber.Ctx) error { + dto := new(types.UpdateSpaceInviteCodeDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + spaceId := dto.SpaceId + + space, err := db.Queries.GetSpaceById(c.Context(), spaceId) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "空间不存在或已解散") + } + return err + } + + if userId != space.OwnerID { + return responsex.Error(c, "您不是该空间的拥有者") + } + + var inviteCode string + + for { + // 生成邀请码 + code := utils.GenerateCode(6) + + // 检查数据库是否存在 + + _, err := db.Queries.GetSpaceByInviteCode(c.Context(), code) + + if err != nil { + if errorx.IsNotFound(err) { + inviteCode = code + break + } else { + return err + } + } + } + + err = db.Queries.UpdateSpaceInviteCode(c.Context(), repo.UpdateSpaceInviteCodeParams{ + ID: spaceId, + InviteCode: inviteCode, + }) + + if err != nil { + return err + } + + vo := new(types.UpdateSpaceInviteCodeVO) + vo.InviteCode = inviteCode + + return responsex.OK(c, vo) +} + +// JoinSpace 加入空间 +func JoinSpace(c fiber.Ctx) error { + dto := new(types.JoinSpaceDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + space, err := db.Queries.GetSpaceByInviteCode(c.Context(), dto.InviteCode) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "邀请码无效") + } + return err + } + + if space.Status == 2 { + return responsex.Error(c, "邀请码无效") + } + + spaceMember, err := db.Queries.GetSpaceMember(c.Context(), repo.GetSpaceMemberParams{ + SpaceID: space.ID, + UserID: userId, + }) + + isNew := false + + if err != nil { + if errorx.IsNotFound(err) { + isNew = true + } else { + return err + } + } + + if isNew { + err = db.Queries.InsertSpaceMember(c.Context(), repo.InsertSpaceMemberParams{ + ID: uniqueid.NextId(), + SpaceID: space.ID, + UserID: userId, + Role: 2, + IsDefault: int16(0), + MemberName: dto.MemberName, + SortNumber: 0, + }) + } else { + if spaceMember.Status != 2 { + return responsex.Error(c, "已在该空间内,不用重新加入") + } + + err = db.Queries.UpdateSpaceMemberJoin(c.Context(), repo.UpdateSpaceMemberJoinParams{ + SpaceID: space.ID, + UserID: userId, + }) + } + + if err != nil { + return err + } + + return responsex.OK(c, nil) +} + +// EnterSpace 进入空间 +func EnterSpace(c fiber.Ctx) error { + dto := new(types.EnterSpaceDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + space, err := db.Queries.GetSpaceById(c.Context(), dto.SpaceId) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "不在该空间内") + } + return err + } + + if space.Status == 2 { + return responsex.Error(c, "不在该空间内") + } + + spaceMember, err := db.Queries.GetSpaceMember(c.Context(), repo.GetSpaceMemberParams{ + SpaceID: dto.SpaceId, + UserID: userId, + }) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.Error(c, "不在该空间内") + } + return err + } + + if spaceMember.Status == 2 { + return responsex.Error(c, "不在该空间内") + } + + err = redisx.SpaceAddOrUpdateMember(dto.SpaceId, userId, spaceMember.Role) + + if err != nil { + return err + } + + return responsex.OK(c, nil) +} + +// EnterDefaultSpace 默认进入空间,登录用 +func EnterDefaultSpace(c fiber.Ctx) error { + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + vo := new(types.EnterSpaceDefaultVO) + + spaceMember, err := db.Queries.GetSpaceMemberByDefault(c.Context(), userId) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.OK(c, vo) + } + return err + } + + err = redisx.SpaceAddOrUpdateMember(spaceMember.SpaceID, userId, spaceMember.Role) + + if err != nil { + return err + } + + vo.SpaceId = spaceMember.SpaceID + vo.Role = spaceMember.Role + + return responsex.OK(c, vo) +} + // 初始化记账分类 func initFinanceCategory(ctx context.Context, q *repo.Queries, spaceId int64, userId int64) error { categoryDefaults, err := q.ListFinanceCategoryDefault(ctx) diff --git a/code/allapp/allapp-go/api/router/middleware/space_guard.go b/code/allapp/allapp-go/api/router/middleware/space_guard.go index b05249ca..d4ab3528 100644 --- a/code/allapp/allapp-go/api/router/middleware/space_guard.go +++ b/code/allapp/allapp-go/api/router/middleware/space_guard.go @@ -51,7 +51,7 @@ func SpaceGuard() fiber.Handler { } // 3 写回 Redis(重建缓存) - _ = redisx.SpaceAddOrUpdateMember(spaceID, userID, int(role)) + _ = redisx.SpaceAddOrUpdateMember(spaceID, userID, role) c.Locals(CtxSpaceIDKey, spaceID) // 4 放行 diff --git a/code/allapp/allapp-go/api/router/router.go b/code/allapp/allapp-go/api/router/router.go index 61b7e9b0..40d068d1 100644 --- a/code/allapp/allapp-go/api/router/router.go +++ b/code/allapp/allapp-go/api/router/router.go @@ -26,11 +26,18 @@ func SetupRouters(app *fiber.App) { spaceRouter.Post("/dissolve", space.DissolveSpace) spaceRouter.Post("/name", space.UpdateSpaceName) spaceRouter.Post("/settings", space.GetSpaceSettings) + spaceRouter.Post("/members", space.ListSpaceMembers) + spaceRouter.Post("/kickout", space.KickoutSpaceMember) + spaceRouter.Post("/transfer", space.TransferSpace) + spaceRouter.Post("/inviteCode", space.UpdateSpaceInviteCode) + spaceRouter.Post("/join", space.JoinSpace) + spaceRouter.Post("/enter", space.EnterSpace) + spaceRouter.Post("/exit", space.ExitSpace) + spaceRouter.Post("/enterDefault", space.EnterDefaultSpace) spaceRouter.Post("/member/default", space.UpdateSpaceDefault) spaceRouter.Post("/member/name", space.UpdateMemberName) spaceRouter.Post("/member/sortNumber", space.UpdateSpaceSortNumber) - spaceRouter.Post("/member/exit", space.ExitSpace) // authGroup.Get("/user/profile", user.GetProfile) // spaceGroup.Get("/space/info", space.GetInfo) diff --git a/code/allapp/allapp-go/api/types/space.go b/code/allapp/allapp-go/api/types/space.go index fdc07d23..46950a7a 100644 --- a/code/allapp/allapp-go/api/types/space.go +++ b/code/allapp/allapp-go/api/types/space.go @@ -63,3 +63,66 @@ type SpaceSortNumber struct { type UpdateSpaceSortNumberDTO struct { Spaces []SpaceSortNumber `json:"spaces"` } + +type SpaceMember struct { + SpaceId int64 `json:"space_id"` + UserId int64 `json:"user_id"` + Role int16 `json:"role"` + MemberName string `json:"member_name"` + MemberStatus int16 `json:"member_status"` + MemberUpdateTime int64 `json:"member_update_time"` + JoinTime int64 `json:"join_time"` + + Avatar string `json:"avatar"` + UserStatus int16 `json:"user_status"` + LastLoginTime int64 `json:"last_login_time"` +} + +type ListSpaceMembersDTO struct { + SpaceId int64 `json:"space_id" validate:"required"` +} + +type ListSpaceMembersVO struct { + Members []SpaceMember `json:"members"` +} + +// KickoutSpaceMemberDTO 踢出空间 +type KickoutSpaceMemberDTO struct { + SpaceId int64 `json:"space_id" validate:"required"` + MemberId int64 `json:"member_id" validate:"required"` +} + +// TransferSpaceDTO 转让空间 +type TransferSpaceDTO struct { + SpaceId int64 `json:"space_id" validate:"required"` + MemberId int64 `json:"member_id" validate:"required"` +} + +type TransferSpaceVO struct { +} + +// UpdateSpaceInviteCodeDTO 更新邀请码 +type UpdateSpaceInviteCodeDTO struct { + SpaceId int64 `json:"space_id" validate:"required"` +} + +type UpdateSpaceInviteCodeVO struct { + InviteCode string `json:"invite_code"` +} + +// JoinSpaceDTO 加入空间 +type JoinSpaceDTO struct { + InviteCode string `json:"invite_code" validate:"required"` + MemberName string `json:"member_name" validate:"required"` +} + +// EnterSpaceDTO 进入空间 +type EnterSpaceDTO struct { + SpaceId int64 `json:"space_id" validate:"required"` +} + +// EnterSpaceDefaultVO 进入空间 +type EnterSpaceDefaultVO struct { + SpaceId int64 `json:"space_id"` + Role int16 `json:"role"` +} diff --git a/code/allapp/allapp-go/db/queries/space.sql b/code/allapp/allapp-go/db/queries/space.sql index 0ae0731d..668e5689 100644 --- a/code/allapp/allapp-go/db/queries/space.sql +++ b/code/allapp/allapp-go/db/queries/space.sql @@ -68,3 +68,10 @@ ORDER BY sm.is_default DESC, sm.sort_number ASC; -- name: UpdateSpaceName :exec UPDATE space SET name = $1 WHERE id = $2; + +-- name: UpdateSpaceOwnerId :exec +UPDATE space set owner_id = $1,update_time = NOW() WHERE id = $2; + +-- name: UpdateSpaceInviteCode :exec +UPDATE space set invite_code = $1,update_time = NOW() WHERE id = $2; + diff --git a/code/allapp/allapp-go/db/queries/space_member.sql b/code/allapp/allapp-go/db/queries/space_member.sql index 258e0bf9..093ed85f 100644 --- a/code/allapp/allapp-go/db/queries/space_member.sql +++ b/code/allapp/allapp-go/db/queries/space_member.sql @@ -43,6 +43,57 @@ WHERE user_id = $2 -- 退出空间 -- name: UpdateSpaceMemberExit :exec UPDATE space_member -SET status = 2 AND exit_time = NOW() +SET status = 2, + exit_time = NOW() WHERE space_id = $1 - AND user_id = $2; \ No newline at end of file + AND user_id = $2; + +-- name: UpdateSpaceMemberJoin :exec +UPDATE space_member +SET status = 0, + join_time = NOW() +WHERE space_id = $1 + AND user_id = $2; + +-- 获取空间下的成员列表 +-- name: ListSpaceMembers :many +SELECT + sm.space_id, + sm.user_id, + sm.role, + sm.status AS member_status, + sm.member_name, + sm.update_time AS member_update_time, + sm.join_time, + + u.avatar, + u.status AS user_status, + u.last_login_time +FROM space_member sm + LEFT JOIN "user" u + ON sm.user_id = u.id + LEFT JOIN space s + ON sm.space_id = s.id +WHERE sm.space_id = $1 + AND sm.status != 2 + AND s.status != 2 +ORDER BY sm.role ASC, sm.join_time ASC; + +-- name: GetSpaceMember :one +SELECT * FROM space_member WHERE space_id = $1 AND user_id = $2; + +-- 修改角色 +-- name: UpdateSpaceMemberRole :exec +UPDATE space_member SET role = $1,update_time = NOW() WHERE space_id = $2 AND user_id = $3; + +-- 获取该用户的默认空间 +-- name: GetSpaceMemberByDefault :one +SELECT + sm.* +FROM space_member sm + LEFT JOIN space s + ON sm.space_id = s.id +WHERE sm.status != 2 + AND s.status != 2 + AND sm.is_default = 1 + AND sm.user_id = $1; diff --git a/code/allapp/allapp-go/db/queries/user.sql b/code/allapp/allapp-go/db/queries/user.sql index 1b37fd29..ece004bb 100644 --- a/code/allapp/allapp-go/db/queries/user.sql +++ b/code/allapp/allapp-go/db/queries/user.sql @@ -12,6 +12,9 @@ WHERE o.openid = $1 AND o.type = $2 LIMIT 1; +-- name: GetUserById :one +SELECT * FROM "user" WHERE id = $1; + -- name: InsertUser :exec INSERT INTO "user" ( id, diff --git a/code/allapp/allapp-go/db/repo/space.sql.go b/code/allapp/allapp-go/db/repo/space.sql.go index 4c9cbed8..c7baeb93 100644 --- a/code/allapp/allapp-go/db/repo/space.sql.go +++ b/code/allapp/allapp-go/db/repo/space.sql.go @@ -211,6 +211,20 @@ func (q *Queries) ListSpacesByUserId(ctx context.Context, userID int64) ([]ListS return items, nil } +const updateSpaceInviteCode = `-- name: UpdateSpaceInviteCode :exec +UPDATE space set invite_code = $1,update_time = NOW() WHERE id = $2 +` + +type UpdateSpaceInviteCodeParams struct { + InviteCode string + ID int64 +} + +func (q *Queries) UpdateSpaceInviteCode(ctx context.Context, arg UpdateSpaceInviteCodeParams) error { + _, err := q.db.Exec(ctx, updateSpaceInviteCode, arg.InviteCode, arg.ID) + return err +} + const updateSpaceName = `-- name: UpdateSpaceName :exec UPDATE space SET name = $1 WHERE id = $2 @@ -225,3 +239,17 @@ func (q *Queries) UpdateSpaceName(ctx context.Context, arg UpdateSpaceNameParams _, err := q.db.Exec(ctx, updateSpaceName, arg.Name, arg.ID) return err } + +const updateSpaceOwnerId = `-- name: UpdateSpaceOwnerId :exec +UPDATE space set owner_id = $1,update_time = NOW() WHERE id = $2 +` + +type UpdateSpaceOwnerIdParams struct { + OwnerID int64 + ID int64 +} + +func (q *Queries) UpdateSpaceOwnerId(ctx context.Context, arg UpdateSpaceOwnerIdParams) error { + _, err := q.db.Exec(ctx, updateSpaceOwnerId, arg.OwnerID, arg.ID) + return err +} diff --git a/code/allapp/allapp-go/db/repo/space_member.sql.go b/code/allapp/allapp-go/db/repo/space_member.sql.go index 3eccdd99..0bf8102c 100644 --- a/code/allapp/allapp-go/db/repo/space_member.sql.go +++ b/code/allapp/allapp-go/db/repo/space_member.sql.go @@ -7,6 +7,8 @@ package repo import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) const countUserSpaces = `-- name: CountUserSpaces :one @@ -23,6 +25,68 @@ func (q *Queries) CountUserSpaces(ctx context.Context, userID int64) (int64, err return count, err } +const getSpaceMember = `-- name: GetSpaceMember :one +SELECT id, space_id, user_id, role, is_default, sort_number, member_name, status, disabled_time, exit_time, join_time, update_time FROM space_member WHERE space_id = $1 AND user_id = $2 +` + +type GetSpaceMemberParams struct { + SpaceID int64 + UserID int64 +} + +func (q *Queries) GetSpaceMember(ctx context.Context, arg GetSpaceMemberParams) (SpaceMember, error) { + row := q.db.QueryRow(ctx, getSpaceMember, arg.SpaceID, arg.UserID) + var i SpaceMember + err := row.Scan( + &i.ID, + &i.SpaceID, + &i.UserID, + &i.Role, + &i.IsDefault, + &i.SortNumber, + &i.MemberName, + &i.Status, + &i.DisabledTime, + &i.ExitTime, + &i.JoinTime, + &i.UpdateTime, + ) + return i, err +} + +const getSpaceMemberByDefault = `-- name: GetSpaceMemberByDefault :one +SELECT + sm.id, sm.space_id, sm.user_id, sm.role, sm.is_default, sm.sort_number, sm.member_name, sm.status, sm.disabled_time, sm.exit_time, sm.join_time, sm.update_time +FROM space_member sm + LEFT JOIN space s + ON sm.space_id = s.id +WHERE sm.status != 2 + AND s.status != 2 + AND sm.is_default = 1 + AND sm.user_id = $1 +` + +// 获取该用户的默认空间 +func (q *Queries) GetSpaceMemberByDefault(ctx context.Context, userID int64) (SpaceMember, error) { + row := q.db.QueryRow(ctx, getSpaceMemberByDefault, userID) + var i SpaceMember + err := row.Scan( + &i.ID, + &i.SpaceID, + &i.UserID, + &i.Role, + &i.IsDefault, + &i.SortNumber, + &i.MemberName, + &i.Status, + &i.DisabledTime, + &i.ExitTime, + &i.JoinTime, + &i.UpdateTime, + ) + return i, err +} + const getUserSpaceRole = `-- name: GetUserSpaceRole :one SELECT role FROM space_member @@ -72,6 +136,75 @@ func (q *Queries) InsertSpaceMember(ctx context.Context, arg InsertSpaceMemberPa return err } +const listSpaceMembers = `-- name: ListSpaceMembers :many +SELECT + sm.space_id, + sm.user_id, + sm.role, + sm.status AS member_status, + sm.member_name, + sm.update_time AS member_update_time, + sm.join_time, + + u.avatar, + u.status AS user_status, + u.last_login_time +FROM space_member sm + LEFT JOIN "user" u + ON sm.user_id = u.id + LEFT JOIN space s + ON sm.space_id = s.id +WHERE sm.space_id = $1 + AND sm.status != 2 + AND s.status != 2 +ORDER BY sm.role ASC, sm.join_time ASC +` + +type ListSpaceMembersRow struct { + SpaceID int64 + UserID int64 + Role int16 + MemberStatus int16 + MemberName string + MemberUpdateTime pgtype.Timestamptz + JoinTime pgtype.Timestamptz + Avatar pgtype.Text + UserStatus pgtype.Int2 + LastLoginTime pgtype.Timestamptz +} + +// 获取空间下的成员列表 +func (q *Queries) ListSpaceMembers(ctx context.Context, spaceID int64) ([]ListSpaceMembersRow, error) { + rows, err := q.db.Query(ctx, listSpaceMembers, spaceID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListSpaceMembersRow + for rows.Next() { + var i ListSpaceMembersRow + if err := rows.Scan( + &i.SpaceID, + &i.UserID, + &i.Role, + &i.MemberStatus, + &i.MemberName, + &i.MemberUpdateTime, + &i.JoinTime, + &i.Avatar, + &i.UserStatus, + &i.LastLoginTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateOtherSpacesIsDefault = `-- name: UpdateOtherSpacesIsDefault :exec UPDATE space_member SET is_default = $1 @@ -110,7 +243,8 @@ func (q *Queries) UpdateSpaceIsDefault(ctx context.Context, arg UpdateSpaceIsDef const updateSpaceMemberExit = `-- name: UpdateSpaceMemberExit :exec UPDATE space_member -SET status = 2 AND exit_time = NOW() +SET status = 2, + exit_time = NOW() WHERE space_id = $1 AND user_id = $2 ` @@ -126,6 +260,24 @@ func (q *Queries) UpdateSpaceMemberExit(ctx context.Context, arg UpdateSpaceMemb return err } +const updateSpaceMemberJoin = `-- name: UpdateSpaceMemberJoin :exec +UPDATE space_member +SET status = 0, + join_time = NOW() +WHERE space_id = $1 + AND user_id = $2 +` + +type UpdateSpaceMemberJoinParams struct { + SpaceID int64 + UserID int64 +} + +func (q *Queries) UpdateSpaceMemberJoin(ctx context.Context, arg UpdateSpaceMemberJoinParams) error { + _, err := q.db.Exec(ctx, updateSpaceMemberJoin, arg.SpaceID, arg.UserID) + return err +} + const updateSpaceMemberName = `-- name: UpdateSpaceMemberName :exec UPDATE space_member SET member_name = $1 WHERE space_id = $2 AND user_id = $3 @@ -142,3 +294,19 @@ func (q *Queries) UpdateSpaceMemberName(ctx context.Context, arg UpdateSpaceMemb _, err := q.db.Exec(ctx, updateSpaceMemberName, arg.MemberName, arg.SpaceID, arg.UserID) return err } + +const updateSpaceMemberRole = `-- name: UpdateSpaceMemberRole :exec +UPDATE space_member SET role = $1,update_time = NOW() WHERE space_id = $2 AND user_id = $3 +` + +type UpdateSpaceMemberRoleParams struct { + Role int16 + SpaceID int64 + UserID int64 +} + +// 修改角色 +func (q *Queries) UpdateSpaceMemberRole(ctx context.Context, arg UpdateSpaceMemberRoleParams) error { + _, err := q.db.Exec(ctx, updateSpaceMemberRole, arg.Role, arg.SpaceID, arg.UserID) + return err +} diff --git a/code/allapp/allapp-go/db/repo/user.sql.go b/code/allapp/allapp-go/db/repo/user.sql.go index b354b693..fa3ee73d 100644 --- a/code/allapp/allapp-go/db/repo/user.sql.go +++ b/code/allapp/allapp-go/db/repo/user.sql.go @@ -52,6 +52,25 @@ func (q *Queries) FindUserByOpenID(ctx context.Context, arg FindUserByOpenIDPara return i, err } +const getUserById = `-- name: GetUserById :one +SELECT id, nickname, avatar, status, last_login_time, create_time, update_time FROM "user" WHERE id = $1 +` + +func (q *Queries) GetUserById(ctx context.Context, id int64) (User, error) { + row := q.db.QueryRow(ctx, getUserById, id) + var i User + err := row.Scan( + &i.ID, + &i.Nickname, + &i.Avatar, + &i.Status, + &i.LastLoginTime, + &i.CreateTime, + &i.UpdateTime, + ) + return i, err +} + const insertUser = `-- name: InsertUser :exec INSERT INTO "user" ( id, diff --git a/code/allapp/allapp-go/utils/pgtypex/index.go b/code/allapp/allapp-go/utils/pgtypex/index.go index 012fd682..0dcb530e 100644 --- a/code/allapp/allapp-go/utils/pgtypex/index.go +++ b/code/allapp/allapp-go/utils/pgtypex/index.go @@ -159,3 +159,17 @@ func TimePtrToPgDate(t *time.Time) pgtype.Date { Valid: true, } } + +func TextToString(d pgtype.Text) string { + if !d.Valid { + return "" + } + return d.String +} + +func Int2ToInt16(v pgtype.Int2) int16 { + if !v.Valid { + return 0 + } + return v.Int16 +} diff --git a/code/allapp/allapp-go/utils/redisx/helper.go b/code/allapp/allapp-go/utils/redisx/helper.go index fb8b655e..c6b7889a 100644 --- a/code/allapp/allapp-go/utils/redisx/helper.go +++ b/code/allapp/allapp-go/utils/redisx/helper.go @@ -15,7 +15,7 @@ func spaceMemberKey(spaceID int64) string { } // SpaceAddOrUpdateMember 添加 / 更新成员角色 -func SpaceAddOrUpdateMember(spaceID int64, userID int64, role int) error { +func SpaceAddOrUpdateMember(spaceID int64, userID int64, role int16) error { key := spaceMemberKey(spaceID) return Client.HSet( diff --git a/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx b/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx index c957c68d..4a2bb641 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx @@ -100,7 +100,7 @@ export default function TabsLayout() { }} > - + diff --git a/code/allapp/allapp-rn/src/app/(tabs)/user/index.tsx b/code/allapp/allapp-rn/src/app/(tabs)/user/index.tsx index 84cecaa6..85f1ca83 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/user/index.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/user/index.tsx @@ -10,7 +10,7 @@ import ListItem from './components/ListItem' export default function User() { const userStore = useUserStore() - console.log(userStore.userInfo) + return ( diff --git a/code/allapp/allapp-rn/src/app/space/manage/index.tsx b/code/allapp/allapp-rn/src/app/space/manage/index.tsx index 9db1138e..48b39d08 100644 --- a/code/allapp/allapp-rn/src/app/space/manage/index.tsx +++ b/code/allapp/allapp-rn/src/app/space/manage/index.tsx @@ -1,12 +1,12 @@ import type { UserSpace } from '@/types/space' import { useFocusEffect, useRouter } from 'expo-router' import { useCallback, useEffect, useRef, useState } from 'react' -import { Pressable, Text, View } from 'react-native' +import { ActivityIndicator, Pressable, RefreshControl, Text, View } from 'react-native' import { KeyboardAvoidingView } from 'react-native-keyboard-controller' import Animated, { useAnimatedRef } from 'react-native-reanimated' import Sortable from 'react-native-sortables' import { toast } from 'sonner-native' -import { AButton, ADivider, AInput, LucideIcon } from '@/components/common/ao' +import { AButton, ADivider, AInput, AModal, LucideIcon } from '@/components/common/ao' import EmptyState from '@/components/common/EmptyStatus' import CustomHeader from '@/components/custom/Header' import { @@ -17,11 +17,13 @@ import { ActionsheetDragIndicatorWrapper, } from '@/components/ui/actionsheet' import { Badge, BadgeText } from '@/components/ui/badge' -import { createSpaceApi, listUserSpacesApi, updateSpaceSortNumberApi } from '@/request/api/space/index' +import { createSpaceApi, joinSpaceApi, listUserSpacesApi, updateSpaceSortNumberApi } from '@/request/api/space/index' +import { useSpaceStore } from '@/store/space' import { useUserStore } from '@/store/user' export default function AuthSpace() { const userStore = useUserStore() + const spaceStore = useSpaceStore() const router = useRouter() // scrollView 相关 @@ -29,6 +31,9 @@ export default function AuthSpace() { const [scrollHeight, setScrollHeight] = useState(0) const ITEM_HEIGHT = 64 + const [loading, setLoading] = useState(false) + const [refreshLoading, setRefreshLoading] = useState(false) + // 空间列表 const [spaces, setSpaces] = useState([]) @@ -51,11 +56,25 @@ export default function AuthSpace() { } }, [createOpen]) + // 加入 + const [showJoin, setShowJoin] = useState(false) + const [joinLoading, setJoinLoading] = useState(false) + const [joinForm, setJoinForm] = useState({ + invite_code: '', + }) + // 获取空间列表 - function listUserSpaces() { + function listUserSpaces(loading = true) { + if (loading) { + setLoading(true) + } + listUserSpacesApi().then((res) => { if (res.isSuccess) { setSpaces(res.data.spaces) + if (loading) { + setLoading(false) + } } }) } @@ -105,6 +124,14 @@ export default function AuthSpace() { ) } + { + item.space_id === spaceStore?.currentSpace?.space_id && ( + + 当前空间 + + ) + } + @@ -161,7 +188,7 @@ export default function AuthSpace() { return ( - + @@ -182,6 +209,9 @@ export default function AuthSpace() { className="flex-1 rounded-full" size="sm" leftIcon={} + onPress={() => { + setShowJoin(true) + }} > 加入其他空间 @@ -196,52 +226,69 @@ export default function AuthSpace() { onLayout={(e) => { setScrollHeight(e.nativeEvent.layout.height) }} + refreshControl={( + { + setRefreshLoading(true) + await listUserSpaces(false) + setRefreshLoading(false) + }} + /> + )} > - {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) => { - const newData = dragData.data - if (JSON.stringify(spaces) !== JSON.stringify(newData)) { - const updateData = newData.map((item, index) => { - return { - space_id: item.space_id, - sort_number: index, - } - }) + { + 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) => { + const newData = dragData.data + if (JSON.stringify(spaces) !== JSON.stringify(newData)) { + const updateData = newData.map((item, index) => { + return { + space_id: item.space_id, + sort_number: index, + } + }) - updateSpaceSortNumberApi({ - spaces: updateData, - }).then((res) => { - if (res.isSuccess) { - // 更新成功 + updateSpaceSortNumberApi({ + spaces: updateData, + }).then((res) => { + if (res.isSuccess) { + // 更新成功 + } + }) } - }) - } - }} - /> - )} + }} + /> + ) + } + @@ -323,6 +370,84 @@ export default function AuthSpace() { + + { + setShowJoin(false) + + setTimeout(() => { + setJoinForm({ + invite_code: '', + }) + }, 300) + }} + body={( + + { + setJoinForm({ + invite_code: text, + }) + }} + /> + + )} + footer={( + + { + if (joinForm.invite_code === '') { + toast.warning('邀请码不能为空') + return + } + setJoinLoading(true) + + try { + await joinSpaceApi({ + invite_code: joinForm.invite_code, + member_name: userStore.userInfo.nickname, + }).then((res) => { + if (res.isSuccess) { + toast.success('加入成功') + setShowJoin(false) + + setTimeout(() => { + setJoinForm({ + invite_code: '', + }) + + listUserSpaces() + }, 300) + } + }) + } + finally { + setJoinLoading(false) + } + }} + + > + 加入 + + + { + toast.warning('功能待实现,敬请期待!') + }} + > + 扫码加入 + + + )} + /> ) } diff --git a/code/allapp/allapp-rn/src/app/space/manage/sub-page/invite/index.tsx b/code/allapp/allapp-rn/src/app/space/manage/sub-page/invite/index.tsx index 5981ba3c..6e146b89 100644 --- a/code/allapp/allapp-rn/src/app/space/manage/sub-page/invite/index.tsx +++ b/code/allapp/allapp-rn/src/app/space/manage/sub-page/invite/index.tsx @@ -1,7 +1,9 @@ +import type { UserSpace } from '@/types/space' import * as Clipboard from 'expo-clipboard' -import { useLocalSearchParams } from 'expo-router' -import { useEffect, useState } from 'react' +import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router' +import { useCallback, useEffect, useState } from 'react' import { + ActivityIndicator, Modal, Pressable, Text, @@ -12,69 +14,159 @@ import QRCode from 'react-native-qrcode-svg' import { toast } from 'sonner-native' import { AButton, ADivider, LucideIcon } from '@/components/common/ao' import CustomHeader from '@/components/custom/Header' +import { getSpaceSettingsApi, refreshInviteCodeApi } from '@/request/api/space' +import { useUserStore } from '@/store/user' export default function Invite() { - const params = useLocalSearchParams<{ invite_code: string }>() - const { invite_code } = params + const router = useRouter() + const userStore = useUserStore() + + const params = useLocalSearchParams<{ space_id: string }>() + const { space_id } = params + + const [loading, setLoading] = useState(false) + const [refreshLoading, setRefreshLoading] = useState(false) + + const [space, setSpace] = useState(null) const [qrcode, setQrcode] = useState('space:') const [previewVisible, setPreviewVisible] = useState(false) async function copyInviteCode() { - await Clipboard.setStringAsync(invite_code) - toast.success('邀请码已复制') + if (space) { + await Clipboard.setStringAsync(space.invite_code) + toast.success('邀请码已复制') + } + else { + toast.error('邀请码无效') + } + } + + async function getSpace() { + setLoading(true) + await getSpaceSettingsApi({ + space_id: Number(space_id), + }).then((res) => { + if (res.isSuccess) { + setSpace(res.data) + setLoading(false) + } + }) } useEffect(() => { - setQrcode(`space:${invite_code}`) - console.log(`space:${invite_code}`) - }, [invite_code]) + if (space) { + setQrcode(`space:${space.invite_code}`) + } + }, [space]) + + useFocusEffect( + useCallback(() => { + getSpace() + }, []), + ) + + function refreshInviteCode() { + if (!space) { + toast.error('空间无效') + router.back() + return + } + + setRefreshLoading(true) + + refreshInviteCodeApi({ + space_id: space.space_id, + }).then((res) => { + if (res.isSuccess) { + setSpace(prev => + prev + ? { + ...prev, + invite_code: res.data.invite_code, + } + : prev, + ) + + setRefreshLoading(false) + + toast.success('刷新成功') + } + }) + } return ( - + { + loading ? ( + + + + ) : ( + - {/* 邀请码卡片 */} - - - 复制邀请码邀请家庭成员加入 - + {/* 邀请码卡片 */} + + + 复制邀请码邀请家庭成员加入 + - - {invite_code} - + + {space?.invite_code} + - - } - > - 复制邀请码 - - + + + } + > + 复制邀请码 + - - - + { + userStore.userInfo.id === space?.owner_id && ( + + } + loading={refreshLoading} + onPress={refreshInviteCode} + > + 刷新 + + ) + } - {/* 二维码卡片 */} - setPreviewVisible(true)} - className="bg-white rounded-2xl py-8 items-center" - > - - + + - + + + + + {/* 二维码卡片 */} + setPreviewVisible(true)} + className="bg-white rounded-2xl py-8 items-center" + > + + + + + ) + } {/* 全屏预览(无动画) */} - - - ) -} diff --git a/code/allapp/allapp-rn/src/app/space/manage/sub-page/member/index.tsx b/code/allapp/allapp-rn/src/app/space/manage/sub-page/member/index.tsx new file mode 100644 index 00000000..a4a786f2 --- /dev/null +++ b/code/allapp/allapp-rn/src/app/space/manage/sub-page/member/index.tsx @@ -0,0 +1,379 @@ +import type { SpaceMember, UserSpace } from '@/types/space' +import { useFocusEffect, useLocalSearchParams } from 'expo-router' +import { useCallback, useState } from 'react' +import { ActivityIndicator, RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native' +import { toast } from 'sonner-native' +import { ADivider, AModal, LucideIcon } from '@/components/common/ao' +import EmptyState from '@/components/common/EmptyStatus' +import CustomHeader from '@/components/custom/Header' +import { + Actionsheet, + ActionsheetBackdrop, + ActionsheetContent, + ActionsheetDragIndicator, + ActionsheetDragIndicatorWrapper, + ActionsheetItem, + ActionsheetItemText, +} from '@/components/ui/actionsheet' +import { + Avatar, + AvatarFallbackText, + AvatarImage, +} from '@/components/ui/avatar' +import { Badge, BadgeText } from '@/components/ui/badge' +import { getSpaceSettingsApi, kickoutSpaceMemberApi, listSpaceMembersApi, transferSpaceApi } from '@/request/api/space' +import { useUserStore } from '@/store/user' +import { formatRelativeTime } from '@/utils/time' + +export default function () { + const userStore = useUserStore() + + const params = useLocalSearchParams<{ space_id: string }>() + const { space_id } = params + + const [space, setSpace] = useState(null) + const [members, setMembers] = useState([]) + + const [loading, setLoading] = useState(true) + const [refreshLoading, setRefreshLoading] = useState(false) + + const [showAction, setShowAction] = useState(false) + const [currentMember, setCurrentMember] = useState(null) + + // 踢出空间 + const [showKickout, setShowKickout] = useState(false) + // 转让空间 + const [showTransfer, setShowTransfer] = useState(false) + + async function listSpaceMembers() { + await listSpaceMembersApi({ + space_id: Number(space_id), + }).then((res) => { + if (res.isSuccess) { + setMembers(res.data.members) + } + }) + } + + async function getSpace() { + await getSpaceSettingsApi({ + space_id: Number(space_id), + }).then((res) => { + if (res.isSuccess) { + setSpace(res.data) + } + }) + } + + async function loadData(loading = true) { + if (loading) { + setLoading(true) + } + + await Promise.all([ + getSpace(), + listSpaceMembers(), + ]) + + if (loading) { + setLoading(false) + } + } + + useFocusEffect( + useCallback(() => { + if (space_id) { + loadData() + } + }, []), + ) + + return ( + + + + + { + loading + ? ( + + + + ) + : space === null + ? ( + + ) + : ( + <> + + + {space.name || ''} + + + + {space.is_default === 1 && ( + + 默认 + + )} + + + + + + + 空间成员 + + + {members.length} + + + + { + setRefreshLoading(true) + + await loadData(false) + + setRefreshLoading(false) + }} + /> + )} + > + { + members.map((item) => { + return ( + { + setCurrentMember(item) + setShowAction(true) + }} + > + + + {item.member_name} + + + + {item.member_name} + + + + + {formatRelativeTime(item.last_login_time)} + + + + + + + + { + item.role === 0 && ( + + 创建人 + + ) + } + + { + item.member_status === 1 && ( + + 禁用 + + ) + } + + + ) + }) + } + + + + ) + } + + + { + setShowAction(false) + setTimeout(() => { + setCurrentMember(null) + }, 300) + }} + > + + + + + + + + { + toast.warning('功能待开发,敬请期待!') + }} + > + + 查看资料 + + + { + currentMember?.user_id !== userStore.userInfo.id && ( + { + toast.warning('功能待开发,敬请期待!') + }} + > + + 修改备注 + + ) + } + + { + space?.owner_id === userStore.userInfo.id + && currentMember?.user_id !== userStore.userInfo.id && ( + <> + + + { + setShowTransfer(true) + }} + > + + 转让空间 + + + { + toast.warning('功能待开发,敬请期待!') + }} + > + + 禁用 + + + + + { + setShowKickout(true) + }} + > + + 踢出空间 + + + ) + } + + + + { + setShowKickout(false) + }} + onClose={() => { + setShowKickout(false) + }} + onConfirm={async () => { + if (!currentMember) { + toast.warning('选中成员无效') + return + } + + await kickoutSpaceMemberApi({ + space_id: currentMember.space_id, + member_id: currentMember.user_id, + }).then((res) => { + if (res.isSuccess) { + toast.success('已将该用户踢出空间') + + setShowKickout(false) + + setShowAction(false) + + setTimeout(() => { + setCurrentMember(null) + + loadData() + }, 300) + } + }) + }} + /> + + { + setShowTransfer(false) + }} + onClose={() => { + setShowTransfer(false) + }} + onConfirm={async () => { + if (!currentMember) { + toast.warning('选中成员无效') + return + } + + await transferSpaceApi({ + space_id: currentMember.space_id, + member_id: currentMember.user_id, + }).then((res) => { + if (res.isSuccess) { + toast.success('已成功转让空间') + + setSpace((val) => { + if (!val) + return val + + return { + ...val, + owner_id: currentMember.user_id, + } + }) + + setShowTransfer(false) + + setShowAction(false) + + setTimeout(() => { + setCurrentMember(null) + + loadData() + }, 300) + } + }) + }} + /> + + ) +} diff --git a/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings/index.tsx b/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings/index.tsx index 49f6632f..41e02044 100644 --- a/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings/index.tsx +++ b/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings/index.tsx @@ -1,12 +1,12 @@ import type { UserSpace } from '@/types/space' -import { router, useLocalSearchParams } from 'expo-router' -import { useEffect, useRef, useState } from 'react' -import { Text, TouchableOpacity, View } from 'react-native' +import { router, useFocusEffect, useLocalSearchParams } from 'expo-router' +import { useCallback, useEffect, useRef, useState } from 'react' +import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native' import { KeyboardAvoidingView } from 'react-native-keyboard-controller' import { toast } from 'sonner-native' import { AButton, AInput, AModal, ASwitch, LucideIcon } from '@/components/common/ao' -import EmptyState from '@/components/common/EmptyStatus' import CustomHeader from '@/components/custom/Header' + import { Actionsheet, ActionsheetBackdrop, @@ -14,7 +14,8 @@ import { ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper, } from '@/components/ui/actionsheet' -import { dissolveSpaceApi, exitSpaceApi, getSpaceSettingsApi, updateSpaceDefaultApi, updateSpaceMemberNameApi, updateSpaceNameApi } from '@/request/api/space' +import { dissolveSpaceApi, enterSpaceApi, exitSpaceApi, getSpaceSettingsApi, updateSpaceDefaultApi, updateSpaceMemberNameApi, updateSpaceNameApi } from '@/request/api/space' +import { useSpaceStore } from '@/store/space' import { useUserStore } from '@/store/user' interface ItemProps { @@ -60,6 +61,7 @@ function Item({ export default function Settings() { const userStore = useUserStore() + const spaceStore = useSpaceStore() const params = useLocalSearchParams<{ spaceId: string }>() const { spaceId } = params @@ -100,21 +102,19 @@ export default function Settings() { } }, [showName]) - useEffect(() => { - if (!spaceId) { - toast.warning('空间无效') - router.back() - } - else { - getSpaceSettingsApi({ - space_id: Number(spaceId), - }).then((res) => { - if (res.isSuccess) { - setSpace(res.data) - } - }) - } - }, [spaceId]) + useFocusEffect( + useCallback(() => { + if (spaceId) { + getSpaceSettingsApi({ + space_id: Number(spaceId), + }).then((res) => { + if (res.isSuccess) { + setSpace(res.data) + } + }) + } + }, []), + ) useEffect(() => { if (space) { @@ -131,10 +131,41 @@ export default function Settings() { { !space ? ( - + + + ) : ( + + { + const toastId = toast.loading('正在进入中 ...', { + duration: Infinity, + }) + + try { + enterSpaceApi({ + space_id: space.space_id, + }).then((res) => { + if (res.isSuccess) { + toast.success('进入成功') + spaceStore.setCurrentSpace({ + space_id: space.space_id, + role: space.role, + }) + router.navigate('/money') + } + }) + } + finally { + toast.dismiss(toastId) + } + }} + /> + 常用设置 @@ -175,7 +206,7 @@ export default function Settings() { { + router.navigate({ + pathname: '/space/manage/sub-page/member', + params: { + space_id: space.space_id, + }, + }) + }} /> ) } @@ -432,7 +471,7 @@ export default function Settings() { {/* 解散 */} { setShowDissolve(false) diff --git a/code/allapp/allapp-rn/src/app/system/settings/index.tsx b/code/allapp/allapp-rn/src/app/system/settings/index.tsx index 64a8b348..5d43da4d 100644 --- a/code/allapp/allapp-rn/src/app/system/settings/index.tsx +++ b/code/allapp/allapp-rn/src/app/system/settings/index.tsx @@ -1,7 +1,6 @@ import { useRouter } from 'expo-router' import { useState } from 'react' import { Text, TouchableOpacity, View } from 'react-native' -import { AModal } from '@/components/common/ao' import CustomHeader from '@/components/custom/Header' import { useAppStore } from '@/store/app' @@ -14,7 +13,7 @@ export default function Share() { function exitLogin() { setShowExitModal(false) - appStore.reset() + // appStore.reset() router.replace('/auth') } diff --git a/code/allapp/allapp-rn/src/app/system/share/components/JoinSpace.tsx b/code/allapp/allapp-rn/src/app/system/share/components/JoinSpace.tsx deleted file mode 100644 index dffa0523..00000000 --- a/code/allapp/allapp-rn/src/app/system/share/components/JoinSpace.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type { AFormRef } from '@/components/common/ao' -import { useRef, useState } from 'react' -import { Text, View } from 'react-native' -import { AButton, ADivider, AForm, AFormItem, AInput } from '@/components/common/ao' -import { Link, LinkText } from '@/components/ui/link' - -interface JoinSpaceProps { - onBack?: () => void - onJoin?: (code: string) => void -} - -export default function JoinSpace({ - onBack, - onJoin, -}: JoinSpaceProps) { - const formRef = useRef(null) - const [form, setForm] = useState({ - code: '', - }) - return ( - - - - setForm({ - ...form, - code: text, - })} - > - - - - - { - if (form.code === '') { - // - } - else { - onJoin?.(form.code) - } - }} - > - 加入空间 - - - - - - - 或 - - - - - - 扫码加入 - - { - onBack?.() - }} - > - 返回 - - - ) -} diff --git a/code/allapp/allapp-rn/src/app/system/share/components/SpaceHome.tsx b/code/allapp/allapp-rn/src/app/system/share/components/SpaceHome.tsx deleted file mode 100644 index 18a1d7e7..00000000 --- a/code/allapp/allapp-rn/src/app/system/share/components/SpaceHome.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Text, View } from 'react-native' -import { AButton, ADivider, LucideIcon } from '@/components/common/ao' - -interface SpaceHomeProps { - onCreate?: () => void - onJoin?: () => void -} - -export default function SpaceHome({ - onCreate, - onJoin, -}: SpaceHomeProps) { - return ( - - - - 创建共享空间 - 创建一个专属空间,邀请朋友加入,共享文件、聊天和协作 - onCreate?.()} - > - 创建我的空间 - - - - - - onJoin?.()} - > - 加入已有空间 - - - ) -} diff --git a/code/allapp/allapp-rn/src/app/system/share/components/SpaceInvite.tsx b/code/allapp/allapp-rn/src/app/system/share/components/SpaceInvite.tsx deleted file mode 100644 index d1e08904..00000000 --- a/code/allapp/allapp-rn/src/app/system/share/components/SpaceInvite.tsx +++ /dev/null @@ -1,333 +0,0 @@ -import type { SpaceMember } from '@/types/space' -import * as Clipboard from 'expo-clipboard' -import { useState } from 'react' -import { FlatList, TouchableHighlight, TouchableOpacity, View } from 'react-native' -import { - Menu, - MenuOption, - MenuOptions, - MenuTrigger, - renderers, -} from 'react-native-popup-menu' -import QRCode from 'react-native-qrcode-svg' -import { AAvatar, AButton, ADivider, LucideIcon } from '@/components/common/ao' -import { AModal } from '@/components/common/ao/index' -import { - Accordion, - AccordionContent, - AccordionHeader, - AccordionIcon, - AccordionItem, - AccordionTitleText, - AccordionTrigger, -} from '@/components/ui/accordion' - -import { Badge, BadgeText } from '@/components/ui/badge' -import { Heading } from '@/components/ui/heading' -import { HStack } from '@/components/ui/hstack' -import { ChevronDownIcon, ChevronUpIcon } from '@/components/ui/icon' -import { Text } from '@/components/ui/text' -import { VStack } from '@/components/ui/vstack' -import { dissolveSpaceApi, exitSpaceApi, removeSpaceMemberApi } from '@/request/api/space/index' -import { useUserStore } from '@/store/user' -import { formatRelativeTime } from '@/utils/time' -import { toast } from '@/utils/toast' - -type ExitAction = 'exit' | 'remove' | 'dissolve' - -interface ExitParams { - type: ExitAction - userId?: number -} - -interface SpaceInviteProps { - userSpace: UserSpace | null - spaceUsers: SpaceMember[] - onExit?: (params: ExitParams) => void -} - -export default function SpaceInvite({ - userSpace, - spaceUsers, - onExit, -}: SpaceInviteProps) { - const userStore = useUserStore() - - // 踢出 退出 解散 - const [modal, setModal] = useState<{ - open: boolean - title: string - type: ExitAction - userId?: number - }>({ - open: false, - title: '', - type: 'dissolve', - }) - - const [accordion, setAccordion] = useState(['a']) - - async function copyInviteCode() { - await Clipboard.setStringAsync('hello world') - - toast.show({ - title: '复制成功', - }) - } - - function handleModalConfirm() { - if (userSpace === null) { - return - } - if (modal.type === 'exit') { - // 退出 - exitSpaceApi({ - space_id: userSpace.id, - }).catch((err) => { - console.log(err) - }) - } - else if (modal.type === 'remove') { - if (!modal.userId) { - return - } - // 移除 - removeSpaceMemberApi({ - space_id: userSpace.id, - user_id: modal.userId, - }).catch((err) => { - console.log(err) - }) - } - else { - // 解散 - dissolveSpaceApi({ - space_id: userSpace.id, - }).catch((err) => { - console.log(err) - }) - } - - setModal({ - ...modal, - open: false, - }) - - onExit?.({ - type: modal.type, - userId: modal.userId, - }) - } - - return ( - - { - setAccordion(value) - }} - > - - - - {({ isExpanded }: { isExpanded: boolean }) => { - return ( - <> - - 邀请码 - - {isExpanded - ? ( - - ) - : ( - - )} - - ) - }} - - - - { - copyInviteCode() - }} - > - {userSpace?.invite_code} - - - - - - - - - - - - - 空间成员 - - - {spaceUsers.length} - {' '} - 人 - - - - - - item.id.toString()} - renderItem={({ item }) => ( - - - - - - {item.username} - {item.role === 0 && ( - - 创建者 - - )} - { - userStore.userInfo?.id === item.id && ( - - - 我 - - - ) - } - - {formatRelativeTime(item.last_login_time)} - - - {userSpace?.role === 0 && ( - - - - - - - - - {userSpace.role === 0 && userStore.userInfo?.id !== item.id && ( - { - setModal({ - ...modal, - open: true, - title: `确定将 ${item.username} 提出空间?`, - type: 'remove', - userId: item.id, - }) - }} - > - - 踢出空间 - - - )} - - - - - )} - - - - )} - /> - - { - if (userSpace?.role === 0) { - setModal({ - ...modal, - open: true, - title: `确定解散该空间么?不可恢复!`, - type: 'dissolve', - }) - } - else { - setModal({ - ...modal, - open: true, - title: `确定退出该空间么?`, - type: 'exit', - }) - } - }} - > - {userSpace?.role === 0 ? '解散空间' : '退出空间'} - - - - { - setModal({ - ...modal, - open: false, - }) - }} - onClose={() => { - setModal({ - ...modal, - open: false, - }) - }} - onConfirm={() => { - handleModalConfirm() - }} - /> - - ) -} diff --git a/code/allapp/allapp-rn/src/app/system/share/components/StepFade.tsx b/code/allapp/allapp-rn/src/app/system/share/components/StepFade.tsx deleted file mode 100644 index f6c975da..00000000 --- a/code/allapp/allapp-rn/src/app/system/share/components/StepFade.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' - -interface StepFadeProps { - visible: boolean - children: React.ReactNode -} - -export default function StepFade({ visible, children }: StepFadeProps) { - if (!visible) - return null - - return ( - - {children} - - ) -} diff --git a/code/allapp/allapp-rn/src/app/system/share/index.tsx b/code/allapp/allapp-rn/src/app/system/share/index.tsx deleted file mode 100644 index e411890c..00000000 --- a/code/allapp/allapp-rn/src/app/system/share/index.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import type { SpaceMember } from '@/types/space' -import { useEffect, useState } from 'react' -import { View } from 'react-native' -import CustomHeader from '@/components/custom/Header' -import { createSpaceApi, joinSpaceApi, listSpaceMembersApi } from '@/request/api/space/index' -import { useSpaceStore } from '@/store/space' -import { useUserStore } from '@/store/user' -import JoinSpace from './components/JoinSpace' -import SpaceHome from './components/SpaceHome' -import SpaceInvite from './components/SpaceInvite' -import StepFade from './components/StepFade' - -export default function PagerWithTab() { - const spaceStore = useSpaceStore() - const userStore = useUserStore() - - const [headerTitle, setHeaderTitle] = useState('我的空间') - const [step, setStep] = useState<'join' | 'invite' | 'home'>('home') - const [spaceUsers, setSpaceUsers] = useState([]) - - // 封装获取空间用户列表并直接更新状态 - const fetchSpaceUsers = async (spaceId: number) => { - try { - const res = await listSpaceMembersApi({ space_id: spaceId }) - if (res.isSuccess) { - setSpaceUsers(res.data.members) - console.log(res.data.members) - } - else { - console.warn('获取空间用户失败') - setSpaceUsers([]) - } - } - catch (err) { - console.error('listSpaceUsersApi error:', err) - setSpaceUsers([]) - } - } - - // 页面加载时判断并拉取用户列表 - useEffect(() => { - const loadSpaceUsers = () => { - if (spaceStore.userSpace !== null) { - setStep('invite') - - fetchSpaceUsers(spaceStore.userSpace.id) - } - else { - setStep('home') - } - } - - loadSpaceUsers() - }, []) - - useEffect(() => { - if (spaceStore.userSpace) { - setHeaderTitle(spaceStore.userSpace.name || '我的空间') - } - else { - setHeaderTitle('我的空间') - } - }, [spaceStore.userSpace]) - - function getSpaceName() { - const username = userStore.userInfo?.username - return username ? `${username}的空间` : '我的空间' - } - - function createSpace() { - createSpaceApi({ - name: getSpaceName(), - }).then((res) => { - if (res.isSuccess) { - spaceStore.setUserSpace({ - id: res.data.space_id, - name: getSpaceName(), - role: 0, - invite_code: res.data.invite_code, - }) - fetchSpaceUsers(res.data.space_id) - setStep('invite') - } - }).catch((err) => { - console.log(err) - }) - } - - function joinSpace(code: string) { - joinSpaceApi({ - invite_code: code, - }).then((res) => { - if (res.isSuccess) { - spaceStore.setUserSpace({ - id: res.data.space_id, - name: res.data.space_name, - invite_code: code, - role: 0, - }) - setStep('invite') - fetchSpaceUsers(res.data.space_id) - } - }).catch((err) => { - console.log(err) - }) - } - - return ( - - - - - { - setStep('join') - }} - /> - - - - { - setStep('home') - }} - onJoin={(code) => { - if (code !== '') { - joinSpace(code) - } - }} - /> - - - - { - if (data.type === 'remove' && data.userId !== undefined) { - setSpaceUsers(prevUsers => - prevUsers.filter(user => user.id !== data.userId), - ) - return - } - - setStep('home') - spaceStore.clear() - setSpaceUsers([]) - }} - /> - - - - ) -} diff --git a/code/allapp/allapp-rn/src/components/common/ao/Modal.tsx b/code/allapp/allapp-rn/src/components/common/ao/Modal.tsx index 91b9bd18..bdc1ab29 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/Modal.tsx +++ b/code/allapp/allapp-rn/src/components/common/ao/Modal.tsx @@ -80,7 +80,7 @@ export default function AModal({ > {title && ( - {typeof body === 'string' + {typeof title === 'string' ? ( {title} @@ -97,7 +97,9 @@ export default function AModal({ {typeof body === 'string' ? ( - {body} + + {body} + ) : ( diff --git a/code/allapp/allapp-rn/src/request/api/space/index.ts b/code/allapp/allapp-rn/src/request/api/space/index.ts index 3313e245..3522f4f2 100644 --- a/code/allapp/allapp-rn/src/request/api/space/index.ts +++ b/code/allapp/allapp-rn/src/request/api/space/index.ts @@ -2,19 +2,20 @@ import type { CreateSpaceParams, CreateSpaceResult, DissolveSpaceParams, + EnterDefaultSpaceResult, + EnterSpaceParams, ExitSpaceParams, GetSpaceSettingsParams, JoinSpaceParams, - JoinSpaceResult, + KickOutSpaceMemberParams, ListSpaceMembersParams, ListSpaceMembersResult, ListUserSpacesResult, - RemoveSpaceMemberParams, - RemoveSpaceMemberResult, + RefreshInviteCodeParams, + RefreshInviteCodeResult, + TransferSpaceParams, UpdateSpaceDefaultParams, UpdateSpaceMemberNameParams, - UpdateSpaceMemberRoleParams, - UpdateSpaceMemberRoleResult, UpdateSpaceNameParams, UpdateSpaceSortNumberParams, UserSpace, @@ -65,25 +66,39 @@ export function dissolveSpaceApi(params: DissolveSpaceParams) { // 退出空间 export function exitSpaceApi(params: ExitSpaceParams) { - return alovaInstance.Post>(`${API_PREFIX}/member/exit`, params) + return alovaInstance.Post>(`${API_PREFIX}/exit`, params) } -// Join Space API -export function joinSpaceApi(params: JoinSpaceParams) { - return alovaInstance.Post>(`${API_PREFIX}/join`, params) -} - -// Remove Space User API -export function removeSpaceMemberApi(params: RemoveSpaceMemberParams) { - return alovaInstance.Post>(`${API_PREFIX}/remove`, params) -} - -// Update Role API -export function updateSpaceMemberRoleApi(params: UpdateSpaceMemberRoleParams) { - return alovaInstance.Post>(`${API_PREFIX}/updateRole`, params) -} - -// List Space Users API +// 获取空间下成员 export function listSpaceMembersApi(params: ListSpaceMembersParams) { return alovaInstance.Post>(`${API_PREFIX}/members`, params) } + +// 踢出空间用户 +export function kickoutSpaceMemberApi(params: KickOutSpaceMemberParams) { + return alovaInstance.Post>(`${API_PREFIX}/kickout`, params) +} + +// 转移空间拥有者 +export function transferSpaceApi(params: TransferSpaceParams) { + return alovaInstance.Post>(`${API_PREFIX}/transfer`, params) +} + +// 刷新邀请码 +export function refreshInviteCodeApi(params: RefreshInviteCodeParams) { + return alovaInstance.Post>(`${API_PREFIX}/inviteCode`, params) +} + +// 加入空间 +export function joinSpaceApi(params: JoinSpaceParams) { + return alovaInstance.Post>(`${API_PREFIX}/join`, params) +} + +// 进入空间 +export function enterSpaceApi(params: EnterSpaceParams) { + return alovaInstance.Post>(`${API_PREFIX}/enter`, params) +} + +export function enterDefaultSpaceApi() { + return alovaInstance.Post>(`${API_PREFIX}/enterDefault`) +} diff --git a/code/allapp/allapp-rn/src/request/index.ts b/code/allapp/allapp-rn/src/request/index.ts index d52bee1f..32c49953 100644 --- a/code/allapp/allapp-rn/src/request/index.ts +++ b/code/allapp/allapp-rn/src/request/index.ts @@ -10,6 +10,7 @@ enum HttpCode { Success = 1000, Unauthorized = 1001, Fail = 500, + BusinessError = 2000, } // 仅保留「无需登录」接口 @@ -28,8 +29,8 @@ function isWhiteListed(url: string) { } const alovaInstance = createAlova({ - baseURL: 'http://192.168.5.6:8086/app', - // baseURL: 'http://192.168.1.137:8086/app', + // baseURL: 'http://192.168.5.6:8086/app', + baseURL: 'http://192.168.1.137:8086/app', requestAdapter: axiosRequestAdapter(), timeout: 5000, cacheFor: null, @@ -49,6 +50,10 @@ const alovaInstance = createAlova({ useNavStore.getState().navigateToAuth() return result } + else if (resData.code === HttpCode.BusinessError) { + toast.error(resData.message || '业务异常,请稍后再试') + return result + } return result }, diff --git a/code/allapp/allapp-rn/src/store/app.ts b/code/allapp/allapp-rn/src/store/app.ts index 57ea034f..25bee585 100644 --- a/code/allapp/allapp-rn/src/store/app.ts +++ b/code/allapp/allapp-rn/src/store/app.ts @@ -1,4 +1,5 @@ import { create } from 'zustand' +import { enterDefaultSpaceApi } from '@/request/api/space' import { getUserSpaceApi } from '@/request/api/user' import { useMoneyCategoryStore } from './moneyCategory' import { useNavStore } from './navigation' @@ -33,26 +34,31 @@ export const useAppStore = create((set, get) => ({ } try { - // 初始化分类 + // 1️⃣ 初始化默认空间(如果用户当前没选中空间) + if (!spaceStore.inSpace()) { + const res = await enterDefaultSpaceApi() + if (res.isSuccess) { + if (res.data.space_id === 0) { + // 跳转到空间管理页面 + } + spaceStore.setCurrentSpace(res.data) + } + } + + // 2️⃣ 初始化分类 await moneyCategoryStore.init() - - // 获取空间信息 - const res = await getUserSpaceApi() - console.log(res.data) - - spaceStore.setUserSpace(res.data) } catch (err) { - console.error('app init error:', err) + console.error('App init error:', err) + } + finally { + // 5️⃣ 初始化完成 + set({ ready: true }) } - - // 初始化完成 - set({ ready: true }) }, reset: () => { set({ ready: false }) - useMoneyCategoryStore.getState().clear() useUserStore.getState().clear() useSpaceStore.getState().clear() diff --git a/code/allapp/allapp-rn/src/store/navigation.ts b/code/allapp/allapp-rn/src/store/navigation.ts index 5cedfe2e..93027270 100644 --- a/code/allapp/allapp-rn/src/store/navigation.ts +++ b/code/allapp/allapp-rn/src/store/navigation.ts @@ -1,8 +1,6 @@ import type { icons } from 'lucide-react-native' -import AsyncStorage from '@react-native-async-storage/async-storage' import { router } from 'expo-router' import { create } from 'zustand' -import { createJSONStorage, persist } from 'zustand/middleware' export interface NavItem { label: string @@ -23,7 +21,7 @@ const DEFAULT_NAV_MENUS: NavItem[] = [ { label: '恋爱记', path: '/love', icon: 'Heart' }, { label: '聊天', path: '/chat', icon: 'MessageCircle' }, { label: '个人中心', path: '/user', icon: 'CircleUserRound' }, - { label: '我的空间', path: '/space/manage', icon: 'Earth' }, + { label: '空间管理', path: '/space/manage', icon: 'Earth' }, ] export const useNavStore = create()( diff --git a/code/allapp/allapp-rn/src/store/space.ts b/code/allapp/allapp-rn/src/store/space.ts index 91d6ba3d..0340db30 100644 --- a/code/allapp/allapp-rn/src/store/space.ts +++ b/code/allapp/allapp-rn/src/store/space.ts @@ -1,22 +1,33 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' import { create } from 'zustand' +import { createJSONStorage, persist } from 'zustand/middleware' + +interface Space { + space_id: number + role: number +} interface SpaceState { - userSpace: UserSpace | null - setUserSpace: (space: UserSpace | null) => void + currentSpace: Space | null + setCurrentSpace: (space: Space | null) => void clear: () => void inSpace: () => boolean } -export const useSpaceStore = create((set, get) => ({ - userSpace: null, - setUserSpace: space => set({ userSpace: space }), - clear: () => { - set({ - userSpace: null, - }) - }, - inSpace: () => { - const userSpace = get().userSpace - return userSpace !== null && userSpace.id !== 0 - }, -})) +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/allapp/allapp-rn/src/types/space/index.ts b/code/allapp/allapp-rn/src/types/space/index.ts index fea0657c..addf762f 100644 --- a/code/allapp/allapp-rn/src/types/space/index.ts +++ b/code/allapp/allapp-rn/src/types/space/index.ts @@ -68,40 +68,19 @@ export interface GetSpaceSettingsParams { space_id: number } -// 加入空间 -export interface JoinSpaceParams { - invite_code: string -} - -export interface JoinSpaceResult { - space_id: number - space_name: string -} - -// 踢出用户 -export interface RemoveSpaceMemberParams { - space_id: number - user_id: number -} - -export interface RemoveSpaceMemberResult { -} - -// 更新用户权限 -export interface UpdateSpaceMemberRoleParams { - space_id: number - user_id: number - role: number -} -export interface UpdateSpaceMemberRoleResult { -} - -// 空间用户列表 +// 获取空间用户 export interface SpaceMember { - id: number - username: string - avatar: string + space_id: number + user_id: number role: number + + member_name: string + member_status: number + member_update_time: number + join_time: number + + avatar: string + user_status: number last_login_time: number } @@ -112,3 +91,40 @@ export interface ListSpaceMembersParams { export interface ListSpaceMembersResult { members: SpaceMember[] } + +// 踢出用户 +export interface KickOutSpaceMemberParams { + space_id: number + member_id: number +} + +// 转入空间 +export interface TransferSpaceParams { + space_id: number + member_id: number +} + +// 刷新邀请码 +export interface RefreshInviteCodeParams { + space_id: number +} +export interface RefreshInviteCodeResult { + invite_code: string +} + +// 加入空间 +export interface JoinSpaceParams { + invite_code: string + member_name: string +} + +// 进入空间 +export interface EnterSpaceParams { + space_id: number +} + +// 进入默认空间 +export interface EnterDefaultSpaceResult { + space_id: number + role: number +} diff --git a/code/allapp/allapp-rn/src/utils/time.ts b/code/allapp/allapp-rn/src/utils/time.ts index cc25d99c..1cffd8a1 100644 --- a/code/allapp/allapp-rn/src/utils/time.ts +++ b/code/allapp/allapp-rn/src/utils/time.ts @@ -73,43 +73,6 @@ export function isInMonth(date: string, month: string): boolean { return dayjs(date).format('YYYY-MM') === month } -export function formatRelativeTime(date: string | Date | number): string { - if (typeof date === 'string' && !date.trim()) { - return '暂无活跃' - } - - const d = dayjs(date) - if (!d.isValid()) - return '暂无活跃' - - const now = dayjs() - const diff = now.diff(d, 'second') // 相差秒数 - - if (diff < 60) { - return '刚刚' - } - else if (diff < 3600) { - const minutes = Math.floor(diff / 60) - return `${minutes}分钟前` - } - else if (diff < 86400) { - const hours = Math.floor(diff / 3600) - return `${hours}小时前` - } - else if (diff < 2592000) { // 30天以内 - const days = Math.floor(diff / 86400) - return `${days}天前` - } - else if (diff < 31536000) { // 1年以内 - const months = Math.floor(diff / 2592000) - return `${months}个月前` - } - else { - const years = Math.floor(diff / 31536000) - return `${years}年前` - } -} - // 获取本月的第一天 export function getStartOfMonth() { return dayjs().startOf('month').format('YYYY-MM-DD') @@ -150,3 +113,51 @@ export function normalizeDateTime( return d.format('YYYY-MM-DD HH:mm:ss') } + +/** + * 格式化时间为相对时间 + * @param timestamp 秒级或毫秒级时间戳 + */ +export function formatRelativeTime(timestamp: number): string { + if (!timestamp) + return '未活跃' + + // 兼容秒级时间戳 + const time + = timestamp.toString().length === 10 + ? dayjs(timestamp * 1000) + : dayjs(timestamp) + + const now = dayjs() + + const diffSeconds = now.diff(time, 'second') + const diffMinutes = now.diff(time, 'minute') + const diffHours = now.diff(time, 'hour') + const diffDays = now.diff(time, 'day') + + if (diffSeconds < 30) { + return '刚刚' + } + + if (diffMinutes < 60) { + return `${diffMinutes}分钟前` + } + + if (diffHours < 24) { + return `${diffHours}小时前` + } + + if (diffDays === 1) { + return '昨天' + } + + if (diffDays < 7) { + return `${diffDays}天前` + } + + if (time.year() === now.year()) { + return time.format('MM-DD HH:mm') + } + + return time.format('YYYY-MM-DD') +} diff --git a/code/allapp/allapp-rn/test.html b/code/allapp/allapp-rn/test.html index 296043a1..a1f81fca 100644 --- a/code/allapp/allapp-rn/test.html +++ b/code/allapp/allapp-rn/test.html @@ -1,121 +1,130 @@ - + - - - Space Card - + + + 加入其它空间 Demo -
-
-
工
+ +
+ 加入其它空间 + → +
-
-
-
- 我的超级长长长长长长工作空间 -
- 创建者 - 默认 -
-
-
- -
-
昵称:张三
-
成员:8人
+ + + + \ No newline at end of file diff --git a/code/email/Email-Springboot/.idea/vcs.xml b/code/email/Email-Springboot/.idea/vcs.xml index d23592a9..6b9b8f6d 100644 --- a/code/email/Email-Springboot/.idea/vcs.xml +++ b/code/email/Email-Springboot/.idea/vcs.xml @@ -2,6 +2,7 @@ + \ No newline at end of file