diff --git a/code/allapp/allapp-go/.idea/data_source_mapping.xml b/code/allapp/allapp-go/.idea/data_source_mapping.xml index 79158e4f..1c9b86ee 100644 --- a/code/allapp/allapp-go/.idea/data_source_mapping.xml +++ b/code/allapp/allapp-go/.idea/data_source_mapping.xml @@ -2,7 +2,6 @@ - diff --git a/code/allapp/allapp-go/api/handler/space/index.go b/code/allapp/allapp-go/api/handler/space/index.go index 23034a77..5c6b9f30 100644 --- a/code/allapp/allapp-go/api/handler/space/index.go +++ b/code/allapp/allapp-go/api/handler/space/index.go @@ -220,11 +220,36 @@ func UpdateSpaceDefault(c fiber.Ctx) error { return err } - err = db.Queries.UpdateSpaceIsDefault(c.Context(), repo.UpdateSpaceIsDefaultParams{ - SpaceID: dto.SpaceId, - UserID: userId, - IsDefault: dto.IsDefault, + err = db.WithTx(c.Context(), func(q *repo.Queries) error { + err = q.UpdateSpaceIsDefault(c.Context(), repo.UpdateSpaceIsDefaultParams{ + SpaceID: dto.SpaceId, + UserID: userId, + IsDefault: dto.IsDefault, + }) + + if err != nil { + return err + } + + var otherDefault int16 + + if dto.IsDefault == 1 { + otherDefault = 0 + } + + err = q.UpdateOtherSpacesIsDefault(c.Context(), repo.UpdateOtherSpacesIsDefaultParams{ + IsDefault: otherDefault, + UserID: userId, + SpaceID: dto.SpaceId, + }) + + if err != nil { + return err + } + + return nil }) + if err != nil { return err } @@ -270,6 +295,85 @@ func UpdateSpaceName(c fiber.Ctx) error { return responsex.OK(c, "更新成功") } +func GetSpaceSettings(c fiber.Ctx) error { + dto := new(types.GetSpaceSettingDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + spaceId := dto.SpaceId + + row, err := db.Queries.GetSpaceByUserIdAndSpaceId(c.Context(), repo.GetSpaceByUserIdAndSpaceIdParams{ + UserID: userId, + ID: spaceId, + }) + + if err != nil { + if errorx.IsNotFound(err) { + return responsex.SpaceError(c, "不在该空间或该空间已解散") + } + return err + } + vo := new(types.UserSpace) + + vo.SpaceId = row.SpaceID + vo.Name = row.Name + vo.IsDefault = row.IsDefault + vo.MemberName = row.MemberName + vo.Role = row.Role + vo.MemberCount = row.MemberCount + vo.OwnerId = row.OwnerID + vo.SortNumber = row.SortNumber + + return responsex.OK(c, vo) +} + +// UpdateSpaceSortNumber 更新空间排序 +func UpdateSpaceSortNumber(c fiber.Ctx) error { + dto := new(types.UpdateSpaceSortNumberDTO) + if err := requestx.BindAndValidate(c, dto); err != nil { + return err + } + + userId, err := middleware.GetUserID(c) + if err != nil { + return err + } + + // 组装批量更新参数 + params := make([]repo.BatchUpdateSpaceSortNumberParams, 0, len(dto.Spaces)) + + for _, datum := range dto.Spaces { + params = append(params, repo.BatchUpdateSpaceSortNumberParams{ + SortNumber: datum.SortNumber, + SpaceID: datum.SpaceId, + UserID: userId, + }) + } + + // 使用统一事务封装 + err = db.WithTx(c.Context(), func(q *repo.Queries) error { + batch := q.BatchUpdateSpaceSortNumber(c.Context(), params) + + if err := db.RunBatch(c.Context(), batch); err != nil { + return err + } + + return nil + }) + + if err != nil { + return err + } + + return responsex.OK(c, "更新成功") +} + // 初始化记账分类 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/router.go b/code/allapp/allapp-go/api/router/router.go index 02d7c9d5..da2c6d69 100644 --- a/code/allapp/allapp-go/api/router/router.go +++ b/code/allapp/allapp-go/api/router/router.go @@ -25,9 +25,11 @@ func SetupRouters(app *fiber.App) { spaceRouter.Post("/list", space.ListUserSpaces) spaceRouter.Post("/dissolve", space.DissolveSpace) spaceRouter.Post("/name", space.UpdateSpaceName) + spaceRouter.Post("/settings", space.GetSpaceSettings) spaceRouter.Post("/member/default", space.UpdateSpaceDefault) spaceRouter.Post("/member/name", space.UpdateMemberName) + spaceRouter.Post("/member/sortNumber", space.UpdateSpaceSortNumber) // 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 083b9860..666f24b6 100644 --- a/code/allapp/allapp-go/api/types/space.go +++ b/code/allapp/allapp-go/api/types/space.go @@ -45,3 +45,16 @@ type UpdateSpaceNameDTO struct { SpaceId int64 `json:"space_id" validate:"required"` SpaceName string `json:"space_name" validate:"required"` } + +type GetSpaceSettingDTO struct { + SpaceId int64 `json:"space_id" validate:"required"` +} + +type SpaceSortNumber struct { + SpaceId int64 `json:"space_id" validate:"required"` + SortNumber int32 `json:"sort_number" validate:"required"` +} + +type UpdateSpaceSortNumberDTO struct { + Spaces []SpaceSortNumber `json:"spaces"` +} diff --git a/code/allapp/allapp-go/db/queries/space.sql b/code/allapp/allapp-go/db/queries/space.sql index a843de5f..08466b49 100644 --- a/code/allapp/allapp-go/db/queries/space.sql +++ b/code/allapp/allapp-go/db/queries/space.sql @@ -42,6 +42,28 @@ FROM space s WHERE sm.user_id = $1 ORDER BY sm.is_default DESC, sm.sort_number ASC; +-- name: GetSpaceByUserIdAndSpaceId :one +SELECT + s.id AS space_id, + s.owner_id, + s.name, + sm.role, + COALESCE(mc.member_count, 0) AS member_count, + sm.is_default, + sm.member_name, + sm.sort_number +FROM space s + JOIN space_member sm + ON s.id = sm.space_id + LEFT JOIN ( + SELECT space_id, COUNT(*) AS member_count + FROM space_member + GROUP BY space_id +) mc + ON s.id = mc.space_id +WHERE sm.user_id = $1 AND s.id = $2 +ORDER BY sm.is_default DESC, sm.sort_number ASC; + -- name: UpdateSpaceName :exec UPDATE space SET name = $1 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 6725528a..ff3b0693 100644 --- a/code/allapp/allapp-go/db/queries/space_member.sql +++ b/code/allapp/allapp-go/db/queries/space_member.sql @@ -25,3 +25,17 @@ WHERE space_id = $2 AND user_id = $3; -- name: UpdateSpaceIsDefault :exec UPDATE space_member SET is_default = $1 WHERE space_id = $2 AND user_id = $3; + +-- 将用户的其它空间更新为非默认 +-- name: UpdateOtherSpacesIsDefault :exec +UPDATE space_member +SET is_default = $1 +WHERE user_id = $2 + AND space_id != $3; + +-- 更新空间顺序 +-- name: BatchUpdateSpaceSortNumber :batchexec +UPDATE space_member +SET sort_number = $1 +WHERE user_id = $2 + AND space_id = $3; \ No newline at end of file diff --git a/code/allapp/allapp-go/db/repo/batch.go b/code/allapp/allapp-go/db/repo/batch.go new file mode 100644 index 00000000..702371db --- /dev/null +++ b/code/allapp/allapp-go/db/repo/batch.go @@ -0,0 +1,72 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: batch.go + +package repo + +import ( + "context" + "errors" + + "github.com/jackc/pgx/v5" +) + +var ( + ErrBatchAlreadyClosed = errors.New("batch already closed") +) + +const batchUpdateSpaceSortNumber = `-- name: BatchUpdateSpaceSortNumber :batchexec +UPDATE space_member +SET sort_number = $1 +WHERE user_id = $2 + AND space_id = $3 +` + +type BatchUpdateSpaceSortNumberBatchResults struct { + br pgx.BatchResults + tot int + closed bool +} + +type BatchUpdateSpaceSortNumberParams struct { + SortNumber int32 + UserID int64 + SpaceID int64 +} + +// 更新空间顺序 +func (q *Queries) BatchUpdateSpaceSortNumber(ctx context.Context, arg []BatchUpdateSpaceSortNumberParams) *BatchUpdateSpaceSortNumberBatchResults { + batch := &pgx.Batch{} + for _, a := range arg { + vals := []interface{}{ + a.SortNumber, + a.UserID, + a.SpaceID, + } + batch.Queue(batchUpdateSpaceSortNumber, vals...) + } + br := q.db.SendBatch(ctx, batch) + return &BatchUpdateSpaceSortNumberBatchResults{br, len(arg), false} +} + +func (b *BatchUpdateSpaceSortNumberBatchResults) Exec(f func(int, error)) { + defer b.br.Close() + for t := 0; t < b.tot; t++ { + if b.closed { + if f != nil { + f(t, ErrBatchAlreadyClosed) + } + continue + } + _, err := b.br.Exec() + if f != nil { + f(t, err) + } + } +} + +func (b *BatchUpdateSpaceSortNumberBatchResults) Close() error { + b.closed = true + return b.br.Close() +} diff --git a/code/allapp/allapp-go/db/repo/db.go b/code/allapp/allapp-go/db/repo/db.go index 96d21c78..19078e54 100644 --- a/code/allapp/allapp-go/db/repo/db.go +++ b/code/allapp/allapp-go/db/repo/db.go @@ -16,6 +16,7 @@ type DBTX interface { Query(context.Context, string, ...interface{}) (pgx.Rows, error) QueryRow(context.Context, string, ...interface{}) pgx.Row CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error) + SendBatch(context.Context, *pgx.Batch) pgx.BatchResults } func New(db DBTX) *Queries { diff --git a/code/allapp/allapp-go/db/repo/space.sql.go b/code/allapp/allapp-go/db/repo/space.sql.go index 245243c3..cd07e10a 100644 --- a/code/allapp/allapp-go/db/repo/space.sql.go +++ b/code/allapp/allapp-go/db/repo/space.sql.go @@ -66,6 +66,61 @@ func (q *Queries) GetSpaceByInviteCode(ctx context.Context, inviteCode string) ( return i, err } +const getSpaceByUserIdAndSpaceId = `-- name: GetSpaceByUserIdAndSpaceId :one +SELECT + s.id AS space_id, + s.owner_id, + s.name, + sm.role, + COALESCE(mc.member_count, 0) AS member_count, + sm.is_default, + sm.member_name, + sm.sort_number +FROM space s + JOIN space_member sm + ON s.id = sm.space_id + LEFT JOIN ( + SELECT space_id, COUNT(*) AS member_count + FROM space_member + GROUP BY space_id +) mc + ON s.id = mc.space_id +WHERE sm.user_id = $1 AND s.id = $2 +ORDER BY sm.is_default DESC, sm.sort_number ASC +` + +type GetSpaceByUserIdAndSpaceIdParams struct { + UserID int64 + ID int64 +} + +type GetSpaceByUserIdAndSpaceIdRow struct { + SpaceID int64 + OwnerID int64 + Name string + Role int16 + MemberCount int64 + IsDefault int16 + MemberName string + SortNumber int32 +} + +func (q *Queries) GetSpaceByUserIdAndSpaceId(ctx context.Context, arg GetSpaceByUserIdAndSpaceIdParams) (GetSpaceByUserIdAndSpaceIdRow, error) { + row := q.db.QueryRow(ctx, getSpaceByUserIdAndSpaceId, arg.UserID, arg.ID) + var i GetSpaceByUserIdAndSpaceIdRow + err := row.Scan( + &i.SpaceID, + &i.OwnerID, + &i.Name, + &i.Role, + &i.MemberCount, + &i.IsDefault, + &i.MemberName, + &i.SortNumber, + ) + return i, err +} + const insertSpace = `-- name: InsertSpace :exec INSERT INTO space ( id,name,invite_code,owner_id,create_time,update_time 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 086ee0c2..680476f8 100644 --- a/code/allapp/allapp-go/db/repo/space_member.sql.go +++ b/code/allapp/allapp-go/db/repo/space_member.sql.go @@ -15,6 +15,7 @@ FROM space_member WHERE user_id = $1 ` +// 合计某个用户的空间数量 func (q *Queries) CountUserSpaces(ctx context.Context, userID int64) (int64, error) { row := q.db.QueryRow(ctx, countUserSpaces, userID) var count int64 @@ -57,6 +58,7 @@ type InsertSpaceMemberParams struct { SortNumber int32 } +// 插入空间用户 func (q *Queries) InsertSpaceMember(ctx context.Context, arg InsertSpaceMemberParams) error { _, err := q.db.Exec(ctx, insertSpaceMember, arg.ID, @@ -70,6 +72,25 @@ func (q *Queries) InsertSpaceMember(ctx context.Context, arg InsertSpaceMemberPa return err } +const updateOtherSpacesIsDefault = `-- name: UpdateOtherSpacesIsDefault :exec +UPDATE space_member +SET is_default = $1 +WHERE user_id = $2 + AND space_id != $3 +` + +type UpdateOtherSpacesIsDefaultParams struct { + IsDefault int16 + UserID int64 + SpaceID int64 +} + +// 将用户的其它空间更新为非默认 +func (q *Queries) UpdateOtherSpacesIsDefault(ctx context.Context, arg UpdateOtherSpacesIsDefaultParams) error { + _, err := q.db.Exec(ctx, updateOtherSpacesIsDefault, arg.IsDefault, arg.UserID, arg.SpaceID) + return err +} + const updateSpaceIsDefault = `-- name: UpdateSpaceIsDefault :exec UPDATE space_member SET is_default = $1 WHERE space_id = $2 AND user_id = $3 @@ -81,6 +102,7 @@ type UpdateSpaceIsDefaultParams struct { UserID int64 } +// 更新空间是否为默认 func (q *Queries) UpdateSpaceIsDefault(ctx context.Context, arg UpdateSpaceIsDefaultParams) error { _, err := q.db.Exec(ctx, updateSpaceIsDefault, arg.IsDefault, arg.SpaceID, arg.UserID) return err @@ -97,6 +119,7 @@ type UpdateSpaceMemberNameParams struct { UserID int64 } +// 更新用户在空间内的名称 func (q *Queries) UpdateSpaceMemberName(ctx context.Context, arg UpdateSpaceMemberNameParams) error { _, err := q.db.Exec(ctx, updateSpaceMemberName, arg.MemberName, arg.SpaceID, arg.UserID) return err diff --git a/code/allapp/allapp-go2/db/queries/money_user_category.sql b/code/allapp/allapp-go2/db/queries/money_user_category.sql index 873b364b..c975ec2e 100644 --- a/code/allapp/allapp-go2/db/queries/money_user_category.sql +++ b/code/allapp/allapp-go2/db/queries/money_user_category.sql @@ -1,5 +1,5 @@ -- name: BatchInsertMoneyUserCategories :copyfrom -INSERT INTO money_user_category (id, user_id, name, icon, type, sort_number) + INSERT INTO money_user_category (id, user_id, name, icon, type, sort_number) VALUES ($1, $2, $3, $4, $5, $6); -- name: FindUserMoneyCategoriesByUserID :many diff --git a/code/allapp/allapp-rn/babel.config.js b/code/allapp/allapp-rn/babel.config.js index 275b9697..39d20356 100644 --- a/code/allapp/allapp-rn/babel.config.js +++ b/code/allapp/allapp-rn/babel.config.js @@ -4,6 +4,6 @@ module.exports = function (api) { presets: [ ['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel', - ] + ], } } diff --git a/code/allapp/allapp-rn/src/app/(tabs)/chat.tsx b/code/allapp/allapp-rn/src/app/(tabs)/chat.tsx index 9177fb38..fa652522 100644 --- a/code/allapp/allapp-rn/src/app/(tabs)/chat.tsx +++ b/code/allapp/allapp-rn/src/app/(tabs)/chat.tsx @@ -1,68 +1,33 @@ -import React from 'react' -import { - StyleSheet, - Text, - TextInput, - TouchableOpacity, - View, -} from 'react-native' -import { KeyboardAvoidingView } from 'react-native-keyboard-controller' +import React, { useState } from 'react' +import { Text, View } from 'react-native' +import { AButton, AModal } from '@/components/common/ao' +import AnimatedSwitch from '@/components/common/ao/Switch' + +export default function TestScreen() { + const [showDissolve, setShowDissolve] = useState(false) -export default function KeyboardAvoidingViewExample() { return ( - - - Header - - - - - - Submit - - - + + { + setShowDissolve(true) + }} + > + 测试 + + { + setShowDissolve(false) + }} + onCancel={() => { + setShowDissolve(false) + }} + onConfirm={() => { + setShowDissolve(false) + }} + /> + ) } - -const styles = StyleSheet.create({ - content: { - flex: 1, - maxHeight: 600, - }, - heading: { - fontSize: 36, - marginBottom: 48, - fontWeight: '600', - }, - inner: { - padding: 24, - flex: 1, - justifyContent: 'space-between', - }, - textInput: { - height: 45, - borderColor: '#000000', - borderWidth: 1, - borderRadius: 10, - marginBottom: 36, - paddingLeft: 10, - }, - button: { - marginTop: 12, - height: 45, - borderRadius: 10, - backgroundColor: 'rgb(40, 64, 147)', - justifyContent: 'center', - alignItems: 'center', - }, - text: { - fontWeight: '500', - fontSize: 16, - color: 'white', - }, -}) diff --git a/code/allapp/allapp-rn/src/app/_layout.tsx b/code/allapp/allapp-rn/src/app/_layout.tsx index 720087ee..eb87597f 100644 --- a/code/allapp/allapp-rn/src/app/_layout.tsx +++ b/code/allapp/allapp-rn/src/app/_layout.tsx @@ -87,7 +87,7 @@ export default function RootLayout() { - + ) } diff --git a/code/allapp/allapp-rn/src/app/space/manage/components/SettingActions.tsx b/code/allapp/allapp-rn/src/app/space/manage/components/SettingActions.tsx deleted file mode 100644 index bca281c5..00000000 --- a/code/allapp/allapp-rn/src/app/space/manage/components/SettingActions.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import type { UserSpace } from '@/types/space' -import { useEffect, useRef, useState } from 'react' -import { Text, View } from 'react-native' -import { KeyboardAvoidingView } from 'react-native-keyboard-controller' -import Animated, { useAnimatedStyle } from 'react-native-reanimated' -import { toast } from 'sonner-native' -import { AButton, LucideIcon } from '@/components/common/ao' -import { AInput, AModal } from '@/components/common/ao/index' -import { - Actionsheet, - ActionsheetBackdrop, - ActionsheetContent, - ActionsheetDragIndicator, - ActionsheetDragIndicatorWrapper, - ActionsheetIcon, - ActionsheetItem, - ActionsheetItemText, -} from '@/components/ui/actionsheet' -import { dissolveSpaceApi, updateSpaceNameApi } from '@/request/api/space' -import { useUserStore } from '@/store/user' - -interface SettingActionsProps { - isOpen: boolean - onClose: () => void - - space: UserSpace | null - - onActionSuccess?: () => void -} - -export default function SettingActions( - { - isOpen, - onClose, - space, - onActionSuccess, - }: SettingActionsProps, -) { - const userStore = useUserStore() - const inputRef = useRef(null) - const [localSpace, setLocalSpace] = useState(space) - - const [showNameModal, setShowNameModal] = useState(false) - const [showMemberNameModal, setShowMemberNameModal] = useState(false) - useEffect(() => { - setLocalSpace(space) - }, [space]) - - useEffect(() => { - if (showNameModal) { - inputRef.current?.focus() - } - }, [showNameModal]) - - return ( - <> - { - onClose() - }} - > - - - - - - { - space?.role === 0 && ( - - } - /> - 成员管理 - - ) - } - - { - space?.owner_id === userStore.userInfo.id && ( - { - setShowNameModal(true) - }} - > - } - /> - 修改空间名称 - - ) - } - - - } - /> - 修改我的空间昵称 - - - - } - /> - - {space?.is_default === 1 ? '取消' : '设置'} - 默认 - - - - { - if (space?.owner_id === userStore.userInfo.id) { - // 解散空间 - dissolveSpaceApi({ - space_id: space.space_id, - }).then((res) => { - if (res.isSuccess) { - toast.success('空间已解散') - onActionSuccess?.() - } - }, - ) - } - else { - // 退出空间 - } - }} - > - } - /> - - {space?.owner_id === userStore.userInfo.id ? '解散' : '退出'} - 空间 - - - - - - { - setShowNameModal(false) - }} - > - - - - - - - - - - - 空间名称 - - - - - - - 保存 - - - - - - ) -} 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 f0c1f574..9db1138e 100644 --- a/code/allapp/allapp-rn/src/app/space/manage/index.tsx +++ b/code/allapp/allapp-rn/src/app/space/manage/index.tsx @@ -1,20 +1,28 @@ import type { UserSpace } from '@/types/space' -import { useEffect, useState } from 'react' +import { useFocusEffect, useRouter } from 'expo-router' +import { useCallback, useEffect, useRef, useState } from 'react' import { Pressable, 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 { AModal } from '@/components/common/ao/index' import EmptyState from '@/components/common/EmptyStatus' import CustomHeader from '@/components/custom/Header' +import { + Actionsheet, + ActionsheetBackdrop, + ActionsheetContent, + ActionsheetDragIndicator, + ActionsheetDragIndicatorWrapper, +} from '@/components/ui/actionsheet' import { Badge, BadgeText } from '@/components/ui/badge' -import { createSpaceApi, listUserSpacesApi } from '@/request/api/space/index' +import { createSpaceApi, listUserSpacesApi, updateSpaceSortNumberApi } from '@/request/api/space/index' import { useUserStore } from '@/store/user' -import SettingActions from './components/SettingActions' export default function AuthSpace() { const userStore = useUserStore() + const router = useRouter() // scrollView 相关 const scrollableRef = useAnimatedRef() @@ -23,23 +31,30 @@ export default function AuthSpace() { // 空间列表 const [spaces, setSpaces] = useState([]) - const [selectedId, setSelectedId] = useState(null) // 新建 const [createOpen, setCreateOpen] = useState(false) const [createForm, setCreateForm] = useState({ name: '', }) + const [createLoading, setCreateLoading] = useState(false) - // 设置 - const [showSettings, setShowSettings] = useState(false) - const [settingCurrentSpace, setSettingCurrentSpace] = useState(null) + const createInputRef = useRef(null) + + useEffect(() => { + if (createOpen) { + const timer = setTimeout(() => { + createInputRef.current?.focus() + }, 300) + + return () => clearTimeout(timer) + } + }, [createOpen]) // 获取空间列表 function listUserSpaces() { listUserSpacesApi().then((res) => { if (res.isSuccess) { - console.log(res.data) setSpaces(res.data.spaces) } }) @@ -50,23 +65,22 @@ export default function AuthSpace() { { - setSelectedId(item.space_id) - scrollToCenter(index) + router.push({ + pathname: '/space/manage/sub-page/settings', + params: { + spaceId: item.space_id, + }, + }) }} style={{ height: ITEM_HEIGHT, }} - className={`bg-[#f9f9f9] rounded-md flex-row items-center justify-between px-4 py-3 border - ${item.space_id === selectedId ? 'border-gray-700' : 'border-gray-200'}`} + className={`rounded-xl flex-row items-center px-4 py-3 bg-[#fff] ${ + index === spaces.length - 1 ? 'mb-4' : '' + }`} > - - - - + + - 创建者 + + 创建者 ) } { item.is_default === 1 && ( - - 默认 + + 默认 ) } @@ -112,33 +126,15 @@ export default function AuthSpace() { + + + + - - - - - - { - e.stopPropagation() - setSettingCurrentSpace(item) - setShowSettings(true) - }} - > - - - ) @@ -157,16 +153,17 @@ export default function AuthSpace() { }) } - useEffect(() => { - listUserSpaces() - }, []) + useFocusEffect( + useCallback(() => { + listUserSpaces() + }, []), + ) return ( - + - - + 选择或创建空间 @@ -224,84 +221,108 @@ export default function AuthSpace() { 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) { + // 更新成功 + } + }) + } + }} /> )} - - - }>进入空间 - - - 空间名称 - { - setCreateForm(prev => ({ - ...prev, - name: val, - })) - }} - /> - - ) - } + { - setCreateOpen(false) - setCreateForm({ - name: '', - }) - }} onClose={() => { setCreateOpen(false) setCreateForm({ name: '', }) }} - onConfirm={async () => { - if (!createForm.name) { - toast.warning('请输入空间名称') - return - } - - await createSpaceApi({ - name: createForm.name, - member_name: userStore.userInfo.nickname, - sort_number: 0, - }).then((res) => { - if (res.isSuccess) { - toast.success('创建成功') - setCreateOpen(false) - setCreateForm({ - name: '', - }) - - listUserSpaces() - } - }) - }} > - + + - { - setShowSettings(false) + + + + - setTimeout(() => { - setSettingCurrentSpace(null) - }, 300) - }} - space={settingCurrentSpace} - /> + + + 创建新空间 + + + + { + setCreateForm(prev => ({ + ...prev, + name: val, + })) + }} + className="w-full" + placeholder="请输入空间名称" + /> + + + { + if (!createForm.name) { + toast.warning('请输入空间名称') + return + } + + setCreateLoading(true) + + try { + createSpaceApi({ + name: createForm.name, + member_name: userStore.userInfo.nickname, + sort_number: spaces.length, + }).then((res) => { + if (res.isSuccess) { + toast.success('创建成功') + setCreateOpen(false) + setCreateForm({ + name: '', + }) + + listUserSpaces() + } + }) + } + finally { + setCreateLoading(false) + } + }} + > + 创建 + + + + + ) } diff --git a/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings.tsx b/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings.tsx deleted file mode 100644 index d32fe0a8..00000000 --- a/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { View } from 'react-native' - -import CustomHeader from '@/components/custom/Header' - -export default function Settings() { - return ( - - - - - - ) -} 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 new file mode 100644 index 00000000..c9078c30 --- /dev/null +++ b/code/allapp/allapp-rn/src/app/space/manage/sub-page/settings/index.tsx @@ -0,0 +1,430 @@ +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 { 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, + ActionsheetContent, + ActionsheetDragIndicator, + ActionsheetDragIndicatorWrapper, +} from '@/components/ui/actionsheet' +import { dissolveSpaceApi, getSpaceSettingsApi, updateSpaceDefaultApi, updateSpaceMemberNameApi, updateSpaceNameApi } from '@/request/api/space' +import { useUserStore } from '@/store/user' + +interface ItemProps { + icon: any + title: string + right?: React.ReactNode + isLast?: boolean + showRightIcon?: boolean + onPress?: () => void +} + +function Item({ + icon, + title, + right, + isLast, + showRightIcon = true, + onPress, +}: ItemProps) { + return ( + <> + + + + {title} + + + + {right} + {showRightIcon && ( + + )} + + + + {!isLast && } + + ) +} + +export default function Settings() { + const userStore = useUserStore() + + const params = useLocalSearchParams<{ spaceId: string }>() + const { spaceId } = params + + const [space, setSpace] = useState(null) + + // 空间名称 + const [showName, setShowName] = useState(false) + const [nameForm, setNameForm] = useState({ + name: '', + }) + const [nameLoading, setNameLoading] = useState(false) + const nameInputRef = useRef(null) + + // 空间昵称 + const [showMemberName, setShowMemberName] = useState(false) + const [memberNameForm, setMemberNameForm] = useState({ + name: '', + }) + const [memberNameLoading, setMemberNameLoading] = useState(false) + const memberNameInputRef = useRef(null) + + // 默认 + const [defaultLoading, setDefaultLoading] = useState(false) + + // 解散 + const [showDissolve, setShowDissolve] = useState(false) + + useEffect(() => { + if (showName) { + const timer = setTimeout(() => { + nameInputRef.current?.focus() + }, 300) + + return () => clearTimeout(timer) + } + }, [showName]) + + useEffect(() => { + if (!spaceId) { + toast.warning('空间无效') + router.back() + } + else { + getSpaceSettingsApi({ + space_id: Number(spaceId), + }).then((res) => { + if (res.isSuccess) { + setSpace(res.data) + } + }) + } + }, [spaceId]) + + return ( + + + { + !space + ? ( + + ) + : ( + + + 常用设置 + + + + { + space?.owner_id === userStore.userInfo.id && ( + {space.name} + )} + onPress={() => { + setNameForm({ + name: space.name, + }) + setShowName(true) + }} + /> + ) + } + + {space.member_name} + )} + onPress={() => { + setMemberNameForm({ + name: space.member_name, + }) + setShowMemberName(true) + }} + /> + + { + if (!space) { + toast.error('空间信息异常') + return + } + try { + setDefaultLoading(true) + + updateSpaceDefaultApi({ + space_id: space.space_id, + is_default: val, + }).then((res) => { + if (res.isSuccess) { + toast.success('更新默认空间成功') + + setSpace(prev => prev + ? ({ + ...prev, + is_default: val, + }) + : null) + } + }) + } + finally { + setDefaultLoading(false) + } + }} + /> + )} + /> + + + + 其它操作 + + + + + + { + setShowDissolve(true) + }} + /> + + + ) + } + + {/* 空间名称 */} + { + setShowName(false) + setNameForm(prev => ({ + ...prev, + name: '', + })) + }} + > + + + + + + + + + + + 空间名称 + + + + setNameForm(prev => prev + ? { + ...prev, + name: text, + } + : null)} + className="w-full" + placeholder="请输入空间名称" + /> + + + { + if (!space) { + toast.error('空间信息异常') + return + } + try { + setNameLoading(true) + + updateSpaceNameApi({ + space_id: space.space_id, + space_name: nameForm.name, + }).then((res) => { + if (res.isSuccess) { + toast.success('空间名称已更新') + + setShowName(false) + + setSpace(prev => prev + ? ({ + ...prev, + name: nameForm.name, + }) + : null) + } + }, + ) + } + finally { + setNameLoading(false) + } + }} + > + 保存 + + + + + + + {/* 空间昵称 */} + { + setShowMemberName(false) + setMemberNameForm(prev => ({ + ...prev, + name: '', + })) + }} + > + + + + + + + + + + + 我的空间昵称 + + + + setMemberNameForm(prev => prev + ? { + ...prev, + name: text, + } + : null)} + className="w-full" + placeholder="请输入空间昵称" + /> + + + { + if (!space) { + toast.error('空间信息异常') + return + } + try { + setMemberNameLoading(true) + + updateSpaceMemberNameApi({ + space_id: space.space_id, + member_name: memberNameForm.name, + }).then((res) => { + if (res.isSuccess) { + toast.success('空间昵称已更新') + + setShowMemberName(false) + + setSpace(prev => prev + ? ({ + ...prev, + member_name: memberNameForm.name, + }) + : null) + } + }, + ) + } + finally { + setMemberNameLoading(false) + } + }} + > + 保存 + + + + + + + {/* 解散 */} + { + setShowDissolve(false) + }} + onCancel={() => { + setShowDissolve(false) + }} + onConfirm={async () => { + if (!space) { + toast.error('空间信息异常') + return + } + + await dissolveSpaceApi({ + space_id: space.space_id, + }).then((res) => { + if (res.isSuccess) { + setShowDissolve(false) + toast.success('解散成功') + + setTimeout(() => { + router.back() + }, 300) + } + }) + }} + /> + + ) +} diff --git a/code/allapp/allapp-rn/src/components/common/EmptyStatus.tsx b/code/allapp/allapp-rn/src/components/common/EmptyStatus.tsx index 68d4669c..16a60547 100644 --- a/code/allapp/allapp-rn/src/components/common/EmptyStatus.tsx +++ b/code/allapp/allapp-rn/src/components/common/EmptyStatus.tsx @@ -30,7 +30,6 @@ export default function EmptyState({ flexGrow: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: 'white', padding: 16, }} showsVerticalScrollIndicator={true} diff --git a/code/allapp/allapp-rn/src/components/common/ao/Input.tsx b/code/allapp/allapp-rn/src/components/common/ao/Input.tsx index afa22600..d3510f1b 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/Input.tsx +++ b/code/allapp/allapp-rn/src/components/common/ao/Input.tsx @@ -47,6 +47,7 @@ const AInput = forwardRef, AInputProps>( isFocused={isFocused} className={className} style={style} + > {leftIcon && ( 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 dafa2699..91b9bd18 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/Modal.tsx +++ b/code/allapp/allapp-rn/src/components/common/ao/Modal.tsx @@ -1,18 +1,19 @@ import type { ViewStyle } from 'react-native' import { useState } from 'react' -import { Text } from 'react-native' +import { Text, TouchableOpacity, View } from 'react-native' +import { + ButtonSpinner, +} from '@/components/ui/button' import { Heading } from '@/components/ui/heading' -import { CloseIcon, Icon } from '@/components/ui/icon' + import { Modal, ModalBackdrop, ModalBody, - ModalCloseButton, ModalContent, ModalFooter, ModalHeader, } from '@/components/ui/modal' -import AButton from '../ao/Button' interface AModalProps { isOpen?: boolean @@ -23,7 +24,6 @@ interface AModalProps { onConfirm?: () => Promise | void onCancel?: () => void showBackdrop?: boolean - showCloseIcon?: boolean className?: string style?: ViewStyle @@ -37,7 +37,6 @@ export default function AModal({ onClose, onConfirm, showBackdrop = true, - showCloseIcon = false, className, style, @@ -76,42 +75,58 @@ export default function AModal({ style={{ ...style, width: '80%', + padding: 0, }} > - - {title && (typeof title === 'string' ? {title} : title)} - { - showCloseIcon && ( - - - - ) - } - - - {body && (typeof body === 'string' ? {body} : body)} - + {title && ( + + {typeof body === 'string' + ? ( + + {title} + + ) + : ( + title + )} + + )} + + {body && ( + + {typeof body === 'string' + ? ( + + {body} + + ) + : ( + body + )} + + )} { footer || ( - <> - + - 取消 - - 取消 + + - 确认 - - + { + innerLoading && ( + + ) + } + 确认 + + ) } diff --git a/code/allapp/allapp-rn/src/components/common/ao/Switch.tsx b/code/allapp/allapp-rn/src/components/common/ao/Switch.tsx new file mode 100644 index 00000000..486fd438 --- /dev/null +++ b/code/allapp/allapp-rn/src/components/common/ao/Switch.tsx @@ -0,0 +1,127 @@ +import { useEffect } from 'react' +import { Pressable, StyleSheet } from 'react-native' +import Animated, { + interpolateColor, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' + +interface SwitchProps { + value: any + onChange: (val: any) => void + trueValue?: any + falseValue?: any + width?: number + height?: number + activeColor?: string + inactiveColor?: string + duration?: number + disabled?: boolean // 新增 disabled +} + +export default function Switch({ + value, + onChange, + trueValue, + falseValue, + width = 52, + height = 24, + activeColor = '#333', + inactiveColor = '#e5e7eb', + duration = 200, + disabled = false, // 默认可用 +}: SwitchProps) { + // 默认值 fallback + const trueVal = trueValue ?? true + const falseVal = falseValue ?? false + + // 当前状态是否为激活 + const isActive = Object.is(value, trueVal) + + // 控制动画的 sharedValue + const progress = useSharedValue(isActive ? 1 : 0) + + useEffect(() => { + progress.value = withTiming(isActive ? 1 : 0, { duration }) + }, [isActive, duration]) + + const knobSize = height - 4 + const translateX = width - knobSize - 4 + + // 动画轨道样式 + const trackStyle = useAnimatedStyle(() => { + const baseColor = interpolateColor( + progress.value, + [0, 1], + [inactiveColor, activeColor], + ) + return { + backgroundColor: baseColor, + opacity: disabled ? 0.7 : 1, // disabled 半透明 + } + }) + + // 动画滑块样式 + const knobStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateX: progress.value * translateX, + }, + ], + opacity: disabled ? 0.8 : 1, // knob 半透明 + } + }) + + return ( + { + if (disabled) + return + const nextValue = isActive ? falseVal : trueVal + onChange(nextValue) + }} + style={{ width, height }} + > + + + + + ) +} + +const styles = StyleSheet.create({ + track: { + justifyContent: 'center', + padding: 2, + }, + knob: { + backgroundColor: '#fff', + elevation: 2, + shadowColor: '#000', + shadowOpacity: 0.2, + shadowRadius: 2, + shadowOffset: { width: 0, height: 1 }, + }, +}) diff --git a/code/allapp/allapp-rn/src/components/common/ao/index.ts b/code/allapp/allapp-rn/src/components/common/ao/index.ts index 61f09116..145de12a 100644 --- a/code/allapp/allapp-rn/src/components/common/ao/index.ts +++ b/code/allapp/allapp-rn/src/components/common/ao/index.ts @@ -15,6 +15,7 @@ import ListItem from './ListItem' import LucideIcon from './LucideIcon' import Modal from './Modal' import SegmentedTabs from './SegmentedTabs' +import Switch from './Switch' import Tab from './Tab' export { @@ -35,6 +36,7 @@ export { ListItem as AListItem, Modal as AModal, SegmentedTabs as ASegmentedTabs, + Switch as ASwitch, Tab as ATab, LucideIcon, } diff --git a/code/allapp/allapp-rn/src/components/ui/switch/index.tsx b/code/allapp/allapp-rn/src/components/ui/switch/index.tsx new file mode 100644 index 00000000..29453722 --- /dev/null +++ b/code/allapp/allapp-rn/src/components/ui/switch/index.tsx @@ -0,0 +1,41 @@ +'use client'; +import React from 'react'; +import { Switch as RNSwitch } from 'react-native'; +import { createSwitch } from '@gluestack-ui/core/switch/creator'; +import { tva } from '@gluestack-ui/utils/nativewind-utils'; +import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; +import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'; + +const UISwitch = createSwitch({ + Root: withStyleContext(RNSwitch), +}); + +const switchStyle = tva({ + base: 'data-[focus=true]:outline-0 data-[focus=true]:ring-2 data-[focus=true]:ring-indicator-primary web:cursor-pointer disabled:cursor-not-allowed data-[disabled=true]:opacity-40 data-[invalid=true]:border-error-700 data-[invalid=true]:rounded-xl data-[invalid=true]:border-2', + + variants: { + size: { + sm: 'scale-75', + md: '', + lg: 'scale-125', + }, + }, +}); + +type ISwitchProps = React.ComponentProps & + VariantProps; +const Switch = React.forwardRef< + React.ComponentRef, + ISwitchProps +>(function Switch({ className, size = 'md', ...props }, ref) { + return ( + + ); +}); + +Switch.displayName = 'Switch'; +export { Switch }; 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 cb83e3f9..f8a697ba 100644 --- a/code/allapp/allapp-rn/src/request/api/space/index.ts +++ b/code/allapp/allapp-rn/src/request/api/space/index.ts @@ -5,6 +5,7 @@ import type { DissolveSpaceResult, ExitSpaceParams, ExitSpaceResult, + GetSpaceSettingsParams, JoinSpaceParams, JoinSpaceResult, ListSpaceMembersParams, @@ -12,9 +13,13 @@ import type { ListUserSpacesResult, RemoveSpaceMemberParams, RemoveSpaceMemberResult, + UpdateSpaceDefaultParams, + UpdateSpaceMemberNameParams, UpdateSpaceMemberRoleParams, UpdateSpaceMemberRoleResult, UpdateSpaceNameParams, + UpdateSpaceSortNumberParams, + UserSpace, } from '@/types/space' import alovaInstance from '../../index' @@ -35,6 +40,26 @@ export function updateSpaceNameApi(params: UpdateSpaceNameParams) { return alovaInstance.Post>(`${API_PREFIX}/name`, params) } +// 修改空间昵称 +export function updateSpaceMemberNameApi(params: UpdateSpaceMemberNameParams) { + return alovaInstance.Post>(`${API_PREFIX}/member/name`, params) +} + +// 修改空间是否默认 +export function updateSpaceDefaultApi(params: UpdateSpaceDefaultParams) { + return alovaInstance.Post>(`${API_PREFIX}/member/default`, params) +} + +// 修改空间排列顺序 +export function updateSpaceSortNumberApi(params: UpdateSpaceSortNumberParams) { + return alovaInstance.Post>(`${API_PREFIX}/member/sortNumber`, params) +} + +// 获取空间设置信息 +export function getSpaceSettingsApi(params: GetSpaceSettingsParams) { + return alovaInstance.Post>(`${API_PREFIX}/settings`, params) +} + // Join Space API export function joinSpaceApi(params: JoinSpaceParams) { return alovaInstance.Post>(`${API_PREFIX}/join`, params) diff --git a/code/allapp/allapp-rn/src/request/index.ts b/code/allapp/allapp-rn/src/request/index.ts index d52bee1f..f12e2875 100644 --- a/code/allapp/allapp-rn/src/request/index.ts +++ b/code/allapp/allapp-rn/src/request/index.ts @@ -28,8 +28,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, diff --git a/code/allapp/allapp-rn/src/types/space/index.ts b/code/allapp/allapp-rn/src/types/space/index.ts index e423beb7..4e0c0673 100644 --- a/code/allapp/allapp-rn/src/types/space/index.ts +++ b/code/allapp/allapp-rn/src/types/space/index.ts @@ -34,11 +34,35 @@ export interface DissolveSpaceParams { export interface DissolveSpaceResult { } -// 更新名称 +// 更新空间名称 export interface UpdateSpaceNameParams { space_id: number space_name: string -} +} + +// 更新空间昵称 +export interface UpdateSpaceMemberNameParams { + space_id: number + member_name: string +} + +// 更新空间是否默认 +export interface UpdateSpaceDefaultParams { + space_id: number + is_default: number +} + +// 更新空间排列顺序 +export interface UpdateSpaceSortNumberParams { + spaces: { + space_id: number + sort_number: number + }[] +} +// 获取空间设置信息 +export interface GetSpaceSettingsParams { + space_id: number +} // 加入空间 export interface JoinSpaceParams { diff --git a/code/allapp/allapp-rn/test.html b/code/allapp/allapp-rn/test.html index 009aa6d1..296043a1 100644 --- a/code/allapp/allapp-rn/test.html +++ b/code/allapp/allapp-rn/test.html @@ -1,82 +1,121 @@ - + + - - -空间状态演示 - + + + Space Card + + + - -
-
-
- 工作空间 - 当前 - 默认 -
-
张三 · 5人
-
-
⋯
-
+
+
+
工
- -
-
-
- 家庭账本 - 默认 +
+
+
+ 我的超级长长长长长长工作空间 +
+ 创建者 + 默认 +
+
-
张三 · 3人
-
-
⋯
-
- -
-
-
- 副业空间 +
+
昵称:张三
+
成员:8人
-
张三 · 2人
-
⋯
-
+ \ No newline at end of file