This commit is contained in:
oneao committed 2026-02-28 17:02:35 +08:00
1 parent fc005e0fb4
commit 10d0cadfe3
18 files changed
+591 -133

No files matched your search

-2
View File
@@ -56,10 +56,8 @@
"react-native-screens": "~4.16.0",
"react-native-sortables": "^1.9.3",
"react-native-svg": "15.12.1",
"react-native-toast-message": "^2.3.3",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.5.1",
"react-native-z-view": "^0.2.4",
"sonner-native": "^0.23.1",
"tailwind-merge": "^3.4.0",
"victory-native": "^37.3.6",
-30
View File
@@ -146,18 +146,12 @@ importers:
react-native-svg:
specifier: 15.12.1
version: 15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-toast-message:
specifier: ^2.3.3
version: 2.3.3(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-web:
specifier: ^0.21.0
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-native-worklets:
specifier: 0.5.1
version: 0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-z-view:
specifier: ^0.2.4
version: 0.2.4(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
sonner-native:
specifier: ^0.23.1
version: 0.23.1(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -5123,12 +5117,6 @@ packages:
react: '*'
react-native: '*'
react-native-toast-message@2.3.3:
resolution: {integrity: sha512-4IIUHwUPvKHu4gjD0Vj2aGQzqPATiblL1ey8tOqsxOWRPGGu52iIbL8M/mCz4uyqecvPdIcMY38AfwRuUADfQQ==}
peerDependencies:
react: '*'
react-native: '*'
react-native-web@0.21.2:
resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
peerDependencies:
@@ -5142,13 +5130,6 @@ packages:
react: '*'
react-native: '*'
react-native-z-view@0.2.4:
resolution: {integrity: sha512-K/z6GhHmDsyEZAy6ib8DEmITSyPb0rCJ95IZjilZbAarx8xB6prdvHvtgmCg67nFKO/l/ldBuYhvxQmClda+pQ==}
peerDependencies:
react: '*'
react-native: '*'
react-native-screens: '*'
react-native@0.81.5:
resolution: {integrity: sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==}
engines: {node: '>= 20.19.4'}
@@ -12365,11 +12346,6 @@ snapshots:
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
warn-once: 0.1.1
react-native-toast-message@2.3.3(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/runtime': 7.28.4
@@ -12404,12 +12380,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
react-native-z-view@0.2.4(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0):
dependencies:
'@jest/create-cache-key-function': 29.7.0
+1 -1
View File
@@ -127,7 +127,7 @@ export default function Auth() {
// avatar: loginRes.data.avatar,
// })
// router.push('/auth/space')
// router.push('/space/manage')
// }
// }
// catch (err) {
@@ -0,0 +1,179 @@
import type { UserSpace } from '@/types/space'
import { useEffect, useState } from 'react'
import { Text, View } from 'react-native'
import { toast } from 'sonner-native'
import { 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 } 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 [localSpace, setLocalSpace] = useState(space)
const [showNameModal, setShowNameModal] = useState(false)
const [showMemberNameModal, setShowMemberNameModal] = useState(false)
useEffect(() => {
setLocalSpace(space)
}, [space])
return (
<>
<Actionsheet
isOpen={isOpen}
onClose={() => {
onClose()
}}
>
<ActionsheetBackdrop />
<ActionsheetContent
style={{
paddingBottom: 6,
}}
>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
{
space?.role === 0 && (
<ActionsheetItem>
<ActionsheetIcon
as={() => <LucideIcon icon="UserCog" />}
/>
<ActionsheetItemText>成员管理</ActionsheetItemText>
</ActionsheetItem>
)
}
{
space?.owner_id === userStore.userInfo.id && (
<ActionsheetItem onPress={() => {
setShowNameModal(true)
}}
>
<ActionsheetIcon
as={() => <LucideIcon icon="Pencil" />}
/>
<ActionsheetItemText>修改空间名称</ActionsheetItemText>
</ActionsheetItem>
)
}
<ActionsheetItem>
<ActionsheetIcon
as={() => <LucideIcon icon="Pencil" />}
/>
<ActionsheetItemText>修改我的空间昵称</ActionsheetItemText>
</ActionsheetItem>
<ActionsheetItem>
<ActionsheetIcon
as={() => <LucideIcon icon="Star" />}
/>
<ActionsheetItemText>
{space?.is_default === 1 ? '取消' : '设置'}
默认
</ActionsheetItemText>
</ActionsheetItem>
<ActionsheetItem onPress={() => {
if (space?.owner_id === userStore.userInfo.id) {
// 解散空间
dissolveSpaceApi({
space_id: space.space_id,
}).then((res) => {
if (res.isSuccess) {
toast.success('空间已解散')
onActionSuccess?.
}
},
)
}
else {
// 退出空间
}
}}
>
<ActionsheetIcon
as={() => <LucideIcon icon="LogOut" />}
/>
<ActionsheetItemText>
{space?.owner_id === userStore.userInfo.id ? '解散' : '退出'}
空间
</ActionsheetItemText>
</ActionsheetItem>
</ActionsheetContent>
</Actionsheet>
<AModal
title="修改空间名称"
showCloseIcon
body={
(
<View className="gap-2">
<Text>空间名称</Text>
<AInput
value={localSpace?.name || ''}
onChangeText={(val) => {
setLocalSpace(prev => prev
? ({
...prev,
name: val,
})
: null)
}}
className="flex-1"
/>
</View>
)
}
isOpen={showNameModal}
onCancel={() => {
setShowNameModal(false)
setLocalSpace(space)
}}
onClose={() => {
setShowNameModal(false)
setLocalSpace(space)
}}
onConfirm={async () => {
// 更新名称
if (!localSpace?.name) {
toast.warning('请输入空间名称')
}
}}
>
</AModal>
</>
)
}
@@ -1,5 +1,5 @@
import type { UserSpace } from '@/types/space'
import { useCallback, useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import Animated, { useAnimatedRef } from 'react-native-reanimated'
import Sortable from 'react-native-sortables'
@@ -11,12 +11,18 @@ import CustomHeader from '@/components/custom/Header'
import { Badge, BadgeText } from '@/components/ui/badge'
import { createSpaceApi, listUserSpacesApi } from '@/request/api/space/index'
import { useUserStore } from '@/store/user'
import SettingActions from './components/SettingActions'
export default function AuthSpace() {
const userStore = useUserStore()
// scrollView 相关
const scrollableRef = useAnimatedRef<Animated.ScrollView>()
const [scrollHeight, setScrollHeight] = useState(0)
const ITEM_HEIGHT = 64
// 空间列表
const [spaces, setSpaces] = useState<UserSpace[]>([])
const [selectedId, setSelectedId] = useState<number | null>(null)
// 新建
@@ -25,8 +31,11 @@ export default function AuthSpace() {
name: '',
})
const [spaces, setSpaces] = useState<UserSpace[]>([])
// 设置
const [showSettings, setShowSettings] = useState(false)
const [settingCurrentSpace, setSettingCurrentSpace] = useState<UserSpace | null>(null)
// 获取空间列表
function listUserSpaces() {
listUserSpacesApi().then((res) => {
if (res.isSuccess) {
@@ -36,12 +45,18 @@ export default function AuthSpace() {
}
const renderItem
= ({ item }: { item: UserSpace }) => (
= ({ item, index }: { item: UserSpace, index: number }) => (
<Pressable
key={item.id}
onPress={() => setSelectedId(item.id)}
key={item.space_id}
onPress={() => {
setSelectedId(item.space_id)
scrollToCenter(index)
}}
style={{
height: ITEM_HEIGHT,
}}
className={`rounded-md flex-row items-center justify-between px-4 py-3 border
${item.id === selectedId ? 'border-gray-700' : 'border-gray-300'}`}
${item.space_id === selectedId ? 'border-gray-700' : 'border-gray-300'}`}
>
<View className="flex-row items-center">
<Sortable.Handle>
@@ -83,7 +98,7 @@ export default function AuthSpace() {
<Text
className="text-xs"
>
{item.user_name}
{item.member_name}
</Text>
</View>
<View className="flex-row gap-1">
@@ -98,21 +113,55 @@ export default function AuthSpace() {
</View>
</View>
<LucideIcon
icon="Bolt"
size={20}
className="p-1"
/>
<View className="flex flex-row items-center gap-3">
<View className="w-8 h-8 rounded-full items-center justify-center bg-[#333333]">
<LucideIcon
icon="UserPlus"
size={18}
color="#fff"
/>
</View>
<Pressable
className="w-8 h-8 rounded-full items-center justify-center bg-[#333333]"
onPress={(e) => {
e.stopPropagation()
setSettingCurrentSpace(item)
setShowSettings(true)
}}
>
<LucideIcon
icon="Bolt"
size={18}
color="#fff"
clickable={false}
/>
</Pressable>
</View>
</Pressable>
)
const scrollToCenter = (index: number) => {
if (!scrollHeight)
return
const itemTop = index * ITEM_HEIGHT
const scrollY
= itemTop - scrollHeight / 2 + ITEM_HEIGHT / 2
scrollableRef.current?.scrollTo({
y: Math.max(0, scrollY),
animated: true,
})
}
useEffect(() => {
listUserSpaces()
}, [])
return (
<View className="flex-1 bg-white">
{/* Header */}
<CustomHeader title="空间列表" />
<View className="flex-1 px-4">
@@ -146,6 +195,9 @@ export default function AuthSpace() {
className="flex-1"
showsVerticalScrollIndicator={false}
ref={scrollableRef}
onLayout={(e) => {
setScrollHeight(e.nativeEvent.layout.height)
}}
>
{spaces.length === 0
? (
@@ -218,7 +270,7 @@ export default function AuthSpace() {
await createSpaceApi({
name: createForm.name,
user_name: userStore.userInfo.nickname,
member_name: userStore.userInfo.nickname,
sort_number: 0,
}).then((res) => {
if (res.isSuccess) {
@@ -227,11 +279,25 @@ export default function AuthSpace() {
setCreateForm({
name: '',
})
listUserSpaces()
}
})
}}
>
</AModal>
<SettingActions
isOpen={showSettings}
onClose={() => {
setShowSettings(false)
setTimeout(() => {
setSettingCurrentSpace(null)
}, 300)
}}
space={settingCurrentSpace}
/>
</View>
)
}
+4 -3
View File
@@ -2,9 +2,9 @@ import type { Method } from 'alova'
import type { AxiosError } from 'axios'
import { axiosRequestAdapter } from '@alova/adapter-axios'
import { createAlova } from 'alova'
import { toast } from 'sonner-native'
import { useNavStore } from '@/store/navigation'
import { useUserStore } from '@/store/user'
import { toast } from '@/utils/toast'
enum HttpCode {
Success = 1000,
@@ -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,
@@ -44,6 +44,7 @@ const alovaInstance = createAlova({
}
if (resData.code === HttpCode.Unauthorized) {
toast.error('登录状态已过期,请重新登录')
useUserStore.getState().clear()
useNavStore.getState().navigateToAuth()
return result
+12 -18
View File
@@ -23,28 +23,22 @@ const DEFAULT_NAV_MENUS: NavItem[] = [
{ label: '恋爱记', path: '/love', icon: 'Heart' },
{ label: '聊天', path: '/chat', icon: 'MessageCircle' },
{ label: '个人中心', path: '/user', icon: 'CircleUserRound' },
{ label: '我的空间', path: '/auth/space', icon: 'Earth' },
{ label: '我的空间', path: '/space/manage', icon: 'Earth' },
]
export const useNavStore = create<NavState>()(
persist(
(set, get) => ({
pathname: '/',
navMenus: DEFAULT_NAV_MENUS,
(set, get) => ({
pathname: '/',
navMenus: DEFAULT_NAV_MENUS,
setPathname: (p: string) => set({ pathname: p }),
navigateToAuth: () => {
const pathname = useNavStore.getState().pathname
setPathname: (p: string) => set({ pathname: p }),
navigateToAuth: () => {
const pathname = useNavStore.getState().pathname
if (pathname !== '/auth') {
router.replace('/auth')
}
},
setNavMenus: navMenus => set({ navMenus }),
}),
{
name: 'nav-storage',
storage: createJSONStorage(() => AsyncStorage),
if (pathname !== '/auth') {
router.replace('/auth')
}
},
),
setNavMenus: navMenus => set({ navMenus }),
}),
)
@@ -1,21 +1,22 @@
export interface UserSpace {
id: number
space_id: number
owner_id: number
name: string
role: number
member_count: number
is_default: number
user_name: string
member_name: string
sort_number: number
}
// 创建空间
export interface CreateSpaceParams {
name: string
user_name: string
member_name: string
sort_number: number
}
export interface CreateSpaceResult {
space_id: number
invite_code: string
@@ -37,6 +38,7 @@ export interface DissolveSpaceResult {
export interface JoinSpaceParams {
invite_code: string
}
export interface JoinSpaceResult {
space_id: number
space_name: string