This commit is contained in:
oneao committed 2025-12-04 17:31:33 +08:00
1 parent 4dcac7b5a0
commit 59d3be2299
25 files changed
+380 -125

No files matched your search

@@ -7,6 +7,7 @@ import { IconQq, IconWeixin, IconZhiwen } from '@/components/iconfont'
import { Divider } from '@/components/ui/divider'
import useFormValidation from '@/hooks/useFormValidation'
import { loginDefaultApi } from '@/request/api/auth'
import { useMoneyCategoryStore } from '@/store/moneyCategory'
import { useUserStore } from '@/store/user'
import { toast } from '@/utils/toast'
@@ -20,6 +21,7 @@ interface LoginForm {
export default function AuthLogin() {
const router = useRouter()
const userStore = useUserStore()
const moneyCategoryStore = useMoneyCategoryStore()
const [loading, setLoading] = useState<boolean>(false)
const [form, setForm] = useState<LoginForm>({
@@ -60,6 +62,7 @@ export default function AuthLogin() {
refreshToken: res.data.refreshToken,
rememberMe: form.rememberMe,
})
moneyCategoryStore.init()
router.replace('/user')
}
@@ -6,10 +6,12 @@ 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({
@@ -22,11 +24,32 @@ export default function JoinSpace({
name="username"
label="邀请码"
>
<AInput placeholder="请输入邀请码"></AInput>
<AInput
placeholder="请输入邀请码"
value={form.code}
onChangeText={text => setForm({
...form,
code: text,
})}
>
</AInput>
</AFormItem>
</AForm>
<AButton className="w-full">加入空间</AButton>
<AButton
className="w-full"
onPress={() => {
if (form.code === '') {
//
}
else {
onJoin?.(form.code)
}
}}
>
加入空间
</AButton>
<View
className="flex-row items-center w-full"
style={{
@@ -1,3 +1,4 @@
import type { UserSpace } from '@/types/space/spaceUser'
import { Text, View } from 'react-native'
import { AButton, ADivider, LucideIcon } from '@/components/common/ao'
@@ -1,9 +1,10 @@
import type { SpaceUser, UserSpace } from '@/types/space/spaceUser'
import * as Clipboard from 'expo-clipboard'
import { useState } from 'react'
import { FlatList, Pressable, TouchableOpacity, View } from 'react-native'
import QRCode from 'react-native-qrcode-svg'
import { AButton, ADivider, LucideIcon } from '@/components/common/ao'
import { AButton, ADivider, LucideIcon } from '@/components/common/ao'
import {
Accordion,
AccordionContent,
@@ -25,16 +26,22 @@ import { ChevronDownIcon, ChevronUpIcon } from '@/components/ui/icon'
import { Menu, MenuItem, MenuItemLabel } from '@/components/ui/menu'
import { Text } from '@/components/ui/text'
import { VStack } from '@/components/ui/vstack'
import { useUserStore } from '@/store/user'
import { formatRelativeTime } from '@/utils/time'
import { toast } from '@/utils/toast'
export default function SpaceInviteAndMembers() {
const [accordion, setAccordion] = useState(['a'])
interface SpaceInviteProps {
userSpace: UserSpace | null
spaceUsers: SpaceUser[]
}
const members = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' },
]
export default function SpaceInvite({
userSpace,
spaceUsers,
}: SpaceInviteProps) {
const userStore = useUserStore()
const [accordion, setAccordion] = useState(['a'])
async function copyInviteCode() {
await Clipboard.setStringAsync('hello world')
@@ -94,7 +101,7 @@ export default function SpaceInviteAndMembers() {
copyInviteCode()
}}
>
<Text className="font-bold text-3xl tracking-[4px] mr-3">123456</Text>
<Text className="font-bold text-3xl tracking-[4px] mr-3">{userSpace?.invite_code}</Text>
<LucideIcon icon="Copy" size={16} color="blue" clickable={false} />
</TouchableOpacity>
@@ -102,7 +109,7 @@ export default function SpaceInviteAndMembers() {
<ADivider direction="horizontal" text="扫描二维码" />
<QRCode
size={120}
value="http://awesome.link.qr"
value={userSpace?.invite_code}
/>
</View>
</AccordionContent>
@@ -112,31 +119,50 @@ export default function SpaceInviteAndMembers() {
<View className="flex flex-row items-center justify-between">
<Text className="font-semibold text-lg">空间成员</Text>
<Badge size="lg" variant="solid" action="muted">
<BadgeText>3 人</BadgeText>
<BadgeText>
{spaceUsers.length}
{' '}
人
</BadgeText>
</Badge>
</View>
<View>
<FlatList
data={members}
keyExtractor={item => item.id.toString()}
data={spaceUsers}
keyExtractor={item => item.user_id.toString()}
renderItem={({ item }) => (
<TouchableOpacity>
<HStack space="md" className="my-2 items-center">
<Avatar className="bg-indigo-600">
<AvatarFallbackText className="text-white text-lg">
{item.name}
<AvatarFallbackText className="text-white text-2xl">
{item.username}
</AvatarFallbackText>
<AvatarBadge />
</Avatar>
<VStack>
<HStack space="sm">
<Heading size="sm">{item.name}</Heading>
<Badge size="sm" variant="solid" action="muted">
<BadgeText>管理员</BadgeText>
</Badge>
<Heading size="sm">{item.username}</Heading>
{[0, 1].includes(item.space_role) && (
<Badge size="sm" variant="solid" action="muted">
<BadgeText>
{item.space_role === 0 ? '创建者' : '管理员'}
</BadgeText>
</Badge>
)}
{
(userStore.userInfo?.id === item.user_id) && (
<Badge size="sm" variant="solid" action='info'>
<BadgeText>
我
</BadgeText>
</Badge>
)
}
</HStack>
<Text size="sm">Marketing Coordinator</Text>
<Text size="sm" className="mt-1">{formatRelativeTime(item.active_time)}</Text>
</VStack>
<View className="ml-auto">
@@ -1,18 +1,87 @@
import type { SpaceUser, UserSpace } from '@/types/space/spaceUser'
import { useEffect, useState } from 'react'
import { View } from 'react-native'
import CustomHeader from '@/components/custom/Header'
import { createSpaceApi } from '@/request/api/space/space'
import { getUserSpaceApi, joinSpaceApi, listSpaceUsersApi } from '@/request/api/space/spaceUser'
import JoinSpace from './components/JoinSpace'
import SpaceHome from './components/SpaceHome'
import SpaceInviteAndMembers from './components/SpaceInviteAndMembers'
import SpaceInviteAndMembers from './components/SpaceInvite'
import StepFade from './components/StepFade'
export default function PagerWithTab() {
const [step, setStep] = useState<'join' | 'invite' | 'home'>('home')
const [userSpace, setUserSpace] = useState<UserSpace | null>(null)
const [spaceUsers, setSpaceUsers] = useState<SpaceUser[]>([])
const fetchData = async () => {
await Promise.all([
getUserSpaceApi({})
.then((res) => {
if (res.isSuccess) {
if (res.data === null) {
setUserSpace(null)
setStep('home')
}
else {
setUserSpace({
space_id: res.data.space_id,
space_name: res.data.space_name,
role: res.data.role,
invite_code: res.data.invite_code,
})
setStep('invite')
}
}
else {
// 提示
}
})
.catch(err => console.error('getUserSpaceApi error:', err)),
listSpaceUsersApi({})
.then((res) => {
if (res.isSuccess) {
setSpaceUsers(res.data.users)
}
else {
// 提示
}
})
.catch(err => console.error('listSpaceUsersApi error:', err)),
])
}
useEffect(() => {
fetchData()
}, [])
function createSpace() {
createSpaceApi({}).then((res) => {
if (res.isSuccess) {
setUserSpace({
space_id: res.data.id,
space_name: '',
role: 0,
invite_code: res.data.invite_code,
})
setStep('invite')
}
}).catch((err) => {
console.log(err)
})
}
function joinSpace(code: string) {
joinSpaceApi({
invite_code: code,
}).then((res) => {
if (res.isSuccess) {
fetchData()
}
}).catch((err) => {
console.log(err)
})
}
return (
@@ -21,9 +90,7 @@ export default function PagerWithTab() {
<View className="flex-1">
<StepFade visible={step === 'home'}>
<SpaceHome
onCreate={() => {
setStep('invite')
}}
onCreate={createSpace}
onJoin={() => {
setStep('join')
}}
@@ -35,11 +102,16 @@ export default function PagerWithTab() {
onBack={() => {
setStep('home')
}}
onJoin={(code) => {
if (code !== '') {
joinSpace(code)
}
}}
/>
</StepFade>
<StepFade visible={step === 'invite'}>
<SpaceInviteAndMembers />
<SpaceInviteAndMembers userSpace={userSpace} spaceUsers={spaceUsers} />
</StepFade>
</View>
</View>
@@ -61,7 +61,7 @@ export default function CustomHeader(props: CustomHeaderProps) {
{...screenProps}
options={{
...screenProps?.options,
headerShown: true,
headerShown: false,
header: () => (
<SafeAreaView
className={`${className}`}
@@ -31,8 +31,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
baseURL: 'http://192.168.5.6:8086/app',
// baseURL: 'http://192.168.1.136:8086/app',
// baseURL: 'http://192.168.5.6:8086/app',
baseURL: 'http://192.168.1.136:8086/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,
@@ -1,3 +1,10 @@
export interface UserSpace {
space_id: number
space_name: string
role: number
invite_code: string
}
// 获取用户的空间
export interface GetUserSpaceParams {
@@ -22,6 +29,7 @@ export interface SpaceUser {
user_id: number
username: string
space_role: number
active_time: string
}
export interface ListSpaceUsersParams {
@@ -68,3 +68,40 @@ export function isInMonth(date: string, month: string): boolean {
// month 形如 "2025-01"
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}年前`
}
}