u
This commit is contained in:
1 parent
2efeb7cee2
commit
c0531c1d0a
35 files changed
+1817
-984
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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 放行
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function TabsLayout() {
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity className="w-12 items-center justify-center">
|
||||
<LucideIcon icon="Bot" size={24} clickable={false} strokeWidth={2} />
|
||||
<LucideIcon icon="Earth" size={24} clickable={false} strokeWidth={2} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Divider orientation="vertical" className="h-8" />
|
||||
|
||||
@@ -10,7 +10,7 @@ import ListItem from './components/ListItem'
|
||||
|
||||
export default function User() {
|
||||
const userStore = useUserStore()
|
||||
console.log(userStore.userInfo)
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 p-4">
|
||||
<View className="flex flex-row items-center">
|
||||
|
||||
@@ -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<UserSpace[]>([])
|
||||
|
||||
@@ -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 && (
|
||||
<Badge size="sm" className="bg-[#333] rounded-full">
|
||||
<BadgeText className="text-white">当前空间</BadgeText>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center gap-3 mt-2">
|
||||
@@ -161,7 +188,7 @@ export default function AuthSpace() {
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#f5f6f8]">
|
||||
<CustomHeader title="空间列表" />
|
||||
<CustomHeader title="空间管理" />
|
||||
|
||||
<View className="flex-1 p-4">
|
||||
<View className="">
|
||||
@@ -182,6 +209,9 @@ export default function AuthSpace() {
|
||||
className="flex-1 rounded-full"
|
||||
size="sm"
|
||||
leftIcon={<LucideIcon icon="HousePlus" />}
|
||||
onPress={() => {
|
||||
setShowJoin(true)
|
||||
}}
|
||||
>
|
||||
加入其他空间
|
||||
</AButton>
|
||||
@@ -196,52 +226,69 @@ export default function AuthSpace() {
|
||||
onLayout={(e) => {
|
||||
setScrollHeight(e.nativeEvent.layout.height)
|
||||
}}
|
||||
refreshControl={(
|
||||
<RefreshControl
|
||||
refreshing={refreshLoading}
|
||||
onRefresh={async () => {
|
||||
setRefreshLoading(true)
|
||||
await listUserSpaces(false)
|
||||
setRefreshLoading(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{spaces.length === 0
|
||||
? (
|
||||
<View className="flex-1 items-center justify-center mt-12">
|
||||
<EmptyState
|
||||
title="暂无空间"
|
||||
description="请选择 创建 或 加入 空间"
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<Sortable.Grid
|
||||
scrollableRef={scrollableRef}
|
||||
keyExtractor={(item: UserSpace) => {
|
||||
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 ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#333" />
|
||||
</View>
|
||||
)
|
||||
: spaces.length === 0
|
||||
? (
|
||||
<View className="flex-1 items-center justify-center mt-12">
|
||||
<EmptyState
|
||||
title="暂无空间"
|
||||
description="请选择 创建 或 加入 空间"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Sortable.Grid
|
||||
scrollableRef={scrollableRef}
|
||||
keyExtractor={(item: UserSpace) => {
|
||||
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) {
|
||||
// 更新成功
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
</Animated.ScrollView>
|
||||
</View>
|
||||
|
||||
@@ -323,6 +370,84 @@ export default function AuthSpace() {
|
||||
</ActionsheetContent>
|
||||
</KeyboardAvoidingView>
|
||||
</Actionsheet>
|
||||
|
||||
<AModal
|
||||
title="加入空间"
|
||||
isOpen={showJoin}
|
||||
onClose={() => {
|
||||
setShowJoin(false)
|
||||
|
||||
setTimeout(() => {
|
||||
setJoinForm({
|
||||
invite_code: '',
|
||||
})
|
||||
}, 300)
|
||||
}}
|
||||
body={(
|
||||
<View className="px-3">
|
||||
<AInput
|
||||
placeholder="请输入空间邀请码"
|
||||
value={joinForm.invite_code}
|
||||
onChangeText={(text) => {
|
||||
setJoinForm({
|
||||
invite_code: text,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
footer={(
|
||||
<View className="w-full px-3 pb-3">
|
||||
<AButton
|
||||
loading={joinLoading}
|
||||
onPress={async () => {
|
||||
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)
|
||||
}
|
||||
}}
|
||||
|
||||
>
|
||||
加入
|
||||
</AButton>
|
||||
<ADivider text="或" />
|
||||
<AButton
|
||||
loading={joinLoading}
|
||||
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
toast.warning('功能待实现,敬请期待!')
|
||||
}}
|
||||
>
|
||||
扫码加入
|
||||
</AButton>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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<UserSpace | null>(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 (
|
||||
<View className="flex-1 bg-[#f5f6f8]">
|
||||
<CustomHeader title="分享空间" />
|
||||
|
||||
<View className="px-5 pt-6">
|
||||
{
|
||||
loading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#333" />
|
||||
</View>
|
||||
) : (
|
||||
<View className="px-5 pt-6">
|
||||
|
||||
{/* 邀请码卡片 */}
|
||||
<View className="bg-white rounded-2xl p-6 items-center">
|
||||
<Text className="text-gray-500 text-sm mb-3">
|
||||
复制邀请码邀请家庭成员加入
|
||||
</Text>
|
||||
{/* 邀请码卡片 */}
|
||||
<View className="bg-white rounded-2xl p-6 items-center">
|
||||
<Text className="text-gray-500 text-sm mb-3">
|
||||
复制邀请码邀请家庭成员加入
|
||||
</Text>
|
||||
|
||||
<Text className="text-4xl font-bold tracking-[8px] text-gray-900 mb-4">
|
||||
{invite_code}
|
||||
</Text>
|
||||
<Text className="text-4xl font-bold tracking-[8px] text-gray-900 mb-4">
|
||||
{space?.invite_code}
|
||||
</Text>
|
||||
|
||||
<AButton
|
||||
onPress={copyInviteCode}
|
||||
className="rounded-full"
|
||||
leftIcon={
|
||||
<LucideIcon icon="Copy" size={16} color="#fff" clickable={false} />
|
||||
}
|
||||
>
|
||||
复制邀请码
|
||||
</AButton>
|
||||
</View>
|
||||
<View className="flex-row gap-3">
|
||||
<AButton
|
||||
onPress={copyInviteCode}
|
||||
className="rounded-full flex-1"
|
||||
leftIcon={
|
||||
<LucideIcon icon="Copy" size={16} color="#fff" clickable={false} />
|
||||
}
|
||||
>
|
||||
复制邀请码
|
||||
</AButton>
|
||||
|
||||
<View className="my-8">
|
||||
<ADivider direction="horizontal" text="或扫描二维码" />
|
||||
</View>
|
||||
{
|
||||
userStore.userInfo.id === space?.owner_id && (
|
||||
<AButton
|
||||
variant="outline"
|
||||
className="rounded-full px-4"
|
||||
leftIcon={
|
||||
<LucideIcon icon="RefreshCw" size={16} clickable={false} />
|
||||
}
|
||||
loading={refreshLoading}
|
||||
onPress={refreshInviteCode}
|
||||
>
|
||||
刷新
|
||||
</AButton>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 二维码卡片 */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => setPreviewVisible(true)}
|
||||
className="bg-white rounded-2xl py-8 items-center"
|
||||
>
|
||||
<QRCode
|
||||
size={160}
|
||||
value={qrcode || ''}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
<View className="my-8">
|
||||
<ADivider direction="horizontal" text="或扫描二维码" />
|
||||
</View>
|
||||
|
||||
{/* 二维码卡片 */}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => setPreviewVisible(true)}
|
||||
className="bg-white rounded-2xl py-8 items-center"
|
||||
>
|
||||
<QRCode
|
||||
size={160}
|
||||
value={qrcode || ''}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 全屏预览(无动画) */}
|
||||
<Modal
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { View } from 'react-native'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<CustomHeader title="成员管理" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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<UserSpace | null>(null)
|
||||
const [members, setMembers] = useState<SpaceMember[]>([])
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshLoading, setRefreshLoading] = useState(false)
|
||||
|
||||
const [showAction, setShowAction] = useState(false)
|
||||
const [currentMember, setCurrentMember] = useState<SpaceMember | null>(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 (
|
||||
<View className="flex-1 ">
|
||||
<CustomHeader
|
||||
title="空间成员"
|
||||
/>
|
||||
|
||||
<View className="flex-1 bg-[#f5f6f8] relative">
|
||||
{
|
||||
loading
|
||||
? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#333" />
|
||||
</View>
|
||||
)
|
||||
: space === null
|
||||
? (
|
||||
<EmptyState title="空间不存在或已解散" />
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<View className="pt-6 pb-24 px-4 bg-[#333] items-center">
|
||||
<Text className="text-lg text-white font-semibold">
|
||||
{space.name || ''}
|
||||
</Text>
|
||||
|
||||
<View className="flex-row gap-2 mt-3">
|
||||
{space.is_default === 1 && (
|
||||
<Badge className="rounded-full bg-[#515151] px-3">
|
||||
<BadgeText className="text-white">默认</BadgeText>
|
||||
</Badge>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mt-[-60] p-6 bg-white h-full rounded-tl-2xl rounded-tr-2xl">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-gray-600 font-semibold">
|
||||
空间成员
|
||||
</Text>
|
||||
<Text>
|
||||
{members.length}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
className="pt-4"
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
refreshControl={(
|
||||
<RefreshControl
|
||||
refreshing={refreshLoading}
|
||||
onRefresh={async () => {
|
||||
setRefreshLoading(true)
|
||||
|
||||
await loadData(false)
|
||||
|
||||
setRefreshLoading(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{
|
||||
members.map((item) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="flex-row justify-between items-center pb-4"
|
||||
key={item.user_id}
|
||||
onPress={() => {
|
||||
setCurrentMember(item)
|
||||
setShowAction(true)
|
||||
}}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Avatar size="md">
|
||||
<AvatarFallbackText>{item.member_name}</AvatarFallbackText>
|
||||
<AvatarImage
|
||||
source={{
|
||||
uri: item.avatar,
|
||||
}}
|
||||
/>
|
||||
</Avatar>
|
||||
<View className="ml-3">
|
||||
<Text className="text-sm">{item.member_name}</Text>
|
||||
<View className="mt-2 gap-2 flex-row items-center">
|
||||
<View className="flex-row gap-1 items-center">
|
||||
<LucideIcon icon="Clock" size={12} color="#6b7280" clickable={false} />
|
||||
<Text className="text-xs text-gray-500">
|
||||
{formatRelativeTime(item.last_login_time)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="flex-row gap-2">
|
||||
{
|
||||
item.role === 0 && (
|
||||
<Badge size="sm" className="rounded-full mt-1">
|
||||
<BadgeText>创建人</BadgeText>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
item.member_status === 1 && (
|
||||
<Badge size="sm" className="rounded-full mt-1" action="error">
|
||||
<BadgeText>禁用</BadgeText>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
|
||||
<Actionsheet
|
||||
isOpen={showAction}
|
||||
onClose={() => {
|
||||
setShowAction(false)
|
||||
setTimeout(() => {
|
||||
setCurrentMember(null)
|
||||
}, 300)
|
||||
}}
|
||||
>
|
||||
<ActionsheetBackdrop />
|
||||
<ActionsheetContent style={{
|
||||
paddingBottom: 16,
|
||||
}}
|
||||
>
|
||||
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
|
||||
<ActionsheetItem
|
||||
onPress={() => {
|
||||
toast.warning('功能待开发,敬请期待!')
|
||||
}}
|
||||
>
|
||||
<LucideIcon icon="SquareUserRound" clickable={false} color="#333" />
|
||||
<ActionsheetItemText>查看资料</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
|
||||
{
|
||||
currentMember?.user_id !== userStore.userInfo.id && (
|
||||
<ActionsheetItem
|
||||
onPress={() => {
|
||||
toast.warning('功能待开发,敬请期待!')
|
||||
}}
|
||||
>
|
||||
<LucideIcon icon="UserPen" clickable={false} color="#333" />
|
||||
<ActionsheetItemText>修改备注</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
space?.owner_id === userStore.userInfo.id
|
||||
&& currentMember?.user_id !== userStore.userInfo.id && (
|
||||
<>
|
||||
<ADivider />
|
||||
|
||||
<ActionsheetItem
|
||||
onPress={() => {
|
||||
setShowTransfer(true)
|
||||
}}
|
||||
>
|
||||
<LucideIcon icon="Rotate3d" clickable={false} color="#333" />
|
||||
<ActionsheetItemText>转让空间</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
|
||||
<ActionsheetItem
|
||||
onPress={() => {
|
||||
toast.warning('功能待开发,敬请期待!')
|
||||
}}
|
||||
>
|
||||
<LucideIcon icon="ShieldAlert" clickable={false} color="#333" />
|
||||
<ActionsheetItemText>禁用</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
|
||||
<ADivider />
|
||||
|
||||
<ActionsheetItem
|
||||
onPress={() => {
|
||||
setShowKickout(true)
|
||||
}}
|
||||
>
|
||||
<LucideIcon icon="CircleX" clickable={false} color="#333" />
|
||||
<ActionsheetItemText>踢出空间</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</ActionsheetContent>
|
||||
</Actionsheet>
|
||||
|
||||
<AModal
|
||||
isOpen={showKickout}
|
||||
title="提示"
|
||||
body="确定该用户踢出空间?"
|
||||
onCancel={() => {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<AModal
|
||||
isOpen={showTransfer}
|
||||
title="提示"
|
||||
body={`确定将该空间的所有权转让给 ${currentMember?.member_name || '未知用户'} ?`}
|
||||
onCancel={() => {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
? (
|
||||
<EmptyState title="正在加载空间中 . . ." />
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#333" />
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
<View className="flex-1 px-4 pt-4">
|
||||
<View className="bg-white rounded-xl overflow-hidden mb-6">
|
||||
<Item
|
||||
icon="Earth"
|
||||
title="进入空间"
|
||||
onPress={async () => {
|
||||
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)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text className="text-sm font-semibold text-gray-400 mb-2 tracking-wider">
|
||||
常用设置
|
||||
@@ -175,7 +206,7 @@ export default function Settings() {
|
||||
|
||||
<Item
|
||||
icon="Star"
|
||||
title="设为默认空间"
|
||||
title="默认空间"
|
||||
isLast
|
||||
showRightIcon={false}
|
||||
right={(
|
||||
@@ -229,7 +260,7 @@ export default function Settings() {
|
||||
router.navigate({
|
||||
pathname: '/space/manage/sub-page/invite',
|
||||
params: {
|
||||
invite_code: space.invite_code,
|
||||
space_id: space.space_id,
|
||||
},
|
||||
})
|
||||
}}
|
||||
@@ -245,7 +276,15 @@ export default function Settings() {
|
||||
isOwner && (
|
||||
<Item
|
||||
icon="Users"
|
||||
title="成员管理"
|
||||
title="空间成员"
|
||||
onPress={() => {
|
||||
router.navigate({
|
||||
pathname: '/space/manage/sub-page/member',
|
||||
params: {
|
||||
space_id: space.space_id,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -432,7 +471,7 @@ export default function Settings() {
|
||||
{/* 解散 */}
|
||||
<AModal
|
||||
title="提示"
|
||||
body={isOwner ? '确认解散该空间么?' : '确定解散空间么?'}
|
||||
body={isOwner ? '确认解散该空间么?' : '确定退出空间么?'}
|
||||
isOpen={showDissolve}
|
||||
onClose={() => {
|
||||
setShowDissolve(false)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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<AFormRef>(null)
|
||||
const [form, setForm] = useState({
|
||||
code: '',
|
||||
})
|
||||
return (
|
||||
<View className="px-4 items-center">
|
||||
<AForm ref={formRef} form={form}>
|
||||
<AFormItem
|
||||
name="username"
|
||||
label="邀请码"
|
||||
>
|
||||
<AInput
|
||||
placeholder="请输入邀请码"
|
||||
value={form.code}
|
||||
onChangeText={text => setForm({
|
||||
...form,
|
||||
code: text,
|
||||
})}
|
||||
>
|
||||
</AInput>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
|
||||
<AButton
|
||||
className="w-full"
|
||||
onPress={() => {
|
||||
if (form.code === '') {
|
||||
//
|
||||
}
|
||||
else {
|
||||
onJoin?.(form.code)
|
||||
}
|
||||
}}
|
||||
>
|
||||
加入空间
|
||||
</AButton>
|
||||
|
||||
<View
|
||||
className="flex-row items-center w-full"
|
||||
style={{
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<ADivider />
|
||||
</View>
|
||||
<Text className="mx-3 text-gray-500 text-sm">或</Text>
|
||||
<View className="flex-1">
|
||||
<ADivider />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AButton variant="outline" className="w-full">扫码加入</AButton>
|
||||
|
||||
<Link
|
||||
className="mt-4"
|
||||
onPress={() => {
|
||||
onBack?.()
|
||||
}}
|
||||
>
|
||||
<LinkText>返回</LinkText>
|
||||
</Link>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<View className="p-4">
|
||||
<View className="flex-col gap-4 justify-center items-center">
|
||||
<LucideIcon icon="Earth" size={48} strokeWidth={1.5} />
|
||||
<Text className="font-semibold text-xl">创建共享空间</Text>
|
||||
<Text className="text-base text-center">创建一个专属空间,邀请朋友加入,共享文件、聊天和协作</Text>
|
||||
<AButton
|
||||
className="w-full"
|
||||
onPress={() => onCreate?.()}
|
||||
>
|
||||
创建我的空间
|
||||
</AButton>
|
||||
</View>
|
||||
|
||||
<ADivider text="或" />
|
||||
|
||||
<AButton
|
||||
variant="outline"
|
||||
onPress={() => onJoin?.()}
|
||||
>
|
||||
加入已有空间
|
||||
</AButton>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<View className="px-4 flex-1">
|
||||
<Accordion
|
||||
size="md"
|
||||
variant="unfilled"
|
||||
type="multiple"
|
||||
isCollapsible={true}
|
||||
isDisabled={false}
|
||||
value={accordion}
|
||||
onValueChange={(value) => {
|
||||
setAccordion(value)
|
||||
}}
|
||||
>
|
||||
<AccordionItem value="a">
|
||||
<AccordionHeader>
|
||||
<AccordionTrigger
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
>
|
||||
{({ isExpanded }: { isExpanded: boolean }) => {
|
||||
return (
|
||||
<>
|
||||
<AccordionTitleText className="font-semibold text-lg">
|
||||
邀请码
|
||||
</AccordionTitleText>
|
||||
{isExpanded
|
||||
? (
|
||||
<AccordionIcon as={ChevronUpIcon} className="ml-3" />
|
||||
)
|
||||
: (
|
||||
<AccordionIcon as={ChevronDownIcon} className="ml-3" />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
<AccordionContent style={{
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center"
|
||||
onPress={() => {
|
||||
copyInviteCode()
|
||||
}}
|
||||
>
|
||||
<Text className="font-bold text-3xl tracking-[4px] mr-3">{userSpace?.invite_code}</Text>
|
||||
<LucideIcon icon="Copy" size={16} color="blue" clickable={false} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View className="items-center justify-center">
|
||||
<ADivider direction="horizontal" text="扫描二维码" />
|
||||
<QRCode
|
||||
size={120}
|
||||
value={userSpace?.invite_code || ''}
|
||||
/>
|
||||
</View>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<View className="mt-4 flex-1">
|
||||
<View className="flex flex-row items-center justify-between">
|
||||
<Text className="font-semibold text-lg">空间成员</Text>
|
||||
<Badge size="lg" variant="solid" action="muted">
|
||||
<BadgeText>
|
||||
{spaceUsers.length}
|
||||
{' '}
|
||||
人
|
||||
</BadgeText>
|
||||
</Badge>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<FlatList
|
||||
data={spaceUsers}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity>
|
||||
<HStack space="md" className="my-2 items-center">
|
||||
<AAvatar avatar={item.avatar} username={item.username} showBadge={true} />
|
||||
<VStack>
|
||||
<HStack space="sm">
|
||||
<Heading size="sm">{item.username}</Heading>
|
||||
{item.role === 0 && (
|
||||
<Badge size="sm" variant="solid" action="muted">
|
||||
<BadgeText>创建者</BadgeText>
|
||||
</Badge>
|
||||
)}
|
||||
{
|
||||
userStore.userInfo?.id === item.id && (
|
||||
<Badge size="sm" variant="solid" action="info">
|
||||
<BadgeText>
|
||||
我
|
||||
</BadgeText>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</HStack>
|
||||
<Text size="sm" className="mt-1">{formatRelativeTime(item.last_login_time)}</Text>
|
||||
</VStack>
|
||||
|
||||
{userSpace?.role === 0 && (
|
||||
<View className="ml-auto">
|
||||
<Menu
|
||||
renderer={renderers.Popover}
|
||||
rendererProps={{
|
||||
placement: 'bottom',
|
||||
preferredPlacement: 'bottom',
|
||||
anchorStyle: { backgroundColor: 'transparent' },
|
||||
closeAnimationDuration: 0,
|
||||
}}
|
||||
>
|
||||
<MenuTrigger>
|
||||
<View className="w-12 items-center justify-center h-10">
|
||||
<LucideIcon icon="Menu" size={20} clickable={false} />
|
||||
</View>
|
||||
</MenuTrigger>
|
||||
<MenuOptions
|
||||
customStyles={{
|
||||
optionsContainer: {
|
||||
backgroundColor: 'white',
|
||||
minWidth: 160,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{userSpace.role === 0 && userStore.userInfo?.id !== item.id && (
|
||||
<MenuOption
|
||||
customStyles={{
|
||||
OptionTouchableComponent: TouchableHighlight,
|
||||
optionTouchable: {
|
||||
underlayColor: '#f0f0f0',
|
||||
activeOpacity: 0.7,
|
||||
},
|
||||
}}
|
||||
onSelect={() => {
|
||||
setModal({
|
||||
...modal,
|
||||
open: true,
|
||||
title: `确定将 ${item.username} 提出空间?`,
|
||||
type: 'remove',
|
||||
userId: item.id,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-row items-center h-10 px-2">
|
||||
<Text className="text-lg">踢出空间</Text>
|
||||
</View>
|
||||
</MenuOption>
|
||||
)}
|
||||
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
</View>
|
||||
)}
|
||||
|
||||
</HStack>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<AButton
|
||||
size="sm"
|
||||
className="mb-8 mt-4"
|
||||
onPress={() => {
|
||||
if (userSpace?.role === 0) {
|
||||
setModal({
|
||||
...modal,
|
||||
open: true,
|
||||
title: `确定解散该空间么?不可恢复!`,
|
||||
type: 'dissolve',
|
||||
})
|
||||
}
|
||||
else {
|
||||
setModal({
|
||||
...modal,
|
||||
open: true,
|
||||
title: `确定退出该空间么?`,
|
||||
type: 'exit',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{userSpace?.role === 0 ? '解散空间' : '退出空间'}
|
||||
</AButton>
|
||||
</View>
|
||||
|
||||
<AModal
|
||||
title={modal.title}
|
||||
isOpen={modal.open}
|
||||
onCancel={() => {
|
||||
setModal({
|
||||
...modal,
|
||||
open: false,
|
||||
})
|
||||
}}
|
||||
onClose={() => {
|
||||
setModal({
|
||||
...modal,
|
||||
open: false,
|
||||
})
|
||||
}}
|
||||
onConfirm={() => {
|
||||
handleModalConfirm()
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(200)}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
@@ -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<SpaceMember[]>([])
|
||||
|
||||
// 封装获取空间用户列表并直接更新状态
|
||||
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 (
|
||||
<View className="flex-1">
|
||||
<CustomHeader title={headerTitle} />
|
||||
<View className="flex-1">
|
||||
<StepFade visible={step === 'home'}>
|
||||
<SpaceHome
|
||||
onCreate={createSpace}
|
||||
onJoin={() => {
|
||||
setStep('join')
|
||||
}}
|
||||
/>
|
||||
</StepFade>
|
||||
|
||||
<StepFade visible={step === 'join'}>
|
||||
<JoinSpace
|
||||
onBack={() => {
|
||||
setStep('home')
|
||||
}}
|
||||
onJoin={(code) => {
|
||||
if (code !== '') {
|
||||
joinSpace(code)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StepFade>
|
||||
|
||||
<StepFade visible={step === 'invite'}>
|
||||
<SpaceInvite
|
||||
userSpace={spaceStore.userSpace}
|
||||
spaceUsers={spaceUsers}
|
||||
onExit={(data) => {
|
||||
if (data.type === 'remove' && data.userId !== undefined) {
|
||||
setSpaceUsers(prevUsers =>
|
||||
prevUsers.filter(user => user.id !== data.userId),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setStep('home')
|
||||
spaceStore.clear()
|
||||
setSpaceUsers([])
|
||||
}}
|
||||
/>
|
||||
</StepFade>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export default function AModal({
|
||||
>
|
||||
{title && (
|
||||
<ModalHeader>
|
||||
{typeof body === 'string'
|
||||
{typeof title === 'string'
|
||||
? (
|
||||
<View className="items-center justify-center w-full py-2">
|
||||
<Heading>{title}</Heading>
|
||||
@@ -97,7 +97,9 @@ export default function AModal({
|
||||
{typeof body === 'string'
|
||||
? (
|
||||
<View className="items-center justify-center w-full">
|
||||
<Text>{body}</Text>
|
||||
<Text className="w-full text-center">
|
||||
{body}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
|
||||
@@ -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<ResponseData<void>>(`${API_PREFIX}/member/exit`, params)
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/exit`, params)
|
||||
}
|
||||
|
||||
// Join Space API
|
||||
export function joinSpaceApi(params: JoinSpaceParams) {
|
||||
return alovaInstance.Post<ResponseData<JoinSpaceResult>>(`${API_PREFIX}/join`, params)
|
||||
}
|
||||
|
||||
// Remove Space User API
|
||||
export function removeSpaceMemberApi(params: RemoveSpaceMemberParams) {
|
||||
return alovaInstance.Post<ResponseData<RemoveSpaceMemberResult>>(`${API_PREFIX}/remove`, params)
|
||||
}
|
||||
|
||||
// Update Role API
|
||||
export function updateSpaceMemberRoleApi(params: UpdateSpaceMemberRoleParams) {
|
||||
return alovaInstance.Post<ResponseData<UpdateSpaceMemberRoleResult>>(`${API_PREFIX}/updateRole`, params)
|
||||
}
|
||||
|
||||
// List Space Users API
|
||||
// 获取空间下成员
|
||||
export function listSpaceMembersApi(params: ListSpaceMembersParams) {
|
||||
return alovaInstance.Post<ResponseData<ListSpaceMembersResult>>(`${API_PREFIX}/members`, params)
|
||||
}
|
||||
|
||||
// 踢出空间用户
|
||||
export function kickoutSpaceMemberApi(params: KickOutSpaceMemberParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/kickout`, params)
|
||||
}
|
||||
|
||||
// 转移空间拥有者
|
||||
export function transferSpaceApi(params: TransferSpaceParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/transfer`, params)
|
||||
}
|
||||
|
||||
// 刷新邀请码
|
||||
export function refreshInviteCodeApi(params: RefreshInviteCodeParams) {
|
||||
return alovaInstance.Post<ResponseData<RefreshInviteCodeResult>>(`${API_PREFIX}/inviteCode`, params)
|
||||
}
|
||||
|
||||
// 加入空间
|
||||
export function joinSpaceApi(params: JoinSpaceParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/join`, params)
|
||||
}
|
||||
|
||||
// 进入空间
|
||||
export function enterSpaceApi(params: EnterSpaceParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/enter`, params)
|
||||
}
|
||||
|
||||
export function enterDefaultSpaceApi() {
|
||||
return alovaInstance.Post<ResponseData<EnterDefaultSpaceResult>>(`${API_PREFIX}/enterDefault`)
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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<AppState>((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()
|
||||
|
||||
@@ -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<NavState>()(
|
||||
|
||||
@@ -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<SpaceState>((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<SpaceState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
currentSpace: null,
|
||||
setCurrentSpace: space => set({ currentSpace: space }),
|
||||
clear: () => set({ currentSpace: null }),
|
||||
inSpace: () => {
|
||||
const currentSpace = get().currentSpace
|
||||
return currentSpace !== null && currentSpace.space_id !== 0
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'space-storage',
|
||||
storage: createJSONStorage(() => AsyncStorage),
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
+105
-96
@@ -1,121 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Space Card</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>加入其它空间 Demo</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #f5f6f8;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f4f6f8;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.space-card {
|
||||
width: 380px;
|
||||
/* 页面列表项 */
|
||||
.space-option {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.space-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.08);
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.space-card.selected {
|
||||
border: 1px solid #111;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.space-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.space-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 16px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.space-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f0f2f5;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.badge.primary {
|
||||
background: #e8f0ff;
|
||||
color: #3b6cff;
|
||||
}
|
||||
|
||||
.space-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.space-option:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
/* modal 背景 */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* modal 内容 */
|
||||
.modal {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal .scan-button {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="space-card selected">
|
||||
<div class="space-header">
|
||||
<div class="space-avatar">工</div>
|
||||
<!-- 加入其它空间选项 -->
|
||||
<div class="space-option" id="joinSpaceBtn">
|
||||
<span>加入其它空间</span>
|
||||
<span>→</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="display:flex; align-items:center; gap:6px;">
|
||||
<div class="space-name">
|
||||
我的超级长长长长长长工作空间
|
||||
</div>
|
||||
<span class="badge">创建者</span>
|
||||
<span class="badge primary">默认</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-info">
|
||||
<div>昵称:张三</div>
|
||||
<div>成员:8人</div>
|
||||
<!-- Modal -->
|
||||
<div class="modal-backdrop" id="modalBackdrop">
|
||||
<div class="modal">
|
||||
<h2>加入空间</h2>
|
||||
<input type="text" placeholder="请输入邀请码" id="inviteInput" />
|
||||
<button id="joinBtn">加入</button>
|
||||
<button class="scan-button" id="scanBtn">扫码加入</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const joinBtnEl = document.getElementById('joinSpaceBtn');
|
||||
const modalEl = document.getElementById('modalBackdrop');
|
||||
|
||||
joinBtnEl.addEventListener('click', () => {
|
||||
modalEl.style.display = 'flex';
|
||||
});
|
||||
|
||||
modalEl.addEventListener('click', (e) => {
|
||||
if (e.target === modalEl) {
|
||||
modalEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('joinBtn').addEventListener('click', () => {
|
||||
const code = document.getElementById('inviteInput').value;
|
||||
alert(`尝试加入空间:${code}`);
|
||||
});
|
||||
|
||||
document.getElementById('scanBtn').addEventListener('click', () => {
|
||||
alert('扫码功能触发(可接入扫码库)');
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../../docusaurus" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Reference in new issue
Block a user