This commit is contained in:
oneao committed 2026-03-02 17:29:51 +08:00
1 parent ab3eaed7ce
commit cf23acdabc
28 files changed
+1292 -486

No files matched your search

+29 -64
View File
@@ -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 (
<KeyboardAvoidingView
behavior="padding"
keyboardVerticalOffset={100}
style={styles.content}
>
<View style={styles.inner}>
<Text style={styles.heading}>Header</Text>
<View>
<TextInput placeholder="Username" style={styles.textInput} />
<TextInput placeholder="Password" style={styles.textInput} />
</View>
<TouchableOpacity style={styles.button}>
<Text style={styles.text}>Submit</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<AButton onPress={() => {
setShowDissolve(true)
}}
>
测试
</AButton>
<AModal
title="提示"
body="确认解散该空间么?"
isOpen={showDissolve}
onClose={() => {
setShowDissolve(false)
}}
onCancel={() => {
setShowDissolve(false)
}}
onConfirm={() => {
setShowDissolve(false)
}}
/>
</View>
)
}
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',
},
})
+1 -1
View File
@@ -87,7 +87,7 @@ export default function RootLayout() {
</GluestackUIProvider>
</KeyboardProvider>
</MenuProvider>
<Toaster />
<Toaster duration={1000} />
</GestureHandlerRootView>
)
}
@@ -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<any>(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 (
<>
<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>
<Actionsheet
isOpen={showNameModal}
onClose={() => {
setShowNameModal(false)
}}
>
<KeyboardAvoidingView
behavior="position"
>
<ActionsheetBackdrop />
<ActionsheetContent>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
<View className="pb-6">
<View className="items-center mb-6">
<Text className="text-lg font-semibold">空间名称</Text>
</View>
<View className="w-full mb-4">
<AInput ref={inputRef} className="w-full" placeholder="请输入空间名称" />
</View>
<AButton className="rounded-full">保存</AButton>
</View>
</ActionsheetContent>
</KeyboardAvoidingView>
</Actionsheet>
</>
)
}
@@ -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<Animated.ScrollView>()
@@ -23,23 +31,30 @@ export default function AuthSpace() {
// 空间列表
const [spaces, setSpaces] = useState<UserSpace[]>([])
const [selectedId, setSelectedId] = useState<number | null>(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<UserSpace | null>(null)
const createInputRef = useRef<any>(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() {
<Pressable
key={item.space_id}
onPress={() => {
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' : ''
}`}
>
<View className="flex-row items-center">
<Sortable.Handle>
<LucideIcon
icon="Grip"
size={20}
className="mr-4"
/>
</Sortable.Handle>
<View className="w-full flex-row items-center justify-between">
<View>
<View className="flex-row items-center gap-2">
<Text
@@ -77,16 +91,16 @@ export default function AuthSpace() {
{
item.owner_id === userStore.userInfo.id && (
<Badge size="sm" variant="solid" action="muted">
<BadgeText>创建者</BadgeText>
<Badge size="sm" className="bg-[#333] rounded-full">
<BadgeText className="text-white">创建者</BadgeText>
</Badge>
)
}
{
item.is_default === 1 && (
<Badge size="sm" variant="solid" action="info">
<BadgeText>默认</BadgeText>
<Badge size="sm" className="bg-[#333] rounded-full">
<BadgeText className="text-white">默认</BadgeText>
</Badge>
)
}
@@ -112,33 +126,15 @@ export default function AuthSpace() {
</View>
</View>
</View>
<Sortable.Handle>
<LucideIcon
icon="Grip"
size={20}
/>
</Sortable.Handle>
</View>
<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>
)
@@ -157,16 +153,17 @@ export default function AuthSpace() {
})
}
useEffect(() => {
listUserSpaces()
}, [])
useFocusEffect(
useCallback(() => {
listUserSpaces()
}, []),
)
return (
<View className="flex-1 bg-white">
<View className="flex-1 bg-[#f5f6f8]">
<CustomHeader title="空间列表" />
<View className="flex-1 px-4">
<View className="flex-1 p-4">
<View className="">
<Text className="text-sm text-gray-500 mb-2">
选择或创建空间
@@ -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) {
// 更新成功
}
})
}
}}
/>
)}
</Animated.ScrollView>
<View className="py-2">
<AButton size="sm" leftIcon={<LucideIcon icon="CircleArrowRight" color="#fff" />}>进入空间</AButton>
</View>
</View>
<AModal
title="创建新空间"
showCloseIcon
body={
(
<View className="gap-2">
<Text>空间名称</Text>
<AInput
className="flex-1"
value={createForm.name}
onChangeText={(val) => {
setCreateForm(prev => ({
...prev,
name: val,
}))
}}
/>
</View>
)
}
<Actionsheet
isOpen={createOpen}
onCancel={() => {
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()
}
})
}}
>
</AModal>
<KeyboardAvoidingView
behavior="position"
>
<ActionsheetBackdrop />
<SettingActions
isOpen={showSettings}
onClose={() => {
setShowSettings(false)
<ActionsheetContent>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
setTimeout(() => {
setSettingCurrentSpace(null)
}, 300)
}}
space={settingCurrentSpace}
/>
<View className="pb-6">
<View className="items-center mb-6">
<Text className="text-lg font-semibold">创建新空间</Text>
</View>
<View className="w-full mb-4">
<AInput
ref={createInputRef}
value={createForm.name}
onChangeText={(val) => {
setCreateForm(prev => ({
...prev,
name: val,
}))
}}
className="w-full"
placeholder="请输入空间名称"
/>
</View>
<AButton
className="rounded-full"
loading={createLoading}
onPress={() => {
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)
}
}}
>
创建
</AButton>
</View>
</ActionsheetContent>
</KeyboardAvoidingView>
</Actionsheet>
</View>
)
}
@@ -1,13 +0,0 @@
import { View } from 'react-native'
import CustomHeader from '@/components/custom/Header'
export default function Settings() {
return (
<View className="flex-1">
<CustomHeader title="空间设置" />
<View></View>
</View>
)
}
@@ -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 (
<>
<TouchableOpacity
className="flex-row items-center justify-between px-4 h-12"
onPress={onPress}
>
<View className="flex-row items-center gap-3">
<LucideIcon icon={icon} size={18} strokeWidth={1.5} />
<Text className="text-sm text-gray-900">{title}</Text>
</View>
<View className="flex-row items-center gap-1">
{right}
{showRightIcon && (
<LucideIcon icon="ChevronRight" size={16} color="#9ca3af" />
)}
</View>
</TouchableOpacity>
{!isLast && <View className="h-px bg-gray-100 ml-14" />}
</>
)
}
export default function Settings() {
const userStore = useUserStore()
const params = useLocalSearchParams<{ spaceId: string }>()
const { spaceId } = params
const [space, setSpace] = useState<UserSpace | null>(null)
// 空间名称
const [showName, setShowName] = useState(false)
const [nameForm, setNameForm] = useState({
name: '',
})
const [nameLoading, setNameLoading] = useState(false)
const nameInputRef = useRef<any>(null)
// 空间昵称
const [showMemberName, setShowMemberName] = useState(false)
const [memberNameForm, setMemberNameForm] = useState({
name: '',
})
const [memberNameLoading, setMemberNameLoading] = useState(false)
const memberNameInputRef = useRef<any>(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 (
<View className="flex-1 bg-[#f5f6f8]">
<CustomHeader title="空间设置" />
{
!space
? (
<EmptyState title="正在加载空间中 . . ." />
)
: (
<View className="flex-1 px-4 pt-4">
<Text className="text-sm font-semibold text-gray-400 mb-2 tracking-wider">
常用设置
</Text>
<View className="bg-white rounded-xl overflow-hidden mb-6">
{
space?.owner_id === userStore.userInfo.id && (
<Item
icon="Earth"
title="空间名称"
right={(
<Text className="text-sm text-gray-400">{space.name}</Text>
)}
onPress={() => {
setNameForm({
name: space.name,
})
setShowName(true)
}}
/>
)
}
<Item
icon="User"
title="我的空间昵称"
right={(
<Text className="text-sm text-gray-400">{space.member_name}</Text>
)}
onPress={() => {
setMemberNameForm({
name: space.member_name,
})
setShowMemberName(true)
}}
/>
<Item
icon="Star"
title="设为默认空间"
isLast
showRightIcon={false}
right={(
<ASwitch
value={space.is_default}
trueValue={1}
falseValue={0}
disabled={defaultLoading}
onChange={(val) => {
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)
}
}}
/>
)}
/>
</View>
<Text className="text-sm font-semibold text-gray-400 mb-2 tracking-wider">
其它操作
</Text>
<View className="bg-white rounded-xl overflow-hidden">
<Item
icon="Users"
title="成员管理"
/>
<Item
icon="LogOut"
title={userStore.userInfo.id === space.owner_id ? '解散空间' : '退出空间'}
isLast
onPress={() => {
setShowDissolve(true)
}}
/>
</View>
</View>
)
}
{/* 空间名称 */}
<Actionsheet
isOpen={showName}
onClose={() => {
setShowName(false)
setNameForm(prev => ({
...prev,
name: '',
}))
}}
>
<KeyboardAvoidingView
behavior="position"
>
<ActionsheetBackdrop />
<ActionsheetContent>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
<View className="pb-6">
<View className="items-center mb-6">
<Text className="text-lg font-semibold">空间名称</Text>
</View>
<View className="w-full mb-4">
<AInput
ref={nameInputRef}
value={nameForm.name}
onChangeText={text => setNameForm(prev => prev
? {
...prev,
name: text,
}
: null)}
className="w-full"
placeholder="请输入空间名称"
/>
</View>
<AButton
className="rounded-full"
loading={nameLoading}
onPress={() => {
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)
}
}}
>
保存
</AButton>
</View>
</ActionsheetContent>
</KeyboardAvoidingView>
</Actionsheet>
{/* 空间昵称 */}
<Actionsheet
isOpen={showMemberName}
onClose={() => {
setShowMemberName(false)
setMemberNameForm(prev => ({
...prev,
name: '',
}))
}}
>
<KeyboardAvoidingView
behavior="position"
>
<ActionsheetBackdrop />
<ActionsheetContent>
<ActionsheetDragIndicatorWrapper>
<ActionsheetDragIndicator />
</ActionsheetDragIndicatorWrapper>
<View className="pb-6">
<View className="items-center mb-6">
<Text className="text-lg font-semibold">我的空间昵称</Text>
</View>
<View className="w-full mb-4">
<AInput
ref={memberNameInputRef}
value={memberNameForm.name}
onChangeText={text => setMemberNameForm(prev => prev
? {
...prev,
name: text,
}
: null)}
className="w-full"
placeholder="请输入空间昵称"
/>
</View>
<AButton
className="rounded-full"
loading={memberNameLoading}
onPress={() => {
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)
}
}}
>
保存
</AButton>
</View>
</ActionsheetContent>
</KeyboardAvoidingView>
</Actionsheet>
{/* 解散 */}
<AModal
title="提示"
body="确认解散该空间么?"
isOpen={showDissolve}
onClose={() => {
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)
}
})
}}
/>
</View>
)
}
@@ -30,7 +30,6 @@ export default function EmptyState({
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'white',
padding: 16,
}}
showsVerticalScrollIndicator={true}
@@ -47,6 +47,7 @@ const AInput = forwardRef<React.ComponentRef<typeof InputField>, AInputProps>(
isFocused={isFocused}
className={className}
style={style}
>
{leftIcon && (
<InputSlot className="pl-3">
@@ -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> | 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,
}}
>
<ModalHeader>
{title && (typeof title === 'string' ? <Heading>{title}</Heading> : title)}
{
showCloseIcon && (
<ModalCloseButton>
<Icon as={CloseIcon} />
</ModalCloseButton>
)
}
</ModalHeader>
<ModalBody>
{body && (typeof body === 'string' ? <Text>{body}</Text> : body)}
</ModalBody>
{title && (
<ModalHeader>
{typeof body === 'string'
? (
<View className="items-center justify-center w-full py-2">
<Heading>{title}</Heading>
</View>
)
: (
title
)}
</ModalHeader>
)}
{body && (
<ModalBody>
{typeof body === 'string'
? (
<View className="items-center justify-center w-full">
<Text>{body}</Text>
</View>
)
: (
body
)}
</ModalBody>
)}
<ModalFooter>
{
footer || (
<>
<AButton
variant="outline"
action="secondary"
className="mr-3"
<View className="flex-row border-t border-gray-300">
<TouchableOpacity
className="flex-1 items-center justify-center py-3 border-r border-gray-300"
onPress={onClose}
size="sm"
>
取消
</AButton>
<AButton
<Text className="text-gray-500">取消</Text>
</TouchableOpacity>
<TouchableOpacity
className="flex-1 flex-row gap-2 items-center justify-center py-3"
onPress={handleConfirm}
size="sm"
loading={innerLoading}
>
确认
</AButton>
</>
{
innerLoading && (
<ButtonSpinner color="#333" />
)
}
<Text className="text-[#333]">确认</Text>
</TouchableOpacity>
</View>
)
}
</ModalFooter>
@@ -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 (
<Pressable
onPress={() => {
if (disabled)
return
const nextValue = isActive ? falseVal : trueVal
onChange(nextValue)
}}
style={{ width, height }}
>
<Animated.View
style={[
styles.track,
{
width,
height,
borderRadius: height / 2,
},
trackStyle,
]}
>
<Animated.View
style={[
styles.knob,
{
width: knobSize,
height: knobSize,
borderRadius: knobSize / 2,
},
knobStyle,
]}
/>
</Animated.View>
</Pressable>
)
}
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 },
},
})
@@ -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,
}
@@ -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<typeof UISwitch> &
VariantProps<typeof switchStyle>;
const Switch = React.forwardRef<
React.ComponentRef<typeof UISwitch>,
ISwitchProps
>(function Switch({ className, size = 'md', ...props }, ref) {
return (
<UISwitch
ref={ref}
{...props}
className={switchStyle({ size, class: className })}
/>
);
});
Switch.displayName = 'Switch';
export { Switch };
@@ -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<ResponseData<void>>(`${API_PREFIX}/name`, params)
}
// 修改空间昵称
export function updateSpaceMemberNameApi(params: UpdateSpaceMemberNameParams) {
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/member/name`, params)
}
// 修改空间是否默认
export function updateSpaceDefaultApi(params: UpdateSpaceDefaultParams) {
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/member/default`, params)
}
// 修改空间排列顺序
export function updateSpaceSortNumberApi(params: UpdateSpaceSortNumberParams) {
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/member/sortNumber`, params)
}
// 获取空间设置信息
export function getSpaceSettingsApi(params: GetSpaceSettingsParams) {
return alovaInstance.Post<ResponseData<UserSpace>>(`${API_PREFIX}/settings`, params)
}
// Join Space API
export function joinSpaceApi(params: JoinSpaceParams) {
return alovaInstance.Post<ResponseData<JoinSpaceResult>>(`${API_PREFIX}/join`, params)
+2 -2
View File
@@ -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,
+26 -2
View File
@@ -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 {